From d83a0b680854f52ce30205d3558c0133d7f8cf19 Mon Sep 17 00:00:00 2001 From: Florent Vilmart <364568+flovilmart@users.noreply.github.com> Date: Sat, 1 Sep 2018 13:58:06 -0400 Subject: [PATCH] Use Prettier JS (#5017) * Adds prettier * Run lint before tests --- .eslintrc.json | 2 +- .prettierrc | 3 + .travis.yml | 1 + package-lock.json | 1093 +++- package.json | 17 +- spec/AccountLockoutPolicy.spec.js | 197 +- spec/AdaptableController.spec.js | 47 +- spec/AdapterLoader.spec.js | 90 +- spec/AggregateRouter.spec.js | 61 +- spec/Analytics.spec.js | 102 +- spec/AudienceRouter.spec.js | 382 +- spec/Auth.spec.js | 71 +- spec/AuthenticationAdapters.spec.js | 452 +- spec/CLI.spec.js | 144 +- spec/CacheController.spec.js | 17 +- spec/Client.spec.js | 82 +- spec/ClientSDK.spec.js | 44 +- spec/CloudCode.spec.js | 1636 +++--- spec/CloudCodeLogger.spec.js | 118 +- spec/DatabaseController.spec.js | 58 +- spec/EmailVerificationToken.spec.js | 725 +-- spec/EnableExpressErrorHandler.spec.js | 66 +- spec/EnableSingleSchemaCache.spec.js | 67 +- spec/EventEmitterPubSub.spec.js | 3 +- spec/FilesController.spec.js | 115 +- spec/GridStoreAdapter.js | 74 +- spec/HTTPRequest.spec.js | 314 +- spec/InMemoryCache.spec.js | 17 +- spec/InMemoryCacheAdapter.spec.js | 36 +- spec/InstallationsRouter.spec.js | 192 +- spec/JobSchedule.spec.js | 206 +- spec/Logger.spec.js | 40 +- spec/LoggerController.spec.js | 82 +- spec/LogsRouter.spec.js | 189 +- spec/Middlewares.spec.js | 118 +- spec/MockAdapter.js | 2 +- spec/MockEmailAdapter.js | 4 +- spec/MockEmailAdapterWithOptions.js | 10 +- spec/MockPushAdapter.js | 2 +- spec/MongoSchemaCollectionAdapter.spec.js | 79 +- spec/MongoStorageAdapter.spec.js | 117 +- spec/MongoTransform.spec.js | 362 +- spec/NullCacheAdapter.spec.js | 21 +- spec/OAuth1.spec.js | 151 +- spec/Parse.Push.spec.js | 623 ++- spec/ParseACL.spec.js | 557 +- spec/ParseAPI.spec.js | 1777 +++--- spec/ParseCloudCodePublisher.spec.js | 31 +- spec/ParseFile.spec.js | 1179 ++-- spec/ParseGeoPoint.spec.js | 781 +-- spec/ParseGlobalConfig.spec.js | 319 +- spec/ParseHooks.spec.js | 1029 ++-- spec/ParseInstallation.spec.js | 1156 ++-- spec/ParseLiveQueryServer.spec.js | 735 ++- spec/ParseObject.spec.js | 2596 +++++---- spec/ParsePolygon.spec.js | 568 +- spec/ParsePubSub.spec.js | 92 +- spec/ParseQuery.Aggregate.spec.js | 1238 +++-- spec/ParseQuery.FullTextSearch.spec.js | 937 ++-- spec/ParseQuery.spec.js | 4788 +++++++++-------- spec/ParseRelation.spec.js | 1122 ++-- spec/ParseRole.spec.js | 928 ++-- spec/ParseServer.spec.js | 39 +- spec/ParseServerRESTController.spec.js | 292 +- spec/ParseSession.spec.js | 196 +- spec/ParseUser.spec.js | 4395 ++++++++------- spec/ParseWebSocket.spec.js | 12 +- spec/ParseWebSocketServer.spec.js | 16 +- spec/PasswordPolicy.spec.js | 1981 ++++--- spec/PointerPermissions.spec.js | 1229 +++-- spec/PostgresConfigParser.spec.js | 29 +- spec/PostgresInitOptions.spec.js | 85 +- spec/PostgresStorageAdapter.spec.js | 43 +- spec/PromiseRouter.spec.js | 50 +- spec/PublicAPI.spec.js | 172 +- spec/PurchaseValidation.spec.js | 383 +- spec/PushController.spec.js | 1896 ++++--- spec/PushQueue.spec.js | 42 +- spec/PushRouter.spec.js | 90 +- spec/PushWorker.spec.js | 534 +- spec/QueryTools.spec.js | 149 +- spec/ReadPreferenceOption.spec.js | 493 +- spec/RedisCacheAdapter.spec.js | 34 +- spec/RedisPubSub.spec.js | 13 +- spec/RestQuery.spec.js | 449 +- spec/RevocableSessionsUpgrade.spec.js | 158 +- spec/Schema.spec.js | 1839 ++++--- spec/SchemaCache.spec.js | 67 +- spec/SessionTokenCache.spec.js | 39 +- spec/Subscription.spec.js | 69 +- spec/TwitterAuth.spec.js | 113 +- spec/Uniqueness.spec.js | 74 +- spec/UserController.spec.js | 87 +- spec/UserPII.spec.js | 599 ++- spec/ValidationAndPasswordsReset.spec.js | 967 ++-- spec/VerifyUserPassword.spec.js | 856 +-- spec/WinstonLoggerAdapter.spec.js | 133 +- spec/batch.spec.js | 28 +- spec/cryptoUtils.spec.js | 8 +- spec/features.spec.js | 27 +- spec/helper.js | 180 +- spec/index.spec.js | 489 +- spec/myoauth.js | 4 +- spec/parsers.spec.js | 24 +- spec/rest.spec.js | 868 +-- spec/schemas.spec.js | 4202 ++++++++------- spec/support/CustomAuth.js | 5 +- spec/support/CustomAuthFunction.js | 7 +- spec/support/CustomMiddleware.js | 2 +- spec/testing-routes.js | 49 +- src/AccountLockout.js | 91 +- src/Adapters/AdapterLoader.js | 6 +- src/Adapters/Analytics/AnalyticsAdapter.js | 1 - src/Adapters/Auth/AuthAdapter.js | 1 - src/Adapters/Auth/OAuth1Client.js | 172 +- src/Adapters/Auth/facebook.js | 43 +- src/Adapters/Auth/facebookaccountkit.js | 63 +- src/Adapters/Auth/github.js | 26 +- src/Adapters/Auth/google.js | 57 +- src/Adapters/Auth/httpsRequest.js | 4 +- src/Adapters/Auth/index.js | 68 +- src/Adapters/Auth/instagram.js | 14 +- src/Adapters/Auth/janraincapture.js | 19 +- src/Adapters/Auth/janrainengage.js | 33 +- src/Adapters/Auth/linkedin.js | 32 +- src/Adapters/Auth/meetup.js | 24 +- src/Adapters/Auth/qq.js | 30 +- src/Adapters/Auth/spotify.js | 45 +- src/Adapters/Auth/twitter.js | 38 +- src/Adapters/Auth/vkontakte.js | 59 +- src/Adapters/Auth/wechat.js | 11 +- src/Adapters/Auth/weibo.js | 15 +- src/Adapters/Cache/InMemoryCache.js | 10 +- src/Adapters/Cache/InMemoryCacheAdapter.js | 5 +- src/Adapters/Cache/LRUCache.js | 10 +- src/Adapters/Cache/NullCacheAdapter.js | 5 +- src/Adapters/Cache/RedisCacheAdapter.js | 11 +- src/Adapters/Files/FilesAdapter.js | 11 +- src/Adapters/Files/GridStoreAdapter.js | 70 +- src/Adapters/Logger/LoggerAdapter.js | 2 +- src/Adapters/Logger/WinstonLogger.js | 69 +- src/Adapters/Logger/WinstonLoggerAdapter.js | 7 +- src/Adapters/MessageQueue/EventEmitterMQ.js | 12 +- src/Adapters/PubSub/EventEmitterPubSub.js | 12 +- src/Adapters/PubSub/PubSubAdapter.js | 2 +- src/Adapters/PubSub/RedisPubSub.js | 12 +- src/Adapters/Push/PushAdapter.js | 2 +- src/Adapters/Storage/Mongo/MongoCollection.js | 101 +- .../Storage/Mongo/MongoSchemaCollection.js | 211 +- .../Storage/Mongo/MongoStorageAdapter.js | 614 ++- src/Adapters/Storage/Mongo/MongoTransform.js | 1857 ++++--- .../Storage/Postgres/PostgresClient.js | 1 - .../Storage/Postgres/PostgresConfigParser.js | 25 +- .../Postgres/PostgresStorageAdapter.js | 1360 +++-- src/Adapters/Storage/Postgres/sql/index.js | 9 +- src/Adapters/Storage/StorageAdapter.js | 93 +- src/Auth.js | 243 +- src/ClientSDK.js | 12 +- src/Config.js | 167 +- src/Controllers/AdaptableController.js | 34 +- src/Controllers/AnalyticsController.js | 34 +- src/Controllers/CacheController.js | 4 +- src/Controllers/DatabaseController.js | 1115 ++-- src/Controllers/FilesController.js | 22 +- src/Controllers/HooksController.js | 165 +- src/Controllers/LiveQueryController.js | 4 +- src/Controllers/LoggerController.js | 100 +- src/Controllers/PushController.js | 190 +- src/Controllers/SchemaCache.js | 29 +- src/Controllers/SchemaController.js | 1119 ++-- src/Controllers/UserController.js | 262 +- src/Controllers/index.js | 215 +- src/Controllers/types.js | 30 +- src/LiveQuery/Client.js | 40 +- src/LiveQuery/ParseCloudCodePublisher.js | 16 +- src/LiveQuery/ParseLiveQueryServer.js | 438 +- src/LiveQuery/ParsePubSub.js | 28 +- src/LiveQuery/ParseWebSocketServer.js | 12 +- src/LiveQuery/QueryTools.js | 258 +- src/LiveQuery/RequestSchema.js | 202 +- src/LiveQuery/SessionTokenCache.js | 60 +- src/LiveQuery/Subscription.js | 10 +- src/LiveQuery/equalObjects.js | 4 +- src/Options/Definitions.js | 794 +-- src/Options/docs.js | 1 - src/Options/index.js | 54 +- src/Options/parsers.js | 14 +- src/ParseMessageQueue.js | 24 +- src/ParseServer.js | 173 +- src/ParseServerRESTController.js | 79 +- src/PromiseRouter.js | 128 +- src/Push/PushQueue.js | 64 +- src/Push/PushWorker.js | 77 +- src/Push/utils.js | 88 +- src/RestQuery.js | 545 +- src/RestWrite.js | 1178 ++-- src/Routers/AggregateRouter.js | 52 +- src/Routers/AnalyticsRouter.js | 5 +- src/Routers/AudiencesRouter.js | 77 +- src/Routers/ClassesRouter.js | 129 +- src/Routers/CloudCodeRouter.js | 118 +- src/Routers/FeaturesRouter.js | 107 +- src/Routers/FilesRouter.js | 161 +- src/Routers/FunctionsRouter.js | 157 +- src/Routers/GlobalConfigRouter.js | 46 +- src/Routers/HooksRouter.js | 143 +- src/Routers/IAPValidationRouter.js | 137 +- src/Routers/InstallationsRouter.js | 40 +- src/Routers/LogsRouter.js | 31 +- src/Routers/PublicAPIRouter.js | 236 +- src/Routers/PurgeRouter.js | 25 +- src/Routers/PushRouter.js | 74 +- src/Routers/RolesRouter.js | 21 +- src/Routers/SchemasRouter.js | 119 +- src/Routers/SessionsRouter.js | 93 +- src/Routers/UsersRouter.js | 320 +- src/StatusHandler.js | 165 +- src/TestUtils.js | 18 +- src/batch.js | 58 +- src/cache.js | 4 +- .../definitions/parse-live-query-server.js | 3 +- src/cli/definitions/parse-server.js | 3 +- src/cli/parse-live-query-server.js | 4 +- src/cli/parse-server.js | 59 +- src/cli/utils/commander.js | 32 +- src/cli/utils/runner.js | 16 +- src/cloud-code/HTTPResponse.js | 18 +- src/cloud-code/Parse.Cloud.js | 61 +- src/cloud-code/httpRequest.js | 23 +- src/cryptoUtils.js | 11 +- src/defaults.js | 24 +- src/deprecated.js | 2 +- src/index.js | 30 +- src/logger.js | 9 +- src/middlewares.js | 150 +- src/password.js | 6 +- src/requiredParameter.js | 4 +- src/rest.js | 298 +- src/triggers.js | 417 +- src/vendor/mongodbUrl.js | 580 +- 240 files changed, 40570 insertions(+), 28492 deletions(-) create mode 100644 .prettierrc diff --git a/.eslintrc.json b/.eslintrc.json index 2a14b903f9..7c2b937b7c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,7 +14,7 @@ "sourceType": "module" }, "rules": { - "indent": ["error", 2], + "indent": ["error", 2, { "SwitchCase": 1 }], "linebreak-style": ["error", "unix"], "no-trailing-spaces": 2, "eol-last": 2, diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000000..f0ef52a9dc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +semi: true +trailingComma: "es5" +singleQuote: true diff --git a/.travis.yml b/.travis.yml index 1c097a4db4..da5bba092d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,6 +48,7 @@ before_script: - silent=1 mongodb-runner --start - greenkeeper-lockfile-update script: +- npm run lint - npm run coverage after_script: - greenkeeper-lockfile-upload diff --git a/package-lock.json b/package-lock.json index 80b62ade45..f39811fb58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1570,6 +1570,15 @@ "mailgun-js": "0.18.0" } }, + "@samverschueren/stream-to-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", + "integrity": "sha512-MI4Xx6LHs4Webyvi6EbspgyAb4D2Q2VtnCQ1blOJcoLS6mVa8lNN2rkIy1CVxfTUpoyIbCTkXES1rLXztFD1lg==", + "dev": true, + "requires": { + "any-observable": "^0.3.0" + } + }, "@semantic-release/error": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-2.2.0.tgz", @@ -2052,6 +2061,12 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "any-observable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", + "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", + "dev": true + }, "apn": { "version": "3.0.0-alpha1", "resolved": "https://registry.npmjs.org/apn/-/apn-3.0.0-alpha1.tgz", @@ -2086,6 +2101,12 @@ "sprintf-js": "~1.0.2" } }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true + }, "arr-flatten": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", @@ -2124,6 +2145,12 @@ "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", "dev": true }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", + "dev": true + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -3048,6 +3075,35 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "bson": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.9.tgz", @@ -3102,6 +3158,12 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-1.0.1.tgz", "integrity": "sha1-Iqk2kB4wKa/NdUfrRIfOtpejvwg=" }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", @@ -3310,6 +3372,24 @@ "restore-cursor": "^2.0.0" } }, + "cli-truncate": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ=", + "dev": true, + "requires": { + "slice-ansi": "0.0.4", + "string-width": "^1.0.1" + }, + "dependencies": { + "slice-ansi": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", + "dev": true + } + } + }, "cli-width": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", @@ -3467,6 +3547,17 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "cosmiconfig": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz", + "integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==", + "dev": true, + "requires": { + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0" + } + }, "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -3581,6 +3672,12 @@ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-1.2.0.tgz", "integrity": "sha512-vKQ9DTQPN1FLYiiEEOQ6IBGFqvjCa5rSK3cWMy/Nespm5d/x3dGFT9UBZnkLxCwua/IXBi2TYnwTEpsOvhC4UQ==" }, + "date-fns": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.29.0.tgz", + "integrity": "sha512-lbTXWZ6M20cWH8N9S6afb0SBm6tMk+uUg6z3MqHPKE9atmsY3kJkTm8vKe93izJ2B2+q5MV990sM2CHgtAZaOw==", + "dev": true + }, "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", @@ -3693,6 +3790,12 @@ } } }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", + "dev": true + }, "deep-diff": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", @@ -4011,6 +4114,12 @@ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, + "elegant-spinner": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", + "dev": true + }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -4031,6 +4140,15 @@ "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", "dev": true }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.12.0.tgz", @@ -4480,6 +4598,56 @@ } } }, + "exit-hook": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", + "dev": true + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "express": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", @@ -4654,6 +4822,77 @@ "tmp": "^0.0.33" } }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", @@ -4735,6 +4974,29 @@ "trim-repeated": "^1.0.0" } }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dev": true, + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, "finalhandler": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", @@ -4764,12 +5026,27 @@ } } }, + "find-parent-dir": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.0.tgz", + "integrity": "sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ=", + "dev": true + }, "find-root": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/find-root/-/find-root-0.1.2.tgz", "integrity": "sha1-mNImfP8ZFsyvJ0OzoO6oHXnX3NE=", "dev": true }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "requires": { + "locate-path": "^3.0.0" + } + }, "flat-cache": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", @@ -5548,6 +5825,12 @@ } } }, + "get-own-enumerable-property-symbols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz", + "integrity": "sha512-TtY/sbOemiMKPRUDDanGCSgBYe7Mf0vbRsWnBZ+9yghpZ1MvcpSpuZFjHdEeY/LZjZy0vdLjS77L6HosisFiug==", + "dev": true + }, "get-proxy": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", @@ -5557,6 +5840,12 @@ "npm-conf": "^1.1.0" } }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -5822,6 +6111,12 @@ "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==", + "dev": true + }, "htmlparser2": { "version": "3.9.2", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", @@ -5875,6 +6170,41 @@ "debug": "^3.1.0" } }, + "husky": { + "version": "1.0.0-rc.13", + "resolved": "https://registry.npmjs.org/husky/-/husky-1.0.0-rc.13.tgz", + "integrity": "sha512-ZNNoaBgfOHRA05UHS/etBoWFDu65mjPoohPYQwOqb5155KOovBp8LMkMoNK0kn3VYdsm+HWdtuHbD4XjfzlfpQ==", + "dev": true, + "requires": { + "cosmiconfig": "^5.0.2", + "execa": "^0.9.0", + "find-up": "^3.0.0", + "get-stdin": "^6.0.0", + "is-ci": "^1.1.0", + "pkg-dir": "^3.0.0", + "please-upgrade-node": "^3.1.1", + "read-pkg": "^4.0.1", + "run-node": "^1.0.0", + "slash": "^2.0.0" + }, + "dependencies": { + "execa": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz", + "integrity": "sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA==", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + } + } + }, "iconv-lite": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", @@ -6060,6 +6390,12 @@ "kind-of": "^3.0.2" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, "is-binary-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", @@ -6075,6 +6411,15 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "^1.0.0" + } + }, "is-callable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", @@ -6124,12 +6469,24 @@ } } }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", + "dev": true + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -6138,6 +6495,15 @@ "number-is-nan": "^1.0.0" } }, + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, "is-installed-globally": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", @@ -6208,6 +6574,15 @@ "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", "dev": true }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "requires": { + "kind-of": "^3.0.2" + } + }, "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", @@ -6220,6 +6595,23 @@ "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", "dev": true }, + "is-observable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", + "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", + "dev": true, + "requires": { + "symbol-observable": "^1.1.0" + }, + "dependencies": { + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "dev": true + } + } + }, "is-odd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", @@ -6305,6 +6697,12 @@ "has": "^1.0.1" } }, + "is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", + "dev": true + }, "is-resolvable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", @@ -6350,6 +6748,12 @@ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -6577,6 +6981,46 @@ } } }, + "jest-get-type": { + "version": "22.4.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-22.4.3.tgz", + "integrity": "sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w==", + "dev": true + }, + "jest-validate": { + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-23.5.0.tgz", + "integrity": "sha512-XmStdYhfdiDKacXX5sNqEE61Zz4/yXaPcDsKvVA0429RBu2pkQyIltCVG7UitJIEAzSs3ociQTdyseAW8VGPiA==", + "dev": true, + "requires": { + "chalk": "^2.0.1", + "jest-get-type": "^22.1.0", + "leven": "^2.1.0", + "pretty-format": "^23.5.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, "jmespath": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", @@ -6688,6 +7132,12 @@ "integrity": "sha1-hCRCjVtWOtjFx/vsB5uaiwnI3Po=", "dev": true }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -6811,6 +7261,12 @@ "package-json": "^4.0.0" } }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -6826,6 +7282,209 @@ "integrity": "sha512-zrycnIMsLw/3ZxTbW7HCez56rcFGecWTx5OZNplzcXUUmJLmoYArC6qdJzmAN5BWiNXGcpjhF9RQ1HSv5zebEw==", "dev": true }, + "lint-staged": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.2.2.tgz", + "integrity": "sha512-BWT3kx242hq5oaKJ8QiazPeHwJnEXImvjmgZfjljMI5HX6RrTxI3cTJXywre6GNafMONCD/suFnEiFmC69Gscg==", + "dev": true, + "requires": { + "chalk": "^2.3.1", + "commander": "^2.14.1", + "cosmiconfig": "^5.0.2", + "debug": "^3.1.0", + "dedent": "^0.7.0", + "execa": "^0.9.0", + "find-parent-dir": "^0.3.0", + "is-glob": "^4.0.0", + "is-windows": "^1.0.2", + "jest-validate": "^23.5.0", + "listr": "^0.14.1", + "lodash": "^4.17.5", + "log-symbols": "^2.2.0", + "micromatch": "^3.1.8", + "npm-which": "^3.0.1", + "p-map": "^1.1.1", + "path-is-inside": "^1.0.2", + "pify": "^3.0.0", + "please-upgrade-node": "^3.0.2", + "staged-git-files": "1.1.1", + "string-argv": "^0.0.2", + "stringify-object": "^3.2.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "execa": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.9.0.tgz", + "integrity": "sha512-BbUMBiX4hqiHZUA5+JujIjNb6TyAlp2D5KLheMjMluwOuzcnylDL4AxZYLLn1n2AGB49eSWwyKvvEQoRpnAtmA==", + "dev": true, + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "listr": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.2.tgz", + "integrity": "sha512-vmaNJ1KlGuGWShHI35X/F8r9xxS0VTHh9GejVXwSN20fG5xpq3Jh4bJbnumoT6q5EDM/8/YP1z3YMtQbFmhuXw==", + "dev": true, + "requires": { + "@samverschueren/stream-to-observable": "^0.3.0", + "is-observable": "^1.1.0", + "is-promise": "^2.1.0", + "is-stream": "^1.1.0", + "listr-silent-renderer": "^1.1.1", + "listr-update-renderer": "^0.4.0", + "listr-verbose-renderer": "^0.4.0", + "p-map": "^1.1.1", + "rxjs": "^6.1.0" + }, + "dependencies": { + "rxjs": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.1.tgz", + "integrity": "sha512-hRVfb1Mcf8rLXq1AZEjYpzBnQbO7Duveu1APXkWRTvqzhmkoQ40Pl2F9Btacx+gJCOqsMiugCGG4I2HPQgJRtA==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + } + } + }, + "listr-silent-renderer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4=", + "dev": true + }, + "listr-update-renderer": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz", + "integrity": "sha1-NE2YDaLKLosUW6MFkI8yrj9MyKc=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cli-truncate": "^0.2.1", + "elegant-spinner": "^1.0.1", + "figures": "^1.7.0", + "indent-string": "^3.0.0", + "log-symbols": "^1.0.2", + "log-update": "^1.0.2", + "strip-ansi": "^3.0.1" + }, + "dependencies": { + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "log-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg=", + "dev": true, + "requires": { + "chalk": "^1.0.0" + } + } + } + }, + "listr-verbose-renderer": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz", + "integrity": "sha1-ggb0z21S3cWCfl/RSYng6WWTOjU=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "cli-cursor": "^1.0.2", + "date-fns": "^1.27.2", + "figures": "^1.7.0" + }, + "dependencies": { + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + } + } + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, "lodash": { "version": "4.17.5", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", @@ -6968,11 +7627,85 @@ "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", "dev": true }, - "lodash.startswith": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz", - "integrity": "sha1-xZjErc4YiiflMUVzHNxsDnF3YAw=", - "dev": true + "lodash.startswith": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.startswith/-/lodash.startswith-4.2.1.tgz", + "integrity": "sha1-xZjErc4YiiflMUVzHNxsDnF3YAw=", + "dev": true + }, + "log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "requires": { + "chalk": "^2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } + } + }, + "log-update": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-1.0.2.tgz", + "integrity": "sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE=", + "dev": true, + "requires": { + "ansi-escapes": "^1.0.0", + "cli-cursor": "^1.0.2" + }, + "dependencies": { + "ansi-escapes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", + "dev": true + }, + "cli-cursor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", + "dev": true, + "requires": { + "restore-cursor": "^1.0.1" + } + }, + "onetime": { + "version": "1.1.0", + "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", + "dev": true + }, + "restore-cursor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", + "dev": true, + "requires": { + "exit-hook": "^1.0.0", + "onetime": "^1.0.0" + } + } + } }, "loose-envify": { "version": "1.3.1", @@ -7116,6 +7849,35 @@ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "mime": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/mime/-/mime-2.3.1.tgz", @@ -7315,17 +8077,49 @@ "which": "^1.2.4" }, "dependencies": { - "lodash": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", - "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", - "dev": true - }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true + } + } + }, + "mongodb-tools": { + "version": "github:mongodb-js/mongodb-tools#df761df21175f2c521c78b8e011a1532569f0dca", + "from": "github:mongodb-js/mongodb-tools", + "dev": true, + "requires": { + "debug": "^2.2.0", + "lodash": "^3.10.1", + "mkdirp": "0.5.0", + "mongodb-core": "^2.1.8", + "rimraf": "2.2.6" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "lodash": { + "version": "3.10.1", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=", + "dev": true + }, + "mkdirp": { + "version": "0.5.0", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } }, "mongodb-core": { "version": "2.1.20", @@ -7337,47 +8131,9 @@ "require_optional": "~1.0.0" } }, - "mongodb-tools": { - "version": "github:mongodb-js/mongodb-tools#df761df21175f2c521c78b8e011a1532569f0dca", - "from": "mongodb-tools@github:mongodb-js/mongodb-tools#df761df21175f2c521c78b8e011a1532569f0dca", - "dev": true, - "requires": { - "debug": "^2.2.0", - "lodash": "^3.10.1", - "mkdirp": "0.5.0", - "mongodb-core": "^2.1.8", - "rimraf": "2.2.6" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.0.tgz", - "integrity": "sha1-HXMHam35hs2TROFecfzAWkyavxI=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - } - } - }, "rimraf": { "version": "2.2.6", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", + "resolved": "http://registry.npmjs.org/rimraf/-/rimraf-2.2.6.tgz", "integrity": "sha1-xZWXVpsU2VatKcrMQr3d9fDqT0w=", "dev": true } @@ -7958,6 +8714,18 @@ "abbrev": "1" } }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, "normalize-path": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", @@ -7985,6 +8753,15 @@ } } }, + "npm-path": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz", + "integrity": "sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw==", + "dev": true, + "requires": { + "which": "^1.2.10" + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -7994,6 +8771,17 @@ "path-key": "^2.0.0" } }, + "npm-which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-which/-/npm-which-3.0.1.tgz", + "integrity": "sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo=", + "dev": true, + "requires": { + "commander": "^2.9.0", + "npm-path": "^2.0.2", + "which": "^1.2.10" + } + }, "npmlog": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", @@ -10472,6 +11260,30 @@ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, + "p-limit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", + "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true + }, "p-reduce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", @@ -10487,6 +11299,12 @@ "p-finally": "^1.0.0" } }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", + "dev": true + }, "pac-proxy-agent": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-2.0.2.tgz", @@ -10573,6 +11391,16 @@ "xmlhttprequest": "1.8.0" } }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, "parseurl": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", @@ -10590,6 +11418,12 @@ "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", "dev": true }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -10758,6 +11592,24 @@ "pinkie": "^2.0.0" } }, + "pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "dev": true, + "requires": { + "find-up": "^3.0.0" + } + }, + "please-upgrade-node": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz", + "integrity": "sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ==", + "dev": true, + "requires": { + "semver-compare": "^1.0.0" + } + }, "pluralize": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", @@ -10804,6 +11656,39 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, + "prettier": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.14.2.tgz", + "integrity": "sha512-McHPg0n1pIke+A/4VcaS2en+pTNjy4xF+Uuq86u/5dyDO59/TtFZtQ708QIRkEZ3qwKz3GVkVa6mpxK/CpB8Rg==", + "dev": true + }, + "pretty-format": { + "version": "23.5.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-23.5.0.tgz", + "integrity": "sha512-iFLvYTXOn+C/s7eV+pr4E8DD7lYa2/klXMEz+lvH14qSDWAJ7S+kFmMe1SIWesATHQxopHTxRcB2nrpExhzaBA==", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0", + "ansi-styles": "^3.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + } + } + }, "private": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", @@ -10952,6 +11837,25 @@ } } }, + "read-pkg": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-4.0.1.tgz", + "integrity": "sha1-ljYlN48+HE1IyFhytabsfV0JMjc=", + "dev": true, + "requires": { + "normalize-package-data": "^2.3.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, "readable-stream": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", @@ -11276,6 +12180,12 @@ "is-promise": "^2.1.0" } }, + "run-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", + "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", + "dev": true + }, "rxjs": { "version": "5.5.11", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", @@ -11340,6 +12250,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" }, + "semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true + }, "semver-diff": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", @@ -11459,6 +12375,12 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, "slice-ansi": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", @@ -11689,6 +12611,38 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "dev": true + }, "spex": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/spex/-/spex-2.0.2.tgz", @@ -11738,6 +12692,12 @@ "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" }, + "staged-git-files": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/staged-git-files/-/staged-git-files-1.1.1.tgz", + "integrity": "sha512-H89UNKr1rQJvI1c/PIR3kiAMBV23yvR7LItZiV74HWZwzt7f3YHuujJ9nJZlt58WlFox7XQsOahexwk7nTe69A==", + "dev": true + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -11779,6 +12739,12 @@ "duplexer": "~0.1.1" } }, + "string-argv": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.0.2.tgz", + "integrity": "sha1-2sMECGkMIfPDYwo/86BYd73L1zY=", + "dev": true + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -11810,6 +12776,17 @@ "safe-buffer": "~5.1.0" } }, + "stringify-object": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.2.2.tgz", + "integrity": "sha512-O696NF21oLiDy8PhpWu8AEqoZHw++QW6mUv0UvKZe8gWSdSvMXkiLufK7OmnP27Dro4GU5kb9U7JIO0mBuCRQg==", + "dev": true, + "requires": { + "get-own-enumerable-property-symbols": "^2.0.1", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + } + }, "stringstream": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", @@ -12191,6 +13168,12 @@ "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "dev": true }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, "tsscmp": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", @@ -12599,6 +13582,16 @@ "integrity": "sha1-/Rp5z2EYo4jgob7YoTlwMNLE/Sw=", "optional": true }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index f6037ebe4b..2f26db886d 100644 --- a/package.json +++ b/package.json @@ -59,13 +59,16 @@ "eslint-plugin-flowtype": "^2.39.1", "flow-bin": "0.79.1", "gaze": "1.1.3", + "husky": "^1.0.0-rc.13", "jasmine": "3.1.0", "jasmine-spec-reporter": "^4.1.0", "jsdoc": "^3.5.5", "jsdoc-babel": "^0.5.0", + "lint-staged": "^7.2.2", "mongodb-runner": "4.1.0", "nodemon": "1.18.4", "nyc": "^12.0.2", + "prettier": "1.14.2", "request-promise": "4.2.2", "supports-color": "^5.4.0" }, @@ -75,7 +78,6 @@ "lint": "flow && eslint --cache ./", "build": "babel src/ -d lib/ --copy-files", "watch": "babel --watch src/ -d lib/ --copy-files", - "pretest": "npm run lint", "test": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 TESTING=1 jasmine", "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=3.2.6} MONGODB_STORAGE_ENGINE=mmapv1 TESTING=1 nyc jasmine", "start": "node ./bin/parse-server", @@ -96,5 +98,18 @@ "type": "opencollective", "url": "https://opencollective.com/parse-server", "logo": "https://opencollective.com/parse-server/logo.txt?reverse=true&variant=binary" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "{src,spec}/**/*.js": [ + "prettier --write", + "flow", + "eslint --cache ./", + "git add" + ] } } diff --git a/spec/AccountLockoutPolicy.spec.js b/spec/AccountLockoutPolicy.spec.js index 099b762c79..5fac8a703d 100644 --- a/spec/AccountLockoutPolicy.spec.js +++ b/spec/AccountLockoutPolicy.spec.js @@ -1,6 +1,6 @@ -"use strict"; +'use strict'; -const Config = require("../lib/Config"); +const Config = require('../lib/Config'); const loginWithWrongCredentialsShouldFail = function(username, password) { return new Promise((resolve, reject) => { @@ -22,7 +22,12 @@ const isAccountLockoutError = function(username, password, duration, waitTime) { Parse.User.logIn(username, password) .then(() => reject('login should have failed')) .catch(err => { - if (err.message === 'Your account is locked due to multiple failed login attempts. Please try again after ' + duration + ' minute(s)') { + if ( + err.message === + 'Your account is locked due to multiple failed login attempts. Please try again after ' + + duration + + ' minute(s)' + ) { resolve(); } else { reject(err); @@ -32,8 +37,7 @@ const isAccountLockoutError = function(username, password, duration, waitTime) { }); }; -describe("Account Lockout Policy: ", () => { - +describe('Account Lockout Policy: ', () => { it('account should not be locked even after failed login attempts if account lockout policy is not set', done => { reconfigureServer({ appName: 'unlimited', @@ -46,17 +50,28 @@ describe("Account Lockout Policy: ", () => { return user.signUp(null); }) .then(() => { - return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 1'); + return loginWithWrongCredentialsShouldFail( + 'username1', + 'incorrect password 1' + ); }) .then(() => { - return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 2'); + return loginWithWrongCredentialsShouldFail( + 'username1', + 'incorrect password 2' + ); }) .then(() => { - return loginWithWrongCredentialsShouldFail('username1', 'incorrect password 3'); + return loginWithWrongCredentialsShouldFail( + 'username1', + 'incorrect password 3' + ); }) .then(() => done()) .catch(err => { - fail('allow unlimited failed login attempts failed: ' + JSON.stringify(err)); + fail( + 'allow unlimited failed login attempts failed: ' + JSON.stringify(err) + ); done(); }); }); @@ -66,9 +81,9 @@ describe("Account Lockout Policy: ", () => { appName: 'duration', accountLockout: { duration: 'invalid value', - threshold: 5 + threshold: 5, }, - publicServerURL: "https://my.public.server.com/1" + publicServerURL: 'https://my.public.server.com/1', }) .then(() => { Config.get('test'); @@ -76,10 +91,17 @@ describe("Account Lockout Policy: ", () => { done(); }) .catch(err => { - if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') { + if ( + err && + err === + 'Account lockout duration should be greater than 0 and less than 100000' + ) { done(); } else { - fail('set duration to an invalid number test failed: ' + JSON.stringify(err)); + fail( + 'set duration to an invalid number test failed: ' + + JSON.stringify(err) + ); done(); } }); @@ -90,9 +112,9 @@ describe("Account Lockout Policy: ", () => { appName: 'threshold', accountLockout: { duration: 5, - threshold: 'invalid number' + threshold: 'invalid number', }, - publicServerURL: "https://my.public.server.com/1" + publicServerURL: 'https://my.public.server.com/1', }) .then(() => { Config.get('test'); @@ -100,10 +122,17 @@ describe("Account Lockout Policy: ", () => { done(); }) .catch(err => { - if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') { + if ( + err && + err === + 'Account lockout threshold should be an integer greater than 0 and less than 1000' + ) { done(); } else { - fail('set threshold to an invalid number test failed: ' + JSON.stringify(err)); + fail( + 'set threshold to an invalid number test failed: ' + + JSON.stringify(err) + ); done(); } }); @@ -114,9 +143,9 @@ describe("Account Lockout Policy: ", () => { appName: 'threshold', accountLockout: { duration: 5, - threshold: 0 + threshold: 0, }, - publicServerURL: "https://my.public.server.com/1" + publicServerURL: 'https://my.public.server.com/1', }) .then(() => { Config.get('test'); @@ -124,10 +153,16 @@ describe("Account Lockout Policy: ", () => { done(); }) .catch(err => { - if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') { + if ( + err && + err === + 'Account lockout threshold should be an integer greater than 0 and less than 1000' + ) { done(); } else { - fail('threshold value < 1 is invalid test failed: ' + JSON.stringify(err)); + fail( + 'threshold value < 1 is invalid test failed: ' + JSON.stringify(err) + ); done(); } }); @@ -138,9 +173,9 @@ describe("Account Lockout Policy: ", () => { appName: 'threshold', accountLockout: { duration: 5, - threshold: 1000 + threshold: 1000, }, - publicServerURL: "https://my.public.server.com/1" + publicServerURL: 'https://my.public.server.com/1', }) .then(() => { Config.get('test'); @@ -148,10 +183,17 @@ describe("Account Lockout Policy: ", () => { done(); }) .catch(err => { - if (err && err === 'Account lockout threshold should be an integer greater than 0 and less than 1000') { + if ( + err && + err === + 'Account lockout threshold should be an integer greater than 0 and less than 1000' + ) { done(); } else { - fail('threshold value > 999 is invalid test failed: ' + JSON.stringify(err)); + fail( + 'threshold value > 999 is invalid test failed: ' + + JSON.stringify(err) + ); done(); } }); @@ -162,9 +204,9 @@ describe("Account Lockout Policy: ", () => { appName: 'duration', accountLockout: { duration: 0, - threshold: 5 + threshold: 5, }, - publicServerURL: "https://my.public.server.com/1" + publicServerURL: 'https://my.public.server.com/1', }) .then(() => { Config.get('test'); @@ -172,10 +214,16 @@ describe("Account Lockout Policy: ", () => { done(); }) .catch(err => { - if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') { + if ( + err && + err === + 'Account lockout duration should be greater than 0 and less than 100000' + ) { done(); } else { - fail('duration value < 1 is invalid test failed: ' + JSON.stringify(err)); + fail( + 'duration value < 1 is invalid test failed: ' + JSON.stringify(err) + ); done(); } }); @@ -186,9 +234,9 @@ describe("Account Lockout Policy: ", () => { appName: 'duration', accountLockout: { duration: 100000, - threshold: 5 + threshold: 5, }, - publicServerURL: "https://my.public.server.com/1" + publicServerURL: 'https://my.public.server.com/1', }) .then(() => { Config.get('test'); @@ -196,10 +244,17 @@ describe("Account Lockout Policy: ", () => { done(); }) .catch(err => { - if (err && err === 'Account lockout duration should be greater than 0 and less than 100000') { + if ( + err && + err === + 'Account lockout duration should be greater than 0 and less than 100000' + ) { done(); } else { - fail('duration value > 99999 is invalid test failed: ' + JSON.stringify(err)); + fail( + 'duration value > 99999 is invalid test failed: ' + + JSON.stringify(err) + ); done(); } }); @@ -210,21 +265,27 @@ describe("Account Lockout Policy: ", () => { appName: 'lockout threshold', accountLockout: { duration: 1, - threshold: 2 + threshold: 2, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { const user = new Parse.User(); - user.setUsername("username2"); - user.setPassword("failedLoginAttemptsThreshold"); + user.setUsername('username2'); + user.setPassword('failedLoginAttemptsThreshold'); return user.signUp(); }) .then(() => { - return loginWithWrongCredentialsShouldFail('username2', 'wrong password'); + return loginWithWrongCredentialsShouldFail( + 'username2', + 'wrong password' + ); }) .then(() => { - return loginWithWrongCredentialsShouldFail('username2', 'wrong password'); + return loginWithWrongCredentialsShouldFail( + 'username2', + 'wrong password' + ); }) .then(() => { return isAccountLockoutError('username2', 'wrong password', 1, 1); @@ -233,7 +294,10 @@ describe("Account Lockout Policy: ", () => { done(); }) .catch(err => { - fail('lock account after failed login attempts test failed: ' + JSON.stringify(err)); + fail( + 'lock account after failed login attempts test failed: ' + + JSON.stringify(err) + ); done(); }); }); @@ -243,34 +307,43 @@ describe("Account Lockout Policy: ", () => { appName: 'lockout threshold', accountLockout: { duration: 0.05, // 0.05*60 = 3 secs - threshold: 2 + threshold: 2, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { const user = new Parse.User(); - user.setUsername("username3"); - user.setPassword("failedLoginAttemptsThreshold"); + user.setUsername('username3'); + user.setPassword('failedLoginAttemptsThreshold'); return user.signUp(); }) .then(() => { - return loginWithWrongCredentialsShouldFail('username3', 'wrong password'); + return loginWithWrongCredentialsShouldFail( + 'username3', + 'wrong password' + ); }) .then(() => { - return loginWithWrongCredentialsShouldFail('username3', 'wrong password'); + return loginWithWrongCredentialsShouldFail( + 'username3', + 'wrong password' + ); }) .then(() => { return isAccountLockoutError('username3', 'wrong password', 0.05, 1); }) .then(() => { - // account should still be locked even after 2 seconds. + // account should still be locked even after 2 seconds. return isAccountLockoutError('username3', 'wrong password', 0.05, 2000); }) .then(() => { done(); }) .catch(err => { - fail('account should be locked for duration mins test failed: ' + JSON.stringify(err)); + fail( + 'account should be locked for duration mins test failed: ' + + JSON.stringify(err) + ); done(); }); }); @@ -280,24 +353,30 @@ describe("Account Lockout Policy: ", () => { appName: 'lockout threshold', accountLockout: { duration: 0.05, // 0.05*60 = 3 secs - threshold: 2 + threshold: 2, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { const user = new Parse.User(); - user.setUsername("username4"); - user.setPassword("correct password"); + user.setUsername('username4'); + user.setPassword('correct password'); return user.signUp(); }) .then(() => { - return loginWithWrongCredentialsShouldFail('username4', 'wrong password'); + return loginWithWrongCredentialsShouldFail( + 'username4', + 'wrong password' + ); }) .then(() => { - return loginWithWrongCredentialsShouldFail('username4', 'wrong password'); + return loginWithWrongCredentialsShouldFail( + 'username4', + 'wrong password' + ); }) .then(() => { - // allow locked user to login after 3 seconds with a valid userid and password + // allow locked user to login after 3 seconds with a valid userid and password return new Promise((resolve, reject) => { setTimeout(() => { Parse.User.logIn('username4', 'correct password') @@ -310,9 +389,11 @@ describe("Account Lockout Policy: ", () => { done(); }) .catch(err => { - fail('allow login for locked account after accountPolicy.duration minutes test failed: ' + JSON.stringify(err)); + fail( + 'allow login for locked account after accountPolicy.duration minutes test failed: ' + + JSON.stringify(err) + ); done(); }); }); - -}) +}); diff --git a/spec/AdaptableController.spec.js b/spec/AdaptableController.spec.js index 3e4ab74468..ef3b891e37 100644 --- a/spec/AdaptableController.spec.js +++ b/spec/AdaptableController.spec.js @@ -1,28 +1,29 @@ - -const AdaptableController = require("../lib/Controllers/AdaptableController").AdaptableController; -const FilesAdapter = require("../lib/Adapters/Files/FilesAdapter").default; -const FilesController = require("../lib/Controllers/FilesController").FilesController; +const AdaptableController = require('../lib/Controllers/AdaptableController') + .AdaptableController; +const FilesAdapter = require('../lib/Adapters/Files/FilesAdapter').default; +const FilesController = require('../lib/Controllers/FilesController') + .FilesController; const MockController = function(options) { AdaptableController.call(this, options); -} +}; MockController.prototype = Object.create(AdaptableController.prototype); MockController.prototype.constructor = AdaptableController; -describe("AdaptableController", ()=>{ - it("should use the provided adapter", (done) => { +describe('AdaptableController', () => { + it('should use the provided adapter', done => { const adapter = new FilesAdapter(); const controller = new FilesController(adapter); expect(controller.adapter).toBe(adapter); // make sure _adapter is private expect(controller._adapter).toBe(undefined); // Override _adapter is not doing anything - controller._adapter = "Hello"; + controller._adapter = 'Hello'; expect(controller.adapter).toBe(adapter); done(); }); - it("should throw when creating a new mock controller", (done) => { + it('should throw when creating a new mock controller', done => { const adapter = new FilesAdapter(); expect(() => { new MockController(adapter); @@ -30,7 +31,7 @@ describe("AdaptableController", ()=>{ done(); }); - it("should fail setting the wrong adapter to the controller", (done) => { + it('should fail setting the wrong adapter to the controller', done => { function WrongAdapter() {} const adapter = new FilesAdapter(); const controller = new FilesController(adapter); @@ -41,7 +42,7 @@ describe("AdaptableController", ()=>{ done(); }); - it("should fail to instantiate a controller with wrong adapter", (done) => { + it('should fail to instantiate a controller with wrong adapter', done => { function WrongAdapter() {} const adapter = new WrongAdapter(); expect(() => { @@ -50,32 +51,32 @@ describe("AdaptableController", ()=>{ done(); }); - it("should fail to instantiate a controller without an adapter", (done) => { + it('should fail to instantiate a controller without an adapter', done => { expect(() => { new FilesController(); }).toThrow(); done(); }); - it("should accept an object adapter", (done) => { + it('should accept an object adapter', done => { const adapter = { - createFile: function() { }, - deleteFile: function() { }, - getFileData: function() { }, - getFileLocation: function() { }, - } + createFile: function() {}, + deleteFile: function() {}, + getFileData: function() {}, + getFileLocation: function() {}, + }; expect(() => { new FilesController(adapter); }).not.toThrow(); done(); }); - it("should accept an prototype based object adapter", (done) => { + it('should accept an prototype based object adapter', done => { function AGoodAdapter() {} - AGoodAdapter.prototype.createFile = function() { }; - AGoodAdapter.prototype.deleteFile = function() { }; - AGoodAdapter.prototype.getFileData = function() { }; - AGoodAdapter.prototype.getFileLocation = function() { }; + AGoodAdapter.prototype.createFile = function() {}; + AGoodAdapter.prototype.deleteFile = function() {}; + AGoodAdapter.prototype.getFileData = function() {}; + AGoodAdapter.prototype.getFileLocation = function() {}; const adapter = new AGoodAdapter(); expect(() => { diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js index 334232f4a7..4ec5494b50 100644 --- a/spec/AdapterLoader.spec.js +++ b/spec/AdapterLoader.spec.js @@ -1,41 +1,41 @@ - -const loadAdapter = require("../lib/Adapters/AdapterLoader").loadAdapter; -const FilesAdapter = require("@parse/fs-files-adapter").default; -const S3Adapter = require("@parse/s3-files-adapter").default; -const ParsePushAdapter = require("@parse/push-adapter").default; +const loadAdapter = require('../lib/Adapters/AdapterLoader').loadAdapter; +const FilesAdapter = require('@parse/fs-files-adapter').default; +const S3Adapter = require('@parse/s3-files-adapter').default; +const ParsePushAdapter = require('@parse/push-adapter').default; const Config = require('../lib/Config'); -describe("AdapterLoader", ()=>{ - - it("should instantiate an adapter from string in object", (done) => { - const adapterPath = require('path').resolve("./spec/MockAdapter"); +describe('AdapterLoader', () => { + it('should instantiate an adapter from string in object', done => { + const adapterPath = require('path').resolve('./spec/MockAdapter'); const adapter = loadAdapter({ adapter: adapterPath, options: { - key: "value", - foo: "bar" - } + key: 'value', + foo: 'bar', + }, }); expect(adapter instanceof Object).toBe(true); - expect(adapter.options.key).toBe("value"); - expect(adapter.options.foo).toBe("bar"); + expect(adapter.options.key).toBe('value'); + expect(adapter.options.foo).toBe('bar'); done(); }); - it("should instantiate an adapter from string", (done) => { - const adapterPath = require('path').resolve("./spec/MockAdapter"); + it('should instantiate an adapter from string', done => { + const adapterPath = require('path').resolve('./spec/MockAdapter'); const adapter = loadAdapter(adapterPath); expect(adapter instanceof Object).toBe(true); done(); }); - it("should instantiate an adapter from string that is module", (done) => { - const adapterPath = require('path').resolve("./lib/Adapters/Files/FilesAdapter"); + it('should instantiate an adapter from string that is module', done => { + const adapterPath = require('path').resolve( + './lib/Adapters/Files/FilesAdapter' + ); const adapter = loadAdapter({ - adapter: adapterPath + adapter: adapterPath, }); expect(typeof adapter).toBe('object'); @@ -46,9 +46,9 @@ describe("AdapterLoader", ()=>{ done(); }); - it("should instantiate an adapter from npm module", (done) => { + it('should instantiate an adapter from npm module', done => { const adapter = loadAdapter({ - module: '@parse/fs-files-adapter' + module: '@parse/fs-files-adapter', }); expect(typeof adapter).toBe('object'); @@ -59,59 +59,59 @@ describe("AdapterLoader", ()=>{ done(); }); - it("should instantiate an adapter from function/Class", (done) => { + it('should instantiate an adapter from function/Class', done => { const adapter = loadAdapter({ - adapter: FilesAdapter + adapter: FilesAdapter, }); expect(adapter instanceof FilesAdapter).toBe(true); done(); }); - it("should instantiate the default adapter from Class", (done) => { + it('should instantiate the default adapter from Class', done => { const adapter = loadAdapter(null, FilesAdapter); expect(adapter instanceof FilesAdapter).toBe(true); done(); }); - it("should use the default adapter", (done) => { + it('should use the default adapter', done => { const defaultAdapter = new FilesAdapter(); const adapter = loadAdapter(null, defaultAdapter); expect(adapter instanceof FilesAdapter).toBe(true); done(); }); - it("should use the provided adapter", (done) => { + it('should use the provided adapter', done => { const originalAdapter = new FilesAdapter(); const adapter = loadAdapter(originalAdapter); expect(adapter).toBe(originalAdapter); done(); }); - it("should fail loading an improperly configured adapter", (done) => { + it('should fail loading an improperly configured adapter', done => { const Adapter = function(options) { if (!options.foo) { - throw "foo is required for that adapter"; + throw 'foo is required for that adapter'; } - } + }; const adapterOptions = { - param: "key", - doSomething: function() {} + param: 'key', + doSomething: function() {}, }; expect(() => { const adapter = loadAdapter(adapterOptions, Adapter); expect(adapter).toEqual(adapterOptions); - }).not.toThrow("foo is required for that adapter"); + }).not.toThrow('foo is required for that adapter'); done(); }); - it("should load push adapter from options", (done) => { + it('should load push adapter from options', done => { const options = { android: { senderId: 'yolo', - apiKey: 'yolo' - } - } + apiKey: 'yolo', + }, + }; expect(() => { const adapter = loadAdapter(undefined, ParsePushAdapter, options); expect(adapter.constructor).toBe(ParsePushAdapter); @@ -120,16 +120,16 @@ describe("AdapterLoader", ()=>{ done(); }); - it("should load custom push adapter from string (#3544)", (done) => { - const adapterPath = require('path').resolve("./spec/MockPushAdapter"); + it('should load custom push adapter from string (#3544)', done => { + const adapterPath = require('path').resolve('./spec/MockPushAdapter'); const options = { ios: { - bundleId: 'bundle.id' - } - } + bundleId: 'bundle.id', + }, + }; const pushAdapterOptions = { adapter: adapterPath, - options + options, }; expect(() => { reconfigureServer({ @@ -144,12 +144,12 @@ describe("AdapterLoader", ()=>{ }).not.toThrow(); }); - it("should load S3Adapter from direct passing", (done) => { - const s3Adapter = new S3Adapter("key", "secret", "bucket") + it('should load S3Adapter from direct passing', done => { + const s3Adapter = new S3Adapter('key', 'secret', 'bucket'); expect(() => { const adapter = loadAdapter(s3Adapter, FilesAdapter); expect(adapter).toBe(s3Adapter); }).not.toThrow(); done(); - }) + }); }); diff --git a/spec/AggregateRouter.spec.js b/spec/AggregateRouter.spec.js index 4e751d85a4..e0f9bac18c 100644 --- a/spec/AggregateRouter.spec.js +++ b/spec/AggregateRouter.spec.js @@ -1,31 +1,36 @@ -const AggregateRouter = require('../lib/Routers/AggregateRouter').AggregateRouter; +const AggregateRouter = require('../lib/Routers/AggregateRouter') + .AggregateRouter; describe('AggregateRouter', () => { it('get pipeline from Array', () => { - const body = [{ - group: { objectId: {} } - }]; - const expected = [ { $group: { _id: {} } } ]; + const body = [ + { + group: { objectId: {} }, + }, + ]; + const expected = [{ $group: { _id: {} } }]; const result = AggregateRouter.getPipeline(body); expect(result).toEqual(expected); }); it('get pipeline from Object', () => { const body = { - group: { objectId: {} } + group: { objectId: {} }, }; - const expected = [ { $group: { _id: {} } } ]; + const expected = [{ $group: { _id: {} } }]; const result = AggregateRouter.getPipeline(body); expect(result).toEqual(expected); }); it('get pipeline from Pipeline Operator (Array)', () => { const body = { - pipeline: [{ - group: { objectId: {} } - }] + pipeline: [ + { + group: { objectId: {} }, + }, + ], }; - const expected = [ { $group: { _id: {} } } ]; + const expected = [{ $group: { _id: {} } }]; const result = AggregateRouter.getPipeline(body); expect(result).toEqual(expected); }); @@ -33,37 +38,45 @@ describe('AggregateRouter', () => { it('get pipeline from Pipeline Operator (Object)', () => { const body = { pipeline: { - group: { objectId: {} } - } + group: { objectId: {} }, + }, }; - const expected = [ { $group: { _id: {} } } ]; + const expected = [{ $group: { _id: {} } }]; const result = AggregateRouter.getPipeline(body); expect(result).toEqual(expected); }); it('get pipeline fails multiple keys in Array stage ', () => { - const body = [{ - group: { objectId: {} }, - match: { name: 'Test' }, - }]; + const body = [ + { + group: { objectId: {} }, + match: { name: 'Test' }, + }, + ]; try { AggregateRouter.getPipeline(body); } catch (e) { - expect(e.message).toBe('Pipeline stages should only have one key found group, match'); + expect(e.message).toBe( + 'Pipeline stages should only have one key found group, match' + ); } }); it('get pipeline fails multiple keys in Pipeline Operator Array stage ', () => { const body = { - pipeline: [{ - group: { objectId: {} }, - match: { name: 'Test' }, - }] + pipeline: [ + { + group: { objectId: {} }, + match: { name: 'Test' }, + }, + ], }; try { AggregateRouter.getPipeline(body); } catch (e) { - expect(e.message).toBe('Pipeline stages should only have one key found group, match'); + expect(e.message).toBe( + 'Pipeline stages should only have one key found group, match' + ); } }); }); diff --git a/spec/Analytics.spec.js b/spec/Analytics.spec.js index 69d142a658..b608aebe32 100644 --- a/spec/Analytics.spec.js +++ b/spec/Analytics.spec.js @@ -1,61 +1,69 @@ const analyticsAdapter = { appOpened: function() {}, - trackEvent: function() {} -} + trackEvent: function() {}, +}; describe('AnalyticsController', () => { - it('should track a simple event', (done) => { - + it('should track a simple event', done => { spyOn(analyticsAdapter, 'trackEvent').and.callThrough(); reconfigureServer({ - analyticsAdapter - }).then(() => { - return Parse.Analytics.track('MyEvent', { - key: 'value', - count: '0' - }) - }).then(() => { - expect(analyticsAdapter.trackEvent).toHaveBeenCalled(); - const lastCall = analyticsAdapter.trackEvent.calls.first(); - const args = lastCall.args; - expect(args[0]).toEqual('MyEvent'); - expect(args[1]).toEqual({ - dimensions: { + analyticsAdapter, + }) + .then(() => { + return Parse.Analytics.track('MyEvent', { key: 'value', - count: '0' + count: '0', + }); + }) + .then( + () => { + expect(analyticsAdapter.trackEvent).toHaveBeenCalled(); + const lastCall = analyticsAdapter.trackEvent.calls.first(); + const args = lastCall.args; + expect(args[0]).toEqual('MyEvent'); + expect(args[1]).toEqual({ + dimensions: { + key: 'value', + count: '0', + }, + }); + done(); + }, + err => { + fail(JSON.stringify(err)); + done(); } - }); - done(); - }, (err) => { - fail(JSON.stringify(err)); - done(); - }) + ); }); - it('should track a app opened event', (done) => { - + it('should track a app opened event', done => { spyOn(analyticsAdapter, 'appOpened').and.callThrough(); reconfigureServer({ - analyticsAdapter - }).then(() => { - return Parse.Analytics.track('AppOpened', { - key: 'value', - count: '0' - }) - }).then(() => { - expect(analyticsAdapter.appOpened).toHaveBeenCalled(); - const lastCall = analyticsAdapter.appOpened.calls.first(); - const args = lastCall.args; - expect(args[0]).toEqual({ - dimensions: { + analyticsAdapter, + }) + .then(() => { + return Parse.Analytics.track('AppOpened', { key: 'value', - count: '0' + count: '0', + }); + }) + .then( + () => { + expect(analyticsAdapter.appOpened).toHaveBeenCalled(); + const lastCall = analyticsAdapter.appOpened.calls.first(); + const args = lastCall.args; + expect(args[0]).toEqual({ + dimensions: { + key: 'value', + count: '0', + }, + }); + done(); + }, + err => { + fail(JSON.stringify(err)); + done(); } - }); - done(); - }, (err) => { - fail(JSON.stringify(err)); - done(); - }) - }) -}) + ); + }); +}); diff --git a/spec/AudienceRouter.spec.js b/spec/AudienceRouter.spec.js index 6b977b8f31..10686fdd37 100644 --- a/spec/AudienceRouter.spec.js +++ b/spec/AudienceRouter.spec.js @@ -1,59 +1,66 @@ const auth = require('../lib/Auth'); const Config = require('../lib/Config'); const rest = require('../lib/rest'); -const AudiencesRouter = require('../lib/Routers/AudiencesRouter').AudiencesRouter; +const AudiencesRouter = require('../lib/Routers/AudiencesRouter') + .AudiencesRouter; describe('AudiencesRouter', () => { - it('uses find condition from request.body', (done) => { + it('uses find condition from request.body', done => { const config = Config.get('test'); const androidAudienceRequest = { - 'name': 'Android Users', - 'query': '{ "test": "android" }' + name: 'Android Users', + query: '{ "test": "android" }', }; const iosAudienceRequest = { - 'name': 'Iphone Users', - 'query': '{ "test": "ios" }' + name: 'Iphone Users', + query: '{ "test": "ios" }', }; const request = { config: config, auth: auth.master(config), body: { where: { - query: '{ "test": "android" }' - } + query: '{ "test": "android" }', + }, }, query: {}, - info: {} + info: {}, }; const router = new AudiencesRouter(); - rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest) + rest + .create(config, auth.nobody(config), '_Audience', androidAudienceRequest) .then(() => { - return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest); + return rest.create( + config, + auth.nobody(config), + '_Audience', + iosAudienceRequest + ); }) .then(() => { return router.handleFind(request); }) - .then((res) => { + .then(res => { const results = res.response.results; expect(results.length).toEqual(1); done(); }) - .catch((err) => { + .catch(err => { fail(JSON.stringify(err)); done(); }); }); - it('uses find condition from request.query', (done) => { + it('uses find condition from request.query', done => { const config = Config.get('test'); const androidAudienceRequest = { - 'name': 'Android Users', - 'query': '{ "test": "android" }' + name: 'Android Users', + query: '{ "test": "android" }', }; const iosAudienceRequest = { - 'name': 'Iphone Users', - 'query': '{ "test": "ios" }' + name: 'Iphone Users', + query: '{ "test": "ios" }', }; const request = { config: config, @@ -61,66 +68,78 @@ describe('AudiencesRouter', () => { body: {}, query: { where: { - 'query': '{ "test": "android" }' - } + query: '{ "test": "android" }', + }, }, - info: {} + info: {}, }; const router = new AudiencesRouter(); - rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest) + rest + .create(config, auth.nobody(config), '_Audience', androidAudienceRequest) .then(() => { - return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest); + return rest.create( + config, + auth.nobody(config), + '_Audience', + iosAudienceRequest + ); }) .then(() => { return router.handleFind(request); }) - .then((res) => { + .then(res => { const results = res.response.results; expect(results.length).toEqual(1); done(); }) - .catch((err) => { + .catch(err => { fail(err); done(); }); }); - it('query installations with limit = 0', (done) => { + it('query installations with limit = 0', done => { const config = Config.get('test'); const androidAudienceRequest = { - 'name': 'Android Users', - 'query': '{ "test": "android" }' + name: 'Android Users', + query: '{ "test": "android" }', }; const iosAudienceRequest = { - 'name': 'Iphone Users', - 'query': '{ "test": "ios" }' + name: 'Iphone Users', + query: '{ "test": "ios" }', }; const request = { config: config, auth: auth.master(config), body: {}, query: { - limit: 0 + limit: 0, }, - info: {} + info: {}, }; Config.get('test'); const router = new AudiencesRouter(); - rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest) + rest + .create(config, auth.nobody(config), '_Audience', androidAudienceRequest) .then(() => { - return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest); + return rest.create( + config, + auth.nobody(config), + '_Audience', + iosAudienceRequest + ); }) .then(() => { return router.handleFind(request); }) - .then((res) => { + .then(res => { const response = res.response; expect(response.results.length).toEqual(0); done(); }) - .catch((err) => { + .catch(err => { fail(JSON.stringify(err)); done(); }); @@ -129,28 +148,36 @@ describe('AudiencesRouter', () => { it('query installations with count = 1', done => { const config = Config.get('test'); const androidAudienceRequest = { - 'name': 'Android Users', - 'query': '{ "test": "android" }' + name: 'Android Users', + query: '{ "test": "android" }', }; const iosAudienceRequest = { - 'name': 'Iphone Users', - 'query': '{ "test": "ios" }' + name: 'Iphone Users', + query: '{ "test": "ios" }', }; const request = { config: config, auth: auth.master(config), body: {}, query: { - count: 1 + count: 1, }, - info: {} + info: {}, }; const router = new AudiencesRouter(); - rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest) - .then(() => rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest)) + rest + .create(config, auth.nobody(config), '_Audience', androidAudienceRequest) + .then(() => + rest.create( + config, + auth.nobody(config), + '_Audience', + iosAudienceRequest + ) + ) .then(() => router.handleFind(request)) - .then((res) => { + .then(res => { const response = res.response; expect(response.results.length).toEqual(2); expect(response.count).toEqual(2); @@ -159,18 +186,18 @@ describe('AudiencesRouter', () => { .catch(error => { fail(JSON.stringify(error)); done(); - }) + }); }); - it('query installations with limit = 0 and count = 1', (done) => { + it('query installations with limit = 0 and count = 1', done => { const config = Config.get('test'); const androidAudienceRequest = { - 'name': 'Android Users', - 'query': '{ "test": "android" }' + name: 'Android Users', + query: '{ "test": "android" }', }; const iosAudienceRequest = { - 'name': 'Iphone Users', - 'query': '{ "test": "ios" }' + name: 'Iphone Users', + query: '{ "test": "ios" }', }; const request = { config: config, @@ -178,47 +205,83 @@ describe('AudiencesRouter', () => { body: {}, query: { limit: 0, - count: 1 + count: 1, }, - info: {} + info: {}, }; const router = new AudiencesRouter(); - rest.create(config, auth.nobody(config), '_Audience', androidAudienceRequest) + rest + .create(config, auth.nobody(config), '_Audience', androidAudienceRequest) .then(() => { - return rest.create(config, auth.nobody(config), '_Audience', iosAudienceRequest); + return rest.create( + config, + auth.nobody(config), + '_Audience', + iosAudienceRequest + ); }) .then(() => { return router.handleFind(request); }) - .then((res) => { + .then(res => { const response = res.response; expect(response.results.length).toEqual(0); expect(response.count).toEqual(2); done(); }) - .catch((err) => { + .catch(err => { fail(JSON.stringify(err)); done(); }); }); - it('should create, read, update and delete audiences throw api', (done) => { - Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' })}, { useMasterKey: true }) - .then(() => { - Parse._request('GET', 'push_audiences', {}, { useMasterKey: true }).then((results) => { + it('should create, read, update and delete audiences throw api', done => { + Parse._request( + 'POST', + 'push_audiences', + { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }) }, + { useMasterKey: true } + ).then(() => { + Parse._request('GET', 'push_audiences', {}, { useMasterKey: true }).then( + results => { expect(results.results.length).toEqual(1); expect(results.results[0].name).toEqual('My Audience'); expect(results.results[0].query.deviceType).toEqual('ios'); - Parse._request('GET', `push_audiences/${results.results[0].objectId}`, {}, { useMasterKey: true }).then((results) => { + Parse._request( + 'GET', + `push_audiences/${results.results[0].objectId}`, + {}, + { useMasterKey: true } + ).then(results => { expect(results.name).toEqual('My Audience'); expect(results.query.deviceType).toEqual('ios'); - Parse._request('PUT', `push_audiences/${results.objectId}`, { name: 'My Audience 2' }, { useMasterKey: true }).then(() => { - Parse._request('GET', `push_audiences/${results.objectId}`, {}, { useMasterKey: true }).then((results) => { + Parse._request( + 'PUT', + `push_audiences/${results.objectId}`, + { name: 'My Audience 2' }, + { useMasterKey: true } + ).then(() => { + Parse._request( + 'GET', + `push_audiences/${results.objectId}`, + {}, + { useMasterKey: true } + ).then(results => { expect(results.name).toEqual('My Audience 2'); expect(results.query.deviceType).toEqual('ios'); - Parse._request('DELETE', `push_audiences/${results.objectId}`, {}, { useMasterKey: true }).then(() => { - Parse._request('GET', 'push_audiences', {}, { useMasterKey: true }).then((results) => { + Parse._request( + 'DELETE', + `push_audiences/${results.objectId}`, + {}, + { useMasterKey: true } + ).then(() => { + Parse._request( + 'GET', + 'push_audiences', + {}, + { useMasterKey: true } + ).then(results => { expect(results.results.length).toEqual(0); done(); }); @@ -226,112 +289,145 @@ describe('AudiencesRouter', () => { }); }); }); - }); - }); - }); - - it('should only create with master key', (done) => { - Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' })}) - .then( - () => {}, - (error) => { - expect(error.message).toEqual('unauthorized: master key is required'); - done(); } ); + }); }); - it('should only find with master key', (done) => { - Parse._request('GET', 'push_audiences', {}) - .then( - () => {}, - (error) => { - expect(error.message).toEqual('unauthorized: master key is required'); - done(); - } - ); + it('should only create with master key', done => { + Parse._request('POST', 'push_audiences', { + name: 'My Audience', + query: JSON.stringify({ deviceType: 'ios' }), + }).then( + () => {}, + error => { + expect(error.message).toEqual('unauthorized: master key is required'); + done(); + } + ); }); - it('should only get with master key', (done) => { - Parse._request('GET', `push_audiences/someId`, {}) - .then( - () => {}, - (error) => { - expect(error.message).toEqual('unauthorized: master key is required'); - done(); - } - ); + it('should only find with master key', done => { + Parse._request('GET', 'push_audiences', {}).then( + () => {}, + error => { + expect(error.message).toEqual('unauthorized: master key is required'); + done(); + } + ); }); - it('should only update with master key', (done) => { - Parse._request('PUT', `push_audiences/someId`, { name: 'My Audience 2' }) - .then( - () => {}, - (error) => { - expect(error.message).toEqual('unauthorized: master key is required'); - done(); - } - ); + it('should only get with master key', done => { + Parse._request('GET', `push_audiences/someId`, {}).then( + () => {}, + error => { + expect(error.message).toEqual('unauthorized: master key is required'); + done(); + } + ); }); - it('should only delete with master key', (done) => { - Parse._request('DELETE', `push_audiences/someId`, {}) - .then( - () => {}, - (error) => { - expect(error.message).toEqual('unauthorized: master key is required'); - done(); - } - ); + it('should only update with master key', done => { + Parse._request('PUT', `push_audiences/someId`, { + name: 'My Audience 2', + }).then( + () => {}, + error => { + expect(error.message).toEqual('unauthorized: master key is required'); + done(); + } + ); }); - it_exclude_dbs(['postgres'])('should support legacy parse.com audience fields', (done) => { - const database = (Config.get(Parse.applicationId)).database.adapter.database; - const now = new Date(); - Parse._request('POST', 'push_audiences', { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' })}, { useMasterKey: true }) - .then((audience) => { + it('should only delete with master key', done => { + Parse._request('DELETE', `push_audiences/someId`, {}).then( + () => {}, + error => { + expect(error.message).toEqual('unauthorized: master key is required'); + done(); + } + ); + }); + + it_exclude_dbs(['postgres'])( + 'should support legacy parse.com audience fields', + done => { + const database = Config.get(Parse.applicationId).database.adapter + .database; + const now = new Date(); + Parse._request( + 'POST', + 'push_audiences', + { name: 'My Audience', query: JSON.stringify({ deviceType: 'ios' }) }, + { useMasterKey: true } + ).then(audience => { database.collection('test__Audience').updateOne( { _id: audience.objectId }, { $set: { times_used: 1, - _last_used: now - } + _last_used: now, + }, }, {}, - (error) => { - expect(error).toEqual(null) - database.collection('test__Audience').find({ _id: audience.objectId}).toArray( - (error, rows) => { - expect(error).toEqual(null) + error => { + expect(error).toEqual(null); + database + .collection('test__Audience') + .find({ _id: audience.objectId }) + .toArray((error, rows) => { + expect(error).toEqual(null); expect(rows[0]['times_used']).toEqual(1); expect(rows[0]['_last_used']).toEqual(now); - Parse._request('GET', 'push_audiences/' + audience.objectId, {}, {useMasterKey: true}) - .then((audience) => { + Parse._request( + 'GET', + 'push_audiences/' + audience.objectId, + {}, + { useMasterKey: true } + ) + .then(audience => { expect(audience.name).toEqual('My Audience'); expect(audience.query.deviceType).toEqual('ios'); expect(audience.timesUsed).toEqual(1); expect(audience.lastUsed).toEqual(now.toISOString()); done(); }) - .catch((error) => { done.fail(error); }) + .catch(error => { + done.fail(error); + }); }); - }); + } + ); }); - }); + } + ); - it('should be able to search on audiences', (done) => { - Parse._request('POST', 'push_audiences', { name: 'neverUsed', query: JSON.stringify({ deviceType: 'ios' })}, { useMasterKey: true }) - .then(() => { - const query = {"timesUsed": {"$exists": false}, "lastUsed": {"$exists": false}}; - Parse._request('GET', 'push_audiences?order=-createdAt&limit=1', {where: query}, {useMasterKey: true}) - .then((results) => { - expect(results.results.length).toEqual(1); - const audience = results.results[0]; - expect(audience.name).toEqual("neverUsed"); - done(); - }) - .catch((error) => { done.fail(error); }) - }) + it('should be able to search on audiences', done => { + Parse._request( + 'POST', + 'push_audiences', + { name: 'neverUsed', query: JSON.stringify({ deviceType: 'ios' }) }, + { useMasterKey: true } + ).then(() => { + const query = { + timesUsed: { $exists: false }, + lastUsed: { $exists: false }, + }; + Parse._request( + 'GET', + 'push_audiences?order=-createdAt&limit=1', + { where: query }, + { useMasterKey: true } + ) + .then(results => { + expect(results.results.length).toEqual(1); + const audience = results.results[0]; + expect(audience.name).toEqual('neverUsed'); + done(); + }) + .catch(error => { + done.fail(error); + }); + }); }); }); diff --git a/spec/Auth.spec.js b/spec/Auth.spec.js index 81aacb01aa..5595f6baba 100644 --- a/spec/Auth.spec.js +++ b/spec/Auth.spec.js @@ -14,45 +14,45 @@ describe('Auth', () => { cacheController: { role: { get: () => Promise.resolve(currentRoles), - set: jasmine.createSpy('set') - } - } - } + set: jasmine.createSpy('set'), + }, + }, + }; spyOn(config.cacheController.role, 'get').and.callThrough(); auth = new Auth({ config: config, isMaster: false, user: { - id: currentUserId + id: currentUserId, }, - installationId: 'installationId' + installationId: 'installationId', }); }); - it('should get user roles from the cache', (done) => { - auth.getUserRoles() - .then((roles) => { - const firstSet = config.cacheController.role.set.calls.first(); - expect(firstSet).toEqual(undefined); + it('should get user roles from the cache', done => { + auth.getUserRoles().then(roles => { + const firstSet = config.cacheController.role.set.calls.first(); + expect(firstSet).toEqual(undefined); - const firstGet = config.cacheController.role.get.calls.first(); - expect(firstGet.args[0]).toEqual(currentUserId); - expect(roles).toEqual(currentRoles); - done(); - }); + const 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) => { + it('should only query the roles once', done => { const loadRolesSpy = spyOn(auth, '_loadRoles').and.callThrough(); - auth.getUserRoles() - .then((roles) => { + auth + .getUserRoles() + .then(roles => { expect(roles).toEqual(currentRoles); - return auth.getUserRoles() + return auth.getUserRoles(); }) .then(() => auth.getUserRoles()) .then(() => auth.getUserRoles()) - .then((roles) => { + .then(roles => { // Should only call the cache adapter once. expect(config.cacheController.role.get.calls.count()).toEqual(1); expect(loadRolesSpy.calls.count()).toEqual(1); @@ -64,42 +64,43 @@ describe('Auth', () => { }); }); - it('should not have any roles with no user', (done) => { - auth.user = null - auth.getUserRoles() - .then((roles) => expect(roles).toEqual([])) + 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([])) + it('should not have any user roles with master', done => { + auth.isMaster = true; + auth + .getUserRoles() + .then(roles => expect(roles).toEqual([])) .then(() => done()); }); - it('should properly handle bcrypt upgrade', (done) => { + it('should properly handle bcrypt upgrade', done => { const bcryptOriginal = require('bcrypt-nodejs'); const bcryptNew = require('bcryptjs'); bcryptOriginal.hash('my1Long:password', null, null, function(err, res) { bcryptNew.compare('my1Long:password', res, function(err, res) { expect(res).toBeTruthy(); done(); - }) + }); }); }); - }); it('should load auth without a config', async () => { const user = new Parse.User(); await user.signUp({ username: 'hello', - password: 'password' + password: 'password', }); expect(user.getSessionToken()).not.toBeUndefined(); const userAuth = await getAuthForSessionToken({ - sessionToken: user.getSessionToken() + sessionToken: user.getSessionToken(), }); expect(userAuth.user instanceof Parse.User).toBe(true); expect(userAuth.user.id).toBe(user.id); @@ -109,7 +110,7 @@ describe('Auth', () => { const user = new Parse.User(); await user.signUp({ username: 'hello', - password: 'password' + password: 'password', }); expect(user.getSessionToken()).not.toBeUndefined(); const userAuth = await getAuthForSessionToken({ diff --git a/spec/AuthenticationAdapters.spec.js b/spec/AuthenticationAdapters.spec.js index 3819028e7d..c3129597c1 100644 --- a/spec/AuthenticationAdapters.spec.js +++ b/spec/AuthenticationAdapters.spec.js @@ -1,31 +1,48 @@ const request = require('request'); -const Config = require("../lib/Config"); -const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns; +const Config = require('../lib/Config'); +const defaultColumns = require('../lib/Controllers/SchemaController') + .defaultColumns; const authenticationLoader = require('../lib/Adapters/Auth'); const path = require('path'); const responses = { instagram: { data: { id: 'userId' } }, - janrainengage: { stat: 'ok', profile: { identifier: 'userId' }}, + janrainengage: { stat: 'ok', profile: { identifier: 'userId' } }, janraincapture: { stat: 'ok', result: 'userId' }, - vkontakte: { response: [{ id: 'userId'}]}, + vkontakte: { response: [{ id: 'userId' }] }, google: { sub: 'userId' }, wechat: { errcode: 0 }, weibo: { uid: 'userId' }, - qq: 'callback( {"openid":"userId"} );' // yes it's like that, run eval in the client :P -} + qq: 'callback( {"openid":"userId"} );', // yes it's like that, run eval in the client :P +}; describe('AuthenticationProviders', function() { - ["facebook", "facebookaccountkit", "github", "instagram", "google", "linkedin", "meetup", "twitter", "janrainengage", "janraincapture", "vkontakte", "qq", "spotify", "wechat", "weibo"].map(function(providerName){ - it("Should validate structure of " + providerName, (done) => { - const provider = require("../lib/Adapters/Auth/" + providerName); - jequal(typeof provider.validateAuthData, "function"); - jequal(typeof provider.validateAppId, "function"); + [ + 'facebook', + 'facebookaccountkit', + 'github', + 'instagram', + 'google', + 'linkedin', + 'meetup', + 'twitter', + 'janrainengage', + 'janraincapture', + 'vkontakte', + 'qq', + 'spotify', + 'wechat', + 'weibo', + ].map(function(providerName) { + it('Should validate structure of ' + providerName, done => { + const provider = require('../lib/Adapters/Auth/' + providerName); + jequal(typeof provider.validateAuthData, 'function'); + jequal(typeof provider.validateAppId, 'function'); const authDataPromise = provider.validateAuthData({}, {}); - const validateAppIdPromise = provider.validateAppId("app", "key", {}); + const validateAppIdPromise = provider.validateAppId('app', 'key', {}); jequal(authDataPromise.constructor, Promise.prototype.constructor); jequal(validateAppIdPromise.constructor, Promise.prototype.constructor); - authDataPromise.then(()=>{}, ()=>{}); - validateAppIdPromise.then(()=>{}, ()=>{}); + authDataPromise.then(() => {}, () => {}); + validateAppIdPromise.then(() => {}, () => {}); done(); }); @@ -33,24 +50,32 @@ describe('AuthenticationProviders', function() { if (providerName === 'twitter') { return; } - spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake((options) => { - if (options === "https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.59&grant_type=client_credentials") { - return { - access_token: 'access_token' + spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'get').and.callFake( + options => { + if ( + options === + 'https://oauth.vk.com/access_token?client_id=appId&client_secret=appSecret&v=5.59&grant_type=client_credentials' + ) { + return { + access_token: 'access_token', + }; } + return Promise.resolve(responses[providerName] || { id: 'userId' }); } + ); + spyOn( + require('../lib/Adapters/Auth/httpsRequest'), + 'request' + ).and.callFake(() => { return Promise.resolve(responses[providerName] || { id: 'userId' }); }); - spyOn(require('../lib/Adapters/Auth/httpsRequest'), 'request').and.callFake(() => { - return Promise.resolve(responses[providerName] || { id: 'userId' }); - }); - const provider = require("../lib/Adapters/Auth/" + providerName); + const provider = require('../lib/Adapters/Auth/' + providerName); let params = {}; if (providerName === 'vkontakte') { params = { appIds: 'appId', - appSecret: 'appSecret' - } + appSecret: 'appSecret', + }; } await provider.validateAuthData({ id: 'userId' }, params); }); @@ -59,8 +84,8 @@ describe('AuthenticationProviders', function() { const getMockMyOauthProvider = function() { return { authData: { - id: "12345", - access_token: "12345", + id: '12345', + access_token: '12345', expiration_date: new Date().toJSON(), }, shouldError: false, @@ -71,7 +96,7 @@ describe('AuthenticationProviders', function() { authenticate: function(options) { if (this.shouldError) { - options.error(this, "An error occurred"); + options.error(this, 'An error occurred'); } else if (this.shouldCancel) { options.error(this, null); } else { @@ -91,54 +116,56 @@ describe('AuthenticationProviders', function() { return true; }, getAuthType: function() { - return "myoauth"; + return 'myoauth'; }, deauthenticate: function() { this.loggedOut = true; this.restoreAuthentication(null); - } + }, }; }; Parse.User.extend({ extended: function() { return true; - } + }, }); const createOAuthUser = function(callback) { return createOAuthUserWithSessionToken(undefined, callback); - } + }; const createOAuthUserWithSessionToken = function(token, callback) { const jsonBody = { authData: { - myoauth: getMockMyOauthProvider().authData - } + myoauth: getMockMyOauthProvider().authData, + }, }; const options = { - headers: {'X-Parse-Application-Id': 'test', + headers: { + 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', 'X-Parse-Installation-Id': 'yolo', 'X-Parse-Session-Token': token, - 'Content-Type': 'application/json' }, + 'Content-Type': 'application/json', + }, url: 'http://localhost:8378/1/users', body: jsonBody, - json: true + json: true, }; - return new Promise((resolve) => { + return new Promise(resolve => { request.post(options, (err, res, body) => { - resolve({err, res, body}); + resolve({ err, res, body }); if (callback) { callback(err, res, body); } }); }); - } + }; - it("should create user with REST API", done => { + it('should create user with REST API', done => { createOAuthUser((error, response, body) => { expect(error).toBe(null); const b = body; @@ -146,28 +173,30 @@ describe('AuthenticationProviders', function() { expect(b.objectId).not.toBeNull(); expect(b.objectId).not.toBeUndefined(); const sessionToken = b.sessionToken; - const q = new Parse.Query("_Session"); + const q = new Parse.Query('_Session'); q.equalTo('sessionToken', sessionToken); - q.first({useMasterKey: true}).then((res) => { - if (!res) { + q.first({ useMasterKey: true }) + .then(res => { + if (!res) { + fail('should not fail fetching the session'); + done(); + return; + } + expect(res.get('installationId')).toEqual('yolo'); + done(); + }) + .catch(() => { fail('should not fail fetching the session'); done(); - return; - } - expect(res.get("installationId")).toEqual('yolo'); - done(); - }).catch(() => { - fail('should not fail fetching the session'); - done(); - }) + }); }); }); - it("should only create a single user with REST API", (done) => { + it('should only create a single user with REST API', done => { let objectId; createOAuthUser((error, response, body) => { expect(error).toBe(null); - const b = body + const b = body; expect(b.objectId).not.toBeNull(); expect(b.objectId).not.toBeUndefined(); objectId = b.objectId; @@ -183,59 +212,72 @@ describe('AuthenticationProviders', function() { }); }); - it("should fail to link if session token don't match user", (done) => { - Parse.User.signUp('myUser', 'password').then((user) => { - return createOAuthUserWithSessionToken(user.getSessionToken()); - }).then(() => { - return Parse.User.logOut(); - }).then(() => { - return Parse.User.signUp('myUser2', 'password'); - }).then((user) => { - return createOAuthUserWithSessionToken(user.getSessionToken()); - }).then(({ body }) => { - expect(body.code).toBe(208); - expect(body.error).toBe('this auth is already used'); - done(); - }).catch(done.fail); + it("should fail to link if session token don't match user", done => { + Parse.User.signUp('myUser', 'password') + .then(user => { + return createOAuthUserWithSessionToken(user.getSessionToken()); + }) + .then(() => { + return Parse.User.logOut(); + }) + .then(() => { + return Parse.User.signUp('myUser2', 'password'); + }) + .then(user => { + return createOAuthUserWithSessionToken(user.getSessionToken()); + }) + .then(({ body }) => { + expect(body.code).toBe(208); + expect(body.error).toBe('this auth is already used'); + done(); + }) + .catch(done.fail); }); - it("unlink and link with custom provider", async () => { + it('unlink and link with custom provider', async () => { const provider = getMockMyOauthProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("myoauth"); - ok(model instanceof Parse.User, "Model should be a Parse.User"); + const model = await Parse.User._logInWith('myoauth'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); strictEqual(Parse.User.current(), model); - ok(model.extended(), "Should have used the subclass."); + ok(model.extended(), 'Should have used the subclass.'); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("myoauth"), "User should be linked to myoauth"); - - await model._unlinkFrom("myoauth"); - ok(!model._isLinked("myoauth"), - "User should not be linked to myoauth"); - ok(!provider.synchronizedUserId, "User id should be cleared"); - ok(!provider.synchronizedAuthToken, "Auth token should be cleared"); - ok(!provider.synchronizedExpiration, - "Expiration should be cleared"); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('myoauth'), 'User should be linked to myoauth'); + + await model._unlinkFrom('myoauth'); + ok(!model._isLinked('myoauth'), 'User should not be linked to myoauth'); + ok(!provider.synchronizedUserId, 'User id should be cleared'); + ok(!provider.synchronizedAuthToken, 'Auth token should be cleared'); + ok(!provider.synchronizedExpiration, 'Expiration should be cleared'); // make sure the auth data is properly deleted const config = Config.get(Parse.applicationId); - const res = await config.database.adapter.find('_User', { - fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation), - }, { objectId: model.id }, {}) + const res = await config.database.adapter.find( + '_User', + { + fields: Object.assign( + {}, + defaultColumns._Default, + defaultColumns._Installation + ), + }, + { objectId: model.id }, + {} + ); expect(res.length).toBe(1); expect(res[0]._auth_data_myoauth).toBeUndefined(); expect(res[0]._auth_data_myoauth).not.toBeNull(); - await model._linkWith("myoauth"); + await model._linkWith('myoauth'); - ok(provider.synchronizedUserId, "User id should have a value"); - ok(provider.synchronizedAuthToken, - "Auth token should have a value"); - ok(provider.synchronizedExpiration, - "Expiration should have a value"); - ok(model._isLinked("myoauth"), - "User should be linked to myoauth"); + ok(provider.synchronizedUserId, 'User id should have a value'); + ok(provider.synchronizedAuthToken, 'Auth token should have a value'); + ok(provider.synchronizedExpiration, 'Expiration should have a value'); + ok(model._isLinked('myoauth'), 'User should be linked to myoauth'); }); function validateValidator(validator) { @@ -244,101 +286,132 @@ describe('AuthenticationProviders', function() { function validateAuthenticationHandler(authenticationHandler) { expect(authenticationHandler).not.toBeUndefined(); - expect(typeof authenticationHandler.getValidatorForProvider).toBe('function'); - expect(typeof authenticationHandler.getValidatorForProvider).toBe('function'); + expect(typeof authenticationHandler.getValidatorForProvider).toBe( + 'function' + ); + expect(typeof authenticationHandler.getValidatorForProvider).toBe( + 'function' + ); } function validateAuthenticationAdapter(authAdapter) { expect(authAdapter).not.toBeUndefined(); - if (!authAdapter) { return; } + if (!authAdapter) { + return; + } expect(typeof authAdapter.validateAuthData).toBe('function'); expect(typeof authAdapter.validateAppId).toBe('function'); } - it('properly loads custom adapter', (done) => { + it('properly loads custom adapter', done => { const validAuthData = { id: 'hello', - token: 'world' - } + token: 'world', + }; const adapter = { validateAppId: function() { return Promise.resolve(); }, validateAuthData: function(authData) { - if (authData.id == validAuthData.id && authData.token == validAuthData.token) { + if ( + authData.id == validAuthData.id && + authData.token == validAuthData.token + ) { return Promise.resolve(); } return Promise.reject(); - } + }, }; const authDataSpy = spyOn(adapter, 'validateAuthData').and.callThrough(); const appIdSpy = spyOn(adapter, 'validateAppId').and.callThrough(); const authenticationHandler = authenticationLoader({ - customAuthentication: adapter + customAuthentication: adapter, }); validateAuthenticationHandler(authenticationHandler); - const validator = authenticationHandler.getValidatorForProvider('customAuthentication'); + const validator = authenticationHandler.getValidatorForProvider( + 'customAuthentication' + ); validateValidator(validator); - validator(validAuthData).then(() => { - expect(authDataSpy).toHaveBeenCalled(); - // AppIds are not provided in the adapter, should not be called - expect(appIdSpy).not.toHaveBeenCalled(); - done(); - }, (err) => { - jfail(err); - done(); - }) + validator(validAuthData).then( + () => { + expect(authDataSpy).toHaveBeenCalled(); + // AppIds are not provided in the adapter, should not be called + expect(appIdSpy).not.toHaveBeenCalled(); + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - it('properly loads custom adapter module object', (done) => { + it('properly loads custom adapter module object', done => { const authenticationHandler = authenticationLoader({ - customAuthentication: path.resolve('./spec/support/CustomAuth.js') + customAuthentication: path.resolve('./spec/support/CustomAuth.js'), }); validateAuthenticationHandler(authenticationHandler); - const validator = authenticationHandler.getValidatorForProvider('customAuthentication'); + const validator = authenticationHandler.getValidatorForProvider( + 'customAuthentication' + ); validateValidator(validator); validator({ - token: 'my-token' - }).then(() => { - done(); - }, (err) => { - jfail(err); - done(); - }) + token: 'my-token', + }).then( + () => { + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - it('properly loads custom adapter module object (again)', (done) => { + it('properly loads custom adapter module object (again)', done => { const authenticationHandler = authenticationLoader({ - customAuthentication: { module: path.resolve('./spec/support/CustomAuthFunction.js'), options: { token: 'valid-token' }} + customAuthentication: { + module: path.resolve('./spec/support/CustomAuthFunction.js'), + options: { token: 'valid-token' }, + }, }); validateAuthenticationHandler(authenticationHandler); - const validator = authenticationHandler.getValidatorForProvider('customAuthentication'); + const validator = authenticationHandler.getValidatorForProvider( + 'customAuthentication' + ); validateValidator(validator); validator({ - token: 'valid-token' - }).then(() => { - done(); - }, (err) => { - jfail(err); - done(); - }) + token: 'valid-token', + }).then( + () => { + done(); + }, + err => { + jfail(err); + done(); + } + ); }); it('properly loads a default adapter with options', () => { const options = { facebook: { - appIds: ['a', 'b'] - } + appIds: ['a', 'b'], + }, }; - const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter('facebook', options); + const { + adapter, + appIds, + providerOptions, + } = authenticationLoader.loadAuthAdapter('facebook', options); validateAuthenticationAdapter(adapter); expect(appIds).toEqual(['a', 'b']); expect(providerOptions).toEqual(options.facebook); @@ -349,10 +422,14 @@ describe('AuthenticationProviders', function() { custom: { validateAppId: () => {}, validateAuthData: () => {}, - appIds: ['a', 'b'] - } + appIds: ['a', 'b'], + }, }; - const { adapter, appIds, providerOptions } = authenticationLoader.loadAuthAdapter('custom', options); + const { + adapter, + appIds, + providerOptions, + } = authenticationLoader.loadAuthAdapter('custom', options); validateAuthenticationAdapter(adapter); expect(appIds).toEqual(['a', 'b']); expect(providerOptions).toEqual(options.custom); @@ -362,66 +439,76 @@ describe('AuthenticationProviders', function() { const options = { facebookaccountkit: { appIds: ['a', 'b'], - appSecret: 'secret' - } + appSecret: 'secret', + }, }; - const {adapter, appIds, providerOptions} = authenticationLoader.loadAuthAdapter('facebookaccountkit', options); + const { + adapter, + appIds, + providerOptions, + } = authenticationLoader.loadAuthAdapter('facebookaccountkit', options); validateAuthenticationAdapter(adapter); expect(appIds).toEqual(['a', 'b']); expect(providerOptions.appSecret).toEqual('secret'); }); - it('should fail if Facebook appIds is not configured properly', (done) => { + it('should fail if Facebook appIds is not configured properly', done => { const options = { facebookaccountkit: { - appIds: [] - } + appIds: [], + }, }; - const {adapter, appIds} = authenticationLoader.loadAuthAdapter('facebookaccountkit', options); - adapter.validateAppId(appIds) - .then(done.fail, err => { - expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND); - done(); - }) + const { adapter, appIds } = authenticationLoader.loadAuthAdapter( + 'facebookaccountkit', + options + ); + adapter.validateAppId(appIds).then(done.fail, err => { + expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + done(); + }); }); - it('should fail to validate Facebook accountkit auth with bad token', (done) => { + it('should fail to validate Facebook accountkit auth with bad token', done => { const options = { facebookaccountkit: { - appIds: ['a', 'b'] - } + appIds: ['a', 'b'], + }, }; const authData = { id: 'fakeid', - access_token: 'badtoken' + access_token: 'badtoken', }; - const {adapter} = authenticationLoader.loadAuthAdapter('facebookaccountkit', options); - adapter.validateAuthData(authData) - .then(done.fail, err => { - expect(err.code).toBe(190); - expect(err.type).toBe('OAuthException'); - done(); - }) + const { adapter } = authenticationLoader.loadAuthAdapter( + 'facebookaccountkit', + options + ); + adapter.validateAuthData(authData).then(done.fail, err => { + expect(err.code).toBe(190); + expect(err.type).toBe('OAuthException'); + done(); + }); }); - it('should fail to validate Facebook accountkit auth with bad token regardless of app secret proof', (done) => { + it('should fail to validate Facebook accountkit auth with bad token regardless of app secret proof', done => { const options = { facebookaccountkit: { appIds: ['a', 'b'], - appSecret: 'badsecret' - } + appSecret: 'badsecret', + }, }; const authData = { id: 'fakeid', - access_token: 'badtoken' + access_token: 'badtoken', }; - const {adapter, providerOptions} = authenticationLoader.loadAuthAdapter('facebookaccountkit', options); - adapter.validateAuthData(authData, providerOptions) - .then(done.fail, err => { - expect(err.code).toBe(190); - expect(err.type).toBe('OAuthException'); - done(); - }) + const { adapter, providerOptions } = authenticationLoader.loadAuthAdapter( + 'facebookaccountkit', + options + ); + adapter.validateAuthData(authData, providerOptions).then(done.fail, err => { + expect(err.code).toBe(190); + expect(err.type).toBe('OAuthException'); + done(); + }); }); }); @@ -447,7 +534,10 @@ describe('google auth adapter', () => { spyOn(httpsRequest, 'get').and.callFake(() => { return Promise.resolve({ user_id: 'userId' }); }); - await google.validateAuthData({ id: 'userId', access_token: 'the_token' }, {}); + await google.validateAuthData( + { id: 'userId', access_token: 'the_token' }, + {} + ); }); it('should use access_token for validation is passed with sub', async () => { @@ -462,9 +552,12 @@ describe('google auth adapter', () => { return Promise.resolve({ sub: 'badId' }); }); try { - await google.validateAuthData({ id: 'userId', id_token: 'the_token' }, {}); - fail() - } catch(e) { + await google.validateAuthData( + { id: 'userId', id_token: 'the_token' }, + {} + ); + fail(); + } catch (e) { expect(e.message).toBe('Google auth is invalid for this user.'); } }); @@ -474,9 +567,12 @@ describe('google auth adapter', () => { return Promise.resolve({ sub: 'badId' }); }); try { - await google.validateAuthData({ id: 'userId', access_token: 'the_token' }, {}); - fail() - } catch(e) { + await google.validateAuthData( + { id: 'userId', access_token: 'the_token' }, + {} + ); + fail(); + } catch (e) { expect(e.message).toBe('Google auth is invalid for this user.'); } }); diff --git a/spec/CLI.spec.js b/spec/CLI.spec.js index 36056fd1f5..ea820d7764 100644 --- a/spec/CLI.spec.js +++ b/spec/CLI.spec.js @@ -1,15 +1,16 @@ 'use strict'; const commander = require('../lib/cli/utils/commander').default; const definitions = require('../lib/cli/definitions/parse-server').default; -const liveQueryDefinitions = require('../lib/cli/definitions/parse-live-query-server').default; +const liveQueryDefinitions = require('../lib/cli/definitions/parse-live-query-server') + .default; const testDefinitions = { - 'arg0': 'PROGRAM_ARG_0', - 'arg1': { + arg0: 'PROGRAM_ARG_0', + arg1: { env: 'PROGRAM_ARG_1', - required: true + required: true, }, - 'arg2': { + arg2: { env: 'PROGRAM_ARG_2', action: function(value) { const intValue = parseInt(value); @@ -17,16 +18,16 @@ const testDefinitions = { throw 'arg2 is invalid'; } return intValue; - } + }, + }, + arg3: {}, + arg4: { + default: 'arg4Value', }, - 'arg3': {}, - 'arg4': { - default: 'arg4Value' - } }; describe('commander additions', () => { - afterEach((done) => { + afterEach(done => { commander.options = []; delete commander.arg0; delete commander.arg1; @@ -36,9 +37,20 @@ describe('commander additions', () => { done(); }); - it('should load properly definitions from args', (done) => { + it('should load properly definitions from args', done => { commander.loadDefinitions(testDefinitions); - commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', '--arg1', 'arg1Value', '--arg2', '2', '--arg3', 'some']); + commander.parse([ + 'node', + './CLI.spec.js', + '--arg0', + 'arg0Value', + '--arg1', + 'arg1Value', + '--arg2', + '2', + '--arg3', + 'some', + ]); expect(commander.arg0).toEqual('arg0Value'); expect(commander.arg1).toEqual('arg1Value'); expect(commander.arg2).toEqual(2); @@ -47,12 +59,12 @@ describe('commander additions', () => { done(); }); - it('should load properly definitions from env', (done) => { + it('should load properly definitions from env', done => { commander.loadDefinitions(testDefinitions); commander.parse([], { - 'PROGRAM_ARG_0': 'arg0ENVValue', - 'PROGRAM_ARG_1': 'arg1ENVValue', - 'PROGRAM_ARG_2': '3', + PROGRAM_ARG_0: 'arg0ENVValue', + PROGRAM_ARG_1: 'arg1ENVValue', + PROGRAM_ARG_2: '3', }); expect(commander.arg0).toEqual('arg0ENVValue'); expect(commander.arg1).toEqual('arg1ENVValue'); @@ -61,14 +73,17 @@ describe('commander additions', () => { done(); }); - it('should load properly use args over env', (done) => { + it('should load properly use args over env', done => { commander.loadDefinitions(testDefinitions); - commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', '--arg4', ''], { - 'PROGRAM_ARG_0': 'arg0ENVValue', - 'PROGRAM_ARG_1': 'arg1ENVValue', - 'PROGRAM_ARG_2': '4', - 'PROGRAM_ARG_4': 'arg4ENVValue' - }); + commander.parse( + ['node', './CLI.spec.js', '--arg0', 'arg0Value', '--arg4', ''], + { + PROGRAM_ARG_0: 'arg0ENVValue', + PROGRAM_ARG_1: 'arg1ENVValue', + PROGRAM_ARG_2: '4', + PROGRAM_ARG_4: 'arg4ENVValue', + } + ); expect(commander.arg0).toEqual('arg0Value'); expect(commander.arg1).toEqual('arg1ENVValue'); expect(commander.arg2).toEqual(4); @@ -76,24 +91,33 @@ describe('commander additions', () => { done(); }); - it('should fail in action as port is invalid', (done) => { + it('should fail in action as port is invalid', done => { commander.loadDefinitions(testDefinitions); - expect(()=> { - commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value'], { - 'PROGRAM_ARG_0': 'arg0ENVValue', - 'PROGRAM_ARG_1': 'arg1ENVValue', - 'PROGRAM_ARG_2': 'hello', + expect(() => { + commander.parse(['node', './CLI.spec.js', '--arg0', 'arg0Value'], { + PROGRAM_ARG_0: 'arg0ENVValue', + PROGRAM_ARG_1: 'arg1ENVValue', + PROGRAM_ARG_2: 'hello', }); }).toThrow('arg2 is invalid'); done(); }); - it('should not override config.json', (done) => { + it('should not override config.json', done => { commander.loadDefinitions(testDefinitions); - commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', './spec/configs/CLIConfig.json'], { - 'PROGRAM_ARG_0': 'arg0ENVValue', - 'PROGRAM_ARG_1': 'arg1ENVValue', - }); + commander.parse( + [ + 'node', + './CLI.spec.js', + '--arg0', + 'arg0Value', + './spec/configs/CLIConfig.json', + ], + { + PROGRAM_ARG_0: 'arg0ENVValue', + PROGRAM_ARG_1: 'arg1ENVValue', + } + ); const options = commander.getOptions(); expect(options.arg2).toBe(8888); expect(options.arg3).toBe('hello'); //config value @@ -101,28 +125,45 @@ describe('commander additions', () => { done(); }); - it('should fail with invalid values in JSON', (done) => { + it('should fail with invalid values in JSON', done => { commander.loadDefinitions(testDefinitions); expect(() => { - commander.parse(['node','./CLI.spec.js','--arg0', 'arg0Value', './spec/configs/CLIConfigFail.json'], { - 'PROGRAM_ARG_0': 'arg0ENVValue', - 'PROGRAM_ARG_1': 'arg1ENVValue', - }); + 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) => { + it('should fail when too many apps are set', done => { commander.loadDefinitions(testDefinitions); expect(() => { - commander.parse(['node','./CLI.spec.js','./spec/configs/CLIConfigFailTooManyApps.json']); + commander.parse([ + 'node', + './CLI.spec.js', + './spec/configs/CLIConfigFailTooManyApps.json', + ]); }).toThrow('Multiple apps are not supported'); done(); }); - it('should load config from apps', (done) => { + it('should load config from apps', done => { commander.loadDefinitions(testDefinitions); - commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigApps.json']); + commander.parse([ + 'node', + './CLI.spec.js', + './spec/configs/CLIConfigApps.json', + ]); const options = commander.getOptions(); expect(options.arg1).toBe('my_app'); expect(options.arg2).toBe(8888); @@ -131,10 +172,14 @@ describe('commander additions', () => { done(); }); - it('should fail when passing an invalid arguement', (done) => { + it('should fail when passing an invalid arguement', done => { commander.loadDefinitions(testDefinitions); expect(() => { - commander.parse(['node', './CLI.spec.js', './spec/configs/CLIConfigUnknownArg.json']); + commander.parse([ + 'node', + './CLI.spec.js', + './spec/configs/CLIConfigUnknownArg.json', + ]); }).toThrow('error: unknown option myArg'); done(); }); @@ -160,7 +205,7 @@ describe('definitions', () => { it('should throw when using deprecated facebookAppIds', () => { expect(() => { - definitions.facebookAppIds.action() + definitions.facebookAppIds.action(); }).toThrow(); }); }); @@ -173,7 +218,10 @@ describe('LiveQuery definitions', () => { if (typeof definition.env !== 'undefined') { expect(typeof definition.env).toBe('string'); } - expect(typeof definition.help).toBe('string', `help for ${key} should be a string`); + expect(typeof definition.help).toBe( + 'string', + `help for ${key} should be a string` + ); if (typeof definition.required !== 'undefined') { expect(typeof definition.required).toBe('boolean'); } diff --git a/spec/CacheController.spec.js b/spec/CacheController.spec.js index ec37ae6b2b..12f4269fd4 100644 --- a/spec/CacheController.spec.js +++ b/spec/CacheController.spec.js @@ -1,4 +1,5 @@ -const CacheController = require('../lib/Controllers/CacheController.js').default; +const CacheController = require('../lib/Controllers/CacheController.js') + .default; describe('CacheController', function() { let FakeCacheAdapter; @@ -10,14 +11,13 @@ describe('CacheController', function() { get: () => Promise.resolve(null), put: jasmine.createSpy('put'), del: jasmine.createSpy('del'), - clear: jasmine.createSpy('clear') - } + clear: jasmine.createSpy('clear'), + }; spyOn(FakeCacheAdapter, 'get').and.callThrough(); }); - - it('should expose role and user caches', (done) => { + it('should expose role and user caches', done => { const cache = new CacheController(FakeCacheAdapter, FakeAppID); expect(cache.role).not.toEqual(null); @@ -28,8 +28,7 @@ describe('CacheController', function() { done(); }); - - ['role', 'user'].forEach((cacheName) => { + ['role', 'user'].forEach(cacheName => { it('should prefix ' + cacheName + ' cache', () => { const cache = new CacheController(FakeCacheAdapter, FakeAppID)[cacheName]; @@ -60,8 +59,7 @@ describe('CacheController', function() { expect(FakeCacheAdapter.clear.calls.count()).toEqual(3); }); - it('should handle cache rejections', (done) => { - + it('should handle cache rejections', done => { FakeCacheAdapter.get = () => Promise.reject(); const cache = new CacheController(FakeCacheAdapter, FakeAppID); @@ -70,5 +68,4 @@ describe('CacheController', function() { fail('Promise should not be rejected.'); }); }); - }); diff --git a/spec/Client.spec.js b/spec/Client.spec.js index b95b673d6f..400503d289 100644 --- a/spec/Client.spec.js +++ b/spec/Client.spec.js @@ -1,5 +1,6 @@ const Client = require('../lib/LiveQuery/Client').Client; -const ParseWebSocket = require('../lib/LiveQuery/ParseWebSocketServer').ParseWebSocket; +const ParseWebSocket = require('../lib/LiveQuery/ParseWebSocketServer') + .ParseWebSocket; describe('Client', function() { it('can be initialized', function() { @@ -13,7 +14,7 @@ describe('Client', function() { it('can push response', function() { const parseWebSocket = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; Client.pushResponse(parseWebSocket, 'message'); @@ -22,7 +23,7 @@ describe('Client', function() { it('can push error', function() { const parseWebSocket = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; Client.pushError(parseWebSocket, 1, 'error', true); @@ -37,10 +38,10 @@ describe('Client', function() { it('can add subscription information', function() { const subscription = {}; const fields = ['test']; - const subscriptionInfo = { + const subscriptionInfo = { subscription: subscription, - fields: fields - } + fields: fields, + }; const client = new Client(1, {}); client.addSubscriptionInfo(1, subscriptionInfo); @@ -51,10 +52,10 @@ describe('Client', function() { it('can get subscription information', function() { const subscription = {}; const fields = ['test']; - const subscriptionInfo = { + const subscriptionInfo = { subscription: subscription, - fields: fields - } + fields: fields, + }; const client = new Client(1, {}); client.addSubscriptionInfo(1, subscriptionInfo); const subscriptionInfoAgain = client.getSubscriptionInfo(1); @@ -65,10 +66,10 @@ describe('Client', function() { it('can delete subscription information', function() { const subscription = {}; const fields = ['test']; - const subscriptionInfo = { + const subscriptionInfo = { subscription: subscription, - fields: fields - } + fields: fields, + }; const client = new Client(1, {}); client.addSubscriptionInfo(1, subscriptionInfo); client.deleteSubscriptionInfo(1); @@ -76,10 +77,9 @@ describe('Client', function() { expect(client.subscriptionInfos.size).toBe(0); }); - it('can generate ParseObject JSON with null selected field', function() { const parseObjectJSON = { - key : 'value', + key: 'value', className: 'test', objectId: 'test', updatedAt: '2015-12-07T21:27:13.746Z', @@ -88,12 +88,14 @@ describe('Client', function() { }; const client = new Client(1, {}); - expect(client._toJSONWithFields(parseObjectJSON, null)).toBe(parseObjectJSON); + expect(client._toJSONWithFields(parseObjectJSON, null)).toBe( + parseObjectJSON + ); }); it('can generate ParseObject JSON with undefined selected field', function() { const parseObjectJSON = { - key : 'value', + key: 'value', className: 'test', objectId: 'test', updatedAt: '2015-12-07T21:27:13.746Z', @@ -102,18 +104,20 @@ describe('Client', function() { }; const client = new Client(1, {}); - expect(client._toJSONWithFields(parseObjectJSON, undefined)).toBe(parseObjectJSON); + expect(client._toJSONWithFields(parseObjectJSON, undefined)).toBe( + parseObjectJSON + ); }); it('can generate ParseObject JSON with selected fields', function() { const parseObjectJSON = { - key : 'value', + key: 'value', className: 'test', objectId: 'test', updatedAt: '2015-12-07T21:27:13.746Z', createdAt: '2015-12-07T21:27:13.746Z', ACL: 'test', - test: 'test' + test: 'test', }; const client = new Client(1, {}); @@ -123,22 +127,24 @@ describe('Client', function() { updatedAt: '2015-12-07T21:27:13.746Z', createdAt: '2015-12-07T21:27:13.746Z', ACL: 'test', - test: 'test' + test: 'test', }); }); it('can generate ParseObject JSON with nonexistent selected fields', function() { const parseObjectJSON = { - key : 'value', + key: 'value', className: 'test', objectId: 'test', updatedAt: '2015-12-07T21:27:13.746Z', createdAt: '2015-12-07T21:27:13.746Z', ACL: 'test', - test: 'test' + test: 'test', }; const client = new Client(1, {}); - const limitedParseObject = client._toJSONWithFields(parseObjectJSON, ['name']); + const limitedParseObject = client._toJSONWithFields(parseObjectJSON, [ + 'name', + ]); expect(limitedParseObject).toEqual({ className: 'test', @@ -152,7 +158,7 @@ describe('Client', function() { it('can push connect response', function() { const parseWebSocket = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; const client = new Client(1, parseWebSocket); client.pushConnect(); @@ -165,7 +171,7 @@ describe('Client', function() { it('can push subscribe response', function() { const parseWebSocket = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; const client = new Client(1, parseWebSocket); client.pushSubscribe(2); @@ -179,7 +185,7 @@ describe('Client', function() { it('can push unsubscribe response', function() { const parseWebSocket = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; const client = new Client(1, parseWebSocket); client.pushUnsubscribe(2); @@ -193,16 +199,16 @@ describe('Client', function() { it('can push create response', function() { const parseObjectJSON = { - key : 'value', + key: 'value', className: 'test', objectId: 'test', updatedAt: '2015-12-07T21:27:13.746Z', createdAt: '2015-12-07T21:27:13.746Z', ACL: 'test', - test: 'test' + test: 'test', }; const parseWebSocket = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; const client = new Client(1, parseWebSocket); client.pushCreate(2, parseObjectJSON); @@ -217,16 +223,16 @@ describe('Client', function() { it('can push enter response', function() { const parseObjectJSON = { - key : 'value', + key: 'value', className: 'test', objectId: 'test', updatedAt: '2015-12-07T21:27:13.746Z', createdAt: '2015-12-07T21:27:13.746Z', ACL: 'test', - test: 'test' + test: 'test', }; const parseWebSocket = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; const client = new Client(1, parseWebSocket); client.pushEnter(2, parseObjectJSON); @@ -241,16 +247,16 @@ describe('Client', function() { it('can push update response', function() { const parseObjectJSON = { - key : 'value', + key: 'value', className: 'test', objectId: 'test', updatedAt: '2015-12-07T21:27:13.746Z', createdAt: '2015-12-07T21:27:13.746Z', ACL: 'test', - test: 'test' + test: 'test', }; const parseWebSocket = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; const client = new Client(1, parseWebSocket); client.pushUpdate(2, parseObjectJSON); @@ -265,16 +271,16 @@ describe('Client', function() { it('can push leave response', function() { const parseObjectJSON = { - key : 'value', + key: 'value', className: 'test', objectId: 'test', updatedAt: '2015-12-07T21:27:13.746Z', createdAt: '2015-12-07T21:27:13.746Z', ACL: 'test', - test: 'test' + test: 'test', }; const parseWebSocket = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; const client = new Client(1, parseWebSocket); client.pushLeave(2, parseObjectJSON); diff --git a/spec/ClientSDK.spec.js b/spec/ClientSDK.spec.js index 3825681748..987770833c 100644 --- a/spec/ClientSDK.spec.js +++ b/spec/ClientSDK.spec.js @@ -5,37 +5,45 @@ describe('ClientSDK', () => { const clientSDKFromVersion = ClientSDK.fromString; expect(clientSDKFromVersion('i1.1.1')).toEqual({ sdk: 'i', - version: '1.1.1' + version: '1.1.1', }); expect(clientSDKFromVersion('i1')).toEqual({ sdk: 'i', - version: '1' + version: '1', }); expect(clientSDKFromVersion('apple-tv1.13.0')).toEqual({ sdk: 'apple-tv', - version: '1.13.0' + version: '1.13.0', }); expect(clientSDKFromVersion('js1.9.0')).toEqual({ sdk: 'js', - version: '1.9.0' + version: '1.9.0', }); }); it('should properly sastisfy', () => { - expect(ClientSDK.compatible({ - js: '>=1.9.0' - })("js1.9.0")).toBe(true); + expect( + ClientSDK.compatible({ + js: '>=1.9.0', + })('js1.9.0') + ).toBe(true); - expect(ClientSDK.compatible({ - js: '>=1.9.0' - })("js2.0.0")).toBe(true); + expect( + ClientSDK.compatible({ + js: '>=1.9.0', + })('js2.0.0') + ).toBe(true); - expect(ClientSDK.compatible({ - js: '>=1.9.0' - })("js1.8.0")).toBe(false); + expect( + ClientSDK.compatible({ + js: '>=1.9.0', + })('js1.8.0') + ).toBe(false); - expect(ClientSDK.compatible({ - js: '>=1.9.0' - })(undefined)).toBe(true); - }) -}) + expect( + ClientSDK.compatible({ + js: '>=1.9.0', + })(undefined) + ).toBe(true); + }); +}); diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 7d2184cfba..dc3c9e7e7c 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -1,27 +1,34 @@ -"use strict" -const Parse = require("parse/node"); +'use strict'; +const Parse = require('parse/node'); const rp = require('request-promise'); -const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter').InMemoryCacheAdapter; +const InMemoryCacheAdapter = require('../lib/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', {}).then(result => { - expect(result).toEqual('It is possible to define cloud code in a file.'); - done(); - }); + reconfigureServer({ + cloud: __dirname + '/cloud/cloudCodeRelativeFile.js', + }).then(() => { + Parse.Cloud.run('cloudCodeInFile', {}).then(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(() => { + reconfigureServer({ cloud: './spec/cloud/cloudCodeAbsoluteFile.js' }).then( + () => { Parse.Cloud.run('cloudCodeInFile', {}).then(result => { - expect(result).toEqual('It is possible to define cloud code in a file.'); + expect(result).toEqual( + 'It is possible to define cloud code in a file.' + ); done(); }); - }) + } + ); }); it('can create functions', done => { @@ -36,11 +43,10 @@ describe('Cloud Code', () => { }); it('is cleared cleared after the previous test', done => { - Parse.Cloud.run('hello', {}) - .catch(error => { - expect(error.code).toEqual(141); - done(); - }); + Parse.Cloud.run('hello', {}).catch(error => { + expect(error.code).toEqual(141); + done(); + }); }); it('basic beforeSave rejection', function(done) { @@ -50,15 +56,18 @@ describe('Cloud Code', () => { const obj = new Parse.Object('BeforeSaveFail'); obj.set('foo', 'bar'); - obj.save().then(() => { - fail('Should not have been able to save BeforeSaveFailure class.'); - done(); - }, () => { - done(); - }) + obj.save().then( + () => { + fail('Should not have been able to save BeforeSaveFailure class.'); + done(); + }, + () => { + done(); + } + ); }); - it('returns an error', (done) => { + it('returns an error', done => { Parse.Cloud.define('cloudCodeWithError', () => { /* eslint-disable no-undef */ foo.bar(); @@ -66,52 +75,63 @@ describe('Cloud Code', () => { return 'I better throw an error.'; }); - Parse.Cloud.run('cloudCodeWithError') - .then( - () => done.fail('should not succeed'), - e => { - expect(e).toEqual(new Parse.Error(141, 'foo is not defined')); - done(); - }); + Parse.Cloud.run('cloudCodeWithError').then( + () => done.fail('should not succeed'), + e => { + expect(e).toEqual(new Parse.Error(141, 'foo is not defined')); + done(); + } + ); }); it('beforeSave rejection with custom error code', function(done) { - Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function () { + Parse.Cloud.beforeSave('BeforeSaveFailWithErrorCode', function() { throw new Parse.Error(999, 'Nope'); }); const 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(); - }); + 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 () { + Parse.Cloud.beforeSave('BeforeSaveFailWithPromise', function() { const query = new Parse.Query('Yolo'); - return query.find().then(() => { - throw 'Nope'; - }, () => { - return Promise.response(); - }); + return query.find().then( + () => { + throw 'Nope'; + }, + () => { + return Promise.response(); + } + ); }); const 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(); - }) + 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) { @@ -121,22 +141,28 @@ describe('Cloud Code', () => { const obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bar'); - obj.save().then(function() { - const query = new Parse.Query('BeforeSaveChanged'); - query.get(obj.id).then(function(objAgain) { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }, function(error) { + obj.save().then( + function() { + const 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(); - }); - }, function(error) { - fail(error); - done(); - }); + } + ); }); - it('test beforeSave returns value on create and update', (done) => { + it('test beforeSave returns value on create and update', done => { Parse.Cloud.beforeSave('BeforeSaveChanged', function(req) { req.object.set('foo', 'baz'); }); @@ -149,8 +175,8 @@ describe('Cloud Code', () => { return obj.save().then(() => { expect(obj.get('foo')).toEqual('baz'); done(); - }) - }) + }); + }); }); it('test afterSave ran and created an object', function(done) { @@ -166,25 +192,27 @@ describe('Cloud Code', () => { setTimeout(function() { const 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(); - }); + query.find().then( + function(results) { + expect(results.length).toEqual(1); + done(); + }, + function(error) { + fail(error); + done(); + } + ); }, 500); }); it('test afterSave ran on created object and returned a promise', function(done) { Parse.Cloud.afterSave('AfterSaveTest2', function(req) { const obj = req.object; - if(!obj.existed()) - { - return new Promise((resolve) => { - setTimeout(function(){ + if (!obj.existed()) { + return new Promise(resolve => { + setTimeout(function() { obj.set('proof', obj.id); - obj.save().then(function(){ + obj.save().then(function() { resolve(); }); }, 1000); @@ -193,19 +221,21 @@ describe('Cloud Code', () => { }); const obj = new Parse.Object('AfterSaveTest2'); - obj.save().then(function(){ + obj.save().then(function() { const query = new Parse.Query('AfterSaveTest2'); query.equalTo('proof', obj.id); - query.find().then(function(results) { - expect(results.length).toEqual(1); - const savedObject = results[0]; - expect(savedObject.get('proof')).toEqual(obj.id); - done(); - }, - function(error) { - fail(error); - done(); - }); + query.find().then( + function(results) { + expect(results.length).toEqual(1); + const savedObject = results[0]; + expect(savedObject.get('proof')).toEqual(obj.id); + done(); + }, + function(error) { + fail(error); + done(); + } + ); }); }); @@ -213,12 +243,11 @@ describe('Cloud Code', () => { xit('test afterSave ignoring promise, object not found', function(done) { Parse.Cloud.afterSave('AfterSaveTest2', function(req) { const obj = req.object; - if(!obj.existed()) - { - return new Promise((resolve) => { - setTimeout(function(){ + if (!obj.existed()) { + return new Promise(resolve => { + setTimeout(function() { obj.set('proof', obj.id); - obj.save().then(function(){ + obj.save().then(function() { resolve(); }); }, 1000); @@ -227,45 +256,50 @@ describe('Cloud Code', () => { }); const obj = new Parse.Object('AfterSaveTest2'); - obj.save().then(function(){ + obj.save().then(function() { done(); - }) + }); const query = new Parse.Query('AfterSaveTest2'); query.equalTo('proof', obj.id); - query.find().then(function(results) { - expect(results.length).toEqual(0); - }, - function(error) { - fail(error); - }); + query.find().then( + function(results) { + expect(results.length).toEqual(0); + }, + function(error) { + fail(error); + } + ); }); it('test afterSave rejecting promise', function(done) { Parse.Cloud.afterSave('AfterSaveTest2', function() { return new Promise((resolve, reject) => { - setTimeout(function(){ - reject("THIS SHOULD BE IGNORED"); + setTimeout(function() { + reject('THIS SHOULD BE IGNORED'); }, 1000); }); }); const obj = new Parse.Object('AfterSaveTest2'); - obj.save().then(function(){ - done(); - }, function(error){ - fail(error); - done(); - }) + obj.save().then( + function() { + done(); + }, + function(error) { + fail(error); + done(); + } + ); }); it('test afterDelete returning promise, object is deleted when destroy resolves', function(done) { Parse.Cloud.afterDelete('AfterDeleteTest2', function(req) { - return new Promise((resolve) => { - setTimeout(function(){ + return new Promise(resolve => { + setTimeout(function() { const obj = new Parse.Object('AfterDeleteTestProof'); obj.set('proof', req.object.id); - obj.save().then(function(){ + obj.save().then(function() { resolve(); }); }, 1000); @@ -275,11 +309,11 @@ describe('Cloud Code', () => { const errorHandler = function(error) { fail(error); done(); - } + }; const obj = new Parse.Object('AfterDeleteTest2'); - obj.save().then(function(){ - obj.destroy().then(function(){ + obj.save().then(function() { + obj.destroy().then(function() { const query = new Parse.Query('AfterDeleteTestProof'); query.equalTo('proof', obj.id); query.find().then(function(results) { @@ -288,17 +322,17 @@ describe('Cloud Code', () => { expect(deletedObject.get('proof')).toEqual(obj.id); done(); }, errorHandler); - }, errorHandler) + }, errorHandler); }, errorHandler); }); it('test afterDelete ignoring promise, object is not yet deleted', function(done) { Parse.Cloud.afterDelete('AfterDeleteTest2', function(req) { - return new Promise((resolve) => { - setTimeout(function(){ + return new Promise(resolve => { + setTimeout(function() { const obj = new Parse.Object('AfterDeleteTestProof'); obj.set('proof', req.object.id); - obj.save().then(function(){ + obj.save().then(function() { resolve(); }); }, 1000); @@ -308,13 +342,13 @@ describe('Cloud Code', () => { const errorHandler = function(error) { fail(error); done(); - } + }; const obj = new Parse.Object('AfterDeleteTest2'); - obj.save().then(function(){ - obj.destroy().then(function(){ + obj.save().then(function() { + obj.destroy().then(function() { done(); - }) + }); const query = new Parse.Query('AfterDeleteTestProof'); query.equalTo('proof', obj.id); @@ -331,19 +365,25 @@ describe('Cloud Code', () => { const obj = new Parse.Object('BeforeSaveChanged'); obj.set('foo', 'bar'); - obj.save().then(function() { - obj.set('foo', 'bar'); - return obj.save(); - }).then(function() { - const 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(); - }); + obj + .save() + .then(function() { + obj.set('foo', 'bar'); + return obj.save(); + }) + .then( + function() { + const 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) { @@ -354,33 +394,45 @@ describe('Cloud Code', () => { const obj = new Parse.Object('BeforeDeleteFail'); let 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'); - - const 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); - }); + 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'); + + const 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 () { + Parse.Cloud.beforeSave('BeforeDeleteFailWithPromise', function() { const query = new Parse.Query('Yolo'); return query.find().then(() => { throw 'Nope'; @@ -389,15 +441,18 @@ describe('Cloud Code', () => { const 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'); + 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(); - }) + done(); + } + ); }); it('test afterDelete ran and created an object', function(done) { @@ -415,13 +470,16 @@ describe('Cloud Code', () => { setTimeout(function() { const 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(); - }); + query.find().then( + function(results) { + expect(results.length).toEqual(1); + done(); + }, + function(error) { + fail(error); + done(); + } + ); }, 500); }); @@ -437,23 +495,25 @@ describe('Cloud Code', () => { __type: 'Object', className: 'Bar', objectId: '234', - x: 3 - } + x: 3, + }, }, - array: [{ - __type: 'Object', - className: 'Bar', - objectId: '345', - x: 2 - }], - a: 2 + array: [ + { + __type: 'Object', + className: 'Bar', + objectId: '345', + x: 2, + }, + ], + a: 2, }; }); - Parse.Cloud.run('foo').then((result) => { + Parse.Cloud.run('foo').then(result => { expect(result.object instanceof Parse.Object).toBeTruthy(); if (!result.object) { - fail("Unable to run foo"); + fail('Unable to run foo'); done(); return; } @@ -478,17 +538,28 @@ describe('Cloud Code', () => { 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); + 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 + ); // Regression for #2294 expect(req.params.file instanceof Parse.File).toBe(true); expect(req.params.file.url()).toEqual('https://some.url'); // Regression for #2204 expect(req.params.array).toEqual(['a', 'b', 'c']); expect(Array.isArray(req.params.array)).toBe(true); - expect(req.params.arrayOfArray).toEqual([['a', 'b', 'c'], ['d', 'e','f']]); + expect(req.params.arrayOfArray).toEqual([ + ['a', 'b', 'c'], + ['d', 'e', 'f'], + ]); expect(Array.isArray(req.params.arrayOfArray)).toBe(true); expect(Array.isArray(req.params.arrayOfArray[0])).toBe(true); expect(Array.isArray(req.params.arrayOfArray[1])).toBe(true); @@ -496,48 +567,48 @@ describe('Cloud Code', () => { }); const params = { - 'date': { - '__type': 'Date', - 'iso': '2016-05-22T09:00:00.000Z' + date: { + __type: 'Date', + iso: '2016-05-22T09:00:00.000Z', }, - 'dateList': [ + dateList: [ { - '__type': 'Date', - 'iso': '2016-05-22T09:00:00.000Z' - } + __type: 'Date', + iso: '2016-05-22T09:00:00.000Z', + }, ], - 'lol': 'hello', - 'complexStructure': { - 'date': [ + lol: 'hello', + complexStructure: { + date: [ { - '__type': 'Date', - 'iso': '2016-05-22T09:00:00.000Z' - } + __type: 'Date', + iso: '2016-05-22T09:00:00.000Z', + }, ], - 'deepDate': { - 'date': [ + deepDate: { + date: [ { - '__type': 'Date', - 'iso': '2016-05-22T09:00:00.000Z' - } - ] + __type: 'Date', + iso: '2016-05-22T09:00:00.000Z', + }, + ], }, - 'deepDate2': [ + deepDate2: [ { - 'date': { - '__type': 'Date', - 'iso': '2016-05-22T09:00:00.000Z' - } - } - ] + date: { + __type: 'Date', + iso: '2016-05-22T09:00:00.000Z', + }, + }, + ], }, - 'file': Parse.File.fromJSON({ + file: Parse.File.fromJSON({ __type: 'File', name: 'name', - url: 'https://some.url' + url: 'https://some.url', }), - 'array': ['a', 'b', 'c'], - 'arrayOfArray': [['a', 'b', 'c'], ['d', 'e', 'f']] + array: ['a', 'b', 'c'], + arrayOfArray: [['a', 'b', 'c'], ['d', 'e', 'f']], }; Parse.Cloud.run('params', params).then(() => { done(); @@ -549,11 +620,11 @@ describe('Cloud Code', () => { return { applicationId: Parse.applicationId, masterKey: Parse.masterKey, - javascriptKey: Parse.javascriptKey + javascriptKey: Parse.javascriptKey, }; }); - Parse.Cloud.run('echoKeys').then((result) => { + Parse.Cloud.run('echoKeys').then(result => { expect(result.applicationId).toEqual(Parse.applicationId); expect(result.masterKey).toEqual(Parse.masterKey); expect(result.javascriptKey).toEqual(Parse.javascriptKey); @@ -570,10 +641,10 @@ describe('Cloud Code', () => { const obj = new Parse.Object('BeforeSaveChanged'); return obj.save().then(() => { return obj; - }) - }) + }); + }); - Parse.Cloud.run('createBeforeSaveChangedObject').then((res) => { + Parse.Cloud.run('createBeforeSaveChangedObject').then(res => { expect(res.get('foo')).toEqual('baz'); done(); }); @@ -582,7 +653,7 @@ describe('Cloud Code', () => { it('dirtyKeys are set on update', done => { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.beforeSave('GameScore', (req) => { + Parse.Cloud.beforeSave('GameScore', req => { const object = req.object; expect(object instanceof Parse.Object).toBeTruthy(); expect(object.get('fooAgain')).toEqual('barAgain'); @@ -603,18 +674,24 @@ describe('Cloud Code', () => { const 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(); - }); + 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) { @@ -624,12 +701,15 @@ describe('Cloud Code', () => { const obj = new Parse.Object('BeforeSaveUnchanged'); obj.set('foo', 'bar'); - obj.save().then(function() { - done(); - }, function(error) { - fail(error); - done(); - }); + obj.save().then( + function() { + done(); + }, + function(error) { + fail(error); + done(); + } + ); }); it('test beforeDelete success', function(done) { @@ -639,18 +719,24 @@ describe('Cloud Code', () => { const obj = new Parse.Object('BeforeDeleteTest'); obj.set('foo', 'bar'); - obj.save().then(function() { - return obj.destroy(); - }).then(function() { - const objAgain = new Parse.Object('BeforeDeleteTest', obj.id); - return objAgain.fetch().then(fail, done); - }, function(error) { - fail(error); - done(); - }); + obj + .save() + .then(function() { + return obj.destroy(); + }) + .then( + function() { + const objAgain = new Parse.Object('BeforeDeleteTest', obj.id); + return objAgain.fetch().then(fail, done); + }, + function(error) { + fail(error); + done(); + } + ); }); - it('test save triggers get user', async (done) => { + it('test save triggers get user', async done => { Parse.Cloud.beforeSave('SaveTriggerUser', function(req) { if (req.user && req.user.id) { return; @@ -666,71 +752,90 @@ describe('Cloud Code', () => { }); const user = new Parse.User(); - user.set("password", "asdf"); - user.set("email", "asdf@example.com"); - user.set("username", "zxcv"); + user.set('password', 'asdf'); + user.set('email', 'asdf@example.com'); + user.set('username', 'zxcv'); await user.signUp(); const obj = new Parse.Object('SaveTriggerUser'); - obj.save().then(function() { - done(); - }, function(error) { - fail(error); - done(); - }); + obj.save().then( + function() { + done(); + }, + function(error) { + fail(error); + done(); + } + ); }); - it('beforeSave change propagates through the save response', (done) => { + it('beforeSave change propagates through the save response', done => { Parse.Cloud.beforeSave('ChangingObject', function(request) { request.object.set('foo', 'baz'); }); const obj = new Parse.Object('ChangingObject'); - obj.save({ foo: 'bar' }).then((objAgain) => { - expect(objAgain.get('foo')).toEqual('baz'); - done(); - }, () => { - fail('Should not have failed to save.'); - done(); - }); + obj.save({ foo: 'bar' }).then( + objAgain => { + expect(objAgain.get('foo')).toEqual('baz'); + done(); + }, + () => { + fail('Should not have failed to save.'); + done(); + } + ); }); - it('beforeSave change propagates through the afterSave #1931', (done) => { + it('beforeSave change propagates through the afterSave #1931', done => { Parse.Cloud.beforeSave('ChangingObject', function(request) { request.object.unset('file'); request.object.unset('date'); }); Parse.Cloud.afterSave('ChangingObject', function(request) { - expect(request.object.has("file")).toBe(false); - expect(request.object.has("date")).toBe(false); + expect(request.object.has('file')).toBe(false); + expect(request.object.has('date')).toBe(false); expect(request.object.get('file')).toBeUndefined(); return Promise.resolve(); }); - const file = new Parse.File("yolo.txt", [1,2,3], "text/plain"); - file.save().then(() => { - const obj = new Parse.Object('ChangingObject'); - return obj.save({ file, date: new Date() }) - }).then(() => { - done(); - }, () => { - fail(); - done(); - }) + const file = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain'); + file + .save() + .then(() => { + const obj = new Parse.Object('ChangingObject'); + return obj.save({ file, date: new Date() }); + }) + .then( + () => { + done(); + }, + () => { + fail(); + done(); + } + ); }); - it('test cloud function parameter validation success', (done) => { + it('test cloud function parameter validation success', done => { // Register a function with validation - Parse.Cloud.define('functionWithParameterValidation', () => { - return 'works'; - }, (request) => { - return request.params.success === 100; - }); + Parse.Cloud.define( + 'functionWithParameterValidation', + () => { + return 'works'; + }, + request => { + return request.params.success === 100; + } + ); - Parse.Cloud.run('functionWithParameterValidation', {"success":100}).then(() => { - done(); - }, () => { - fail('Validation should not have failed.'); - done(); - }); + Parse.Cloud.run('functionWithParameterValidation', { success: 100 }).then( + () => { + done(); + }, + () => { + 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 => { @@ -747,7 +852,7 @@ describe('Cloud Code', () => { .then(result => { expect(result).toEqual('AAA'); Parse.User.current().set('data', 'BBB'); - return Parse.User.current().save(null, {useMasterKey: true}); + return Parse.User.current().save(null, { useMasterKey: true }); }) .then(() => Parse.Cloud.run('testQuery')) .then(result => { @@ -763,7 +868,7 @@ describe('Cloud Code', () => { const cacheAdapter = new InMemoryCacheAdapter({ ttl: 100000000 }); reconfigureServer({ cacheAdapter }) .then(() => { - Parse.Cloud.define('checkStaleUser', (request) => { + Parse.Cloud.define('checkStaleUser', request => { return request.user.get('data'); }); @@ -782,43 +887,52 @@ describe('Cloud Code', () => { '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(() => Promise.all([cacheAdapter.get('test:user:' + session1), cacheAdapter.get('test:user:' + session2)])) + //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(() => + 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() + 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(() => + 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(); @@ -829,64 +943,76 @@ describe('Cloud Code', () => { it('trivial beforeSave should not affect fetched pointers (regression test for #1238)', done => { Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => {}); - const TestObject = Parse.Object.extend("TestObject"); - const NoBeforeSaveObject = Parse.Object.extend("NoBeforeSave"); - const BeforeSaveObject = Parse.Object.extend("BeforeSaveUnchanged"); + const TestObject = Parse.Object.extend('TestObject'); + const NoBeforeSaveObject = Parse.Object.extend('NoBeforeSave'); + const BeforeSaveObject = Parse.Object.extend('BeforeSaveUnchanged'); const aTestObject = new TestObject(); - aTestObject.set("foo", "bar"); - aTestObject.save() + aTestObject.set('foo', 'bar'); + aTestObject + .save() .then(aTestObject => { const aNoBeforeSaveObj = new NoBeforeSaveObject(); - aNoBeforeSaveObj.set("aTestObject", aTestObject); - expect(aNoBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar"); + 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"); + expect(aNoBeforeSaveObj.get('aTestObject').get('foo')).toEqual('bar'); const aBeforeSaveObj = new BeforeSaveObject(); - aBeforeSaveObj.set("aTestObject", aTestObject); - expect(aBeforeSaveObj.get("aTestObject").get("foo")).toEqual("bar"); + 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"); + expect(aBeforeSaveObj.get('aTestObject').get('foo')).toEqual('bar'); done(); }); }); it('beforeSave should not affect fetched pointers', done => { - Parse.Cloud.beforeSave('BeforeSaveUnchanged',() => {}); + Parse.Cloud.beforeSave('BeforeSaveUnchanged', () => {}); Parse.Cloud.beforeSave('BeforeSaveChanged', function(req) { req.object.set('foo', 'baz'); }); - const TestObject = Parse.Object.extend("TestObject"); - const BeforeSaveUnchangedObject = Parse.Object.extend("BeforeSaveUnchanged"); - const BeforeSaveChangedObject = Parse.Object.extend("BeforeSaveChanged"); + const TestObject = Parse.Object.extend('TestObject'); + const BeforeSaveUnchangedObject = Parse.Object.extend( + 'BeforeSaveUnchanged' + ); + const BeforeSaveChangedObject = Parse.Object.extend('BeforeSaveChanged'); const aTestObject = new TestObject(); - aTestObject.set("foo", "bar"); - aTestObject.save() + aTestObject.set('foo', 'bar'); + aTestObject + .save() .then(aTestObject => { const aBeforeSaveUnchangedObject = new BeforeSaveUnchangedObject(); - aBeforeSaveUnchangedObject.set("aTestObject", aTestObject); - expect(aBeforeSaveUnchangedObject.get("aTestObject").get("foo")).toEqual("bar"); + aBeforeSaveUnchangedObject.set('aTestObject', aTestObject); + expect( + aBeforeSaveUnchangedObject.get('aTestObject').get('foo') + ).toEqual('bar'); return aBeforeSaveUnchangedObject.save(); }) .then(aBeforeSaveUnchangedObject => { - expect(aBeforeSaveUnchangedObject.get("aTestObject").get("foo")).toEqual("bar"); + expect( + aBeforeSaveUnchangedObject.get('aTestObject').get('foo') + ).toEqual('bar'); const aBeforeSaveChangedObject = new BeforeSaveChangedObject(); - aBeforeSaveChangedObject.set("aTestObject", aTestObject); - expect(aBeforeSaveChangedObject.get("aTestObject").get("foo")).toEqual("bar"); + aBeforeSaveChangedObject.set('aTestObject', aTestObject); + expect(aBeforeSaveChangedObject.get('aTestObject').get('foo')).toEqual( + 'bar' + ); return aBeforeSaveChangedObject.save(); }) .then(aBeforeSaveChangedObject => { - expect(aBeforeSaveChangedObject.get("aTestObject").get("foo")).toEqual("bar"); - expect(aBeforeSaveChangedObject.get("foo")).toEqual("baz"); + expect(aBeforeSaveChangedObject.get('aTestObject').get('foo')).toEqual( + 'bar' + ); + expect(aBeforeSaveChangedObject.get('foo')).toEqual('baz'); done(); }); }); @@ -896,16 +1022,17 @@ describe('Cloud Code', () => { const NoBeforeSaveObject = Parse.Object.extend('NoBeforeSave'); const BeforeSaveObject = Parse.Object.extend('BeforeSaveChanged'); - Parse.Cloud.beforeSave('BeforeSaveChanged', (req) => { + Parse.Cloud.beforeSave('BeforeSaveChanged', req => { const object = req.object; object.set('before', 'save'); }); Parse.Cloud.define('removeme', (req, res) => { const testObject = new TestObject(); - return testObject.save() + return testObject + .save() .then(testObject => { - const object = new NoBeforeSaveObject({remove: testObject}); + const object = new NoBeforeSaveObject({ remove: testObject }); return object.save(); }) .then(object => { @@ -917,9 +1044,10 @@ describe('Cloud Code', () => { Parse.Cloud.define('removeme2', (req, res) => { const testObject = new TestObject(); - return testObject.save() + return testObject + .save() .then(testObject => { - const object = new BeforeSaveObject({remove: testObject}); + const object = new BeforeSaveObject({ remove: testObject }); return object.save(); }) .then(object => { @@ -939,7 +1067,8 @@ describe('Cloud Code', () => { expect(aBeforeSaveObj.get('before')).toEqual('save'); expect(aBeforeSaveObj.get('remove')).toEqual(undefined); done(); - }).catch((err) => { + }) + .catch(err => { jfail(err); done(); }); @@ -949,39 +1078,46 @@ describe('Cloud Code', () => { TODO: fix for Postgres trying to delete a field that doesn't exists doesn't play nice */ - it_exclude_dbs(['postgres'])('should fully delete objects when using `unset` and `set` with beforeSave (regression test for #1840)', done => { - const TestObject = Parse.Object.extend('TestObject'); - const BeforeSaveObject = Parse.Object.extend('BeforeSaveChanged'); - - Parse.Cloud.beforeSave('BeforeSaveChanged', (req) => { - const object = req.object; - object.set('before', 'save'); - object.unset('remove'); - }); - - let object; - const testObject = new TestObject({key: 'value'}); - testObject.save().then(() => { - object = new BeforeSaveObject(); - return object.save().then(() => { - object.set({remove:testObject}) - return object.save(); + it_exclude_dbs(['postgres'])( + 'should fully delete objects when using `unset` and `set` with beforeSave (regression test for #1840)', + done => { + const TestObject = Parse.Object.extend('TestObject'); + const BeforeSaveObject = Parse.Object.extend('BeforeSaveChanged'); + + Parse.Cloud.beforeSave('BeforeSaveChanged', req => { + const object = req.object; + object.set('before', 'save'); + object.unset('remove'); }); - }).then((objectAgain) => { - expect(objectAgain.get('remove')).toBeUndefined(); - expect(object.get('remove')).toBeUndefined(); - done(); - }).catch((err) => { - jfail(err); - done(); - }); - }); + + let object; + const testObject = new TestObject({ key: 'value' }); + testObject + .save() + .then(() => { + object = new BeforeSaveObject(); + return object.save().then(() => { + object.set({ remove: testObject }); + return object.save(); + }); + }) + .then(objectAgain => { + expect(objectAgain.get('remove')).toBeUndefined(); + expect(object.get('remove')).toBeUndefined(); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); + } + ); it('should not include relation op (regression test for #1606)', done => { const TestObject = Parse.Object.extend('TestObject'); const BeforeSaveObject = Parse.Object.extend('BeforeSaveChanged'); let testObj; - Parse.Cloud.beforeSave('BeforeSaveChanged', (req) => { + Parse.Cloud.beforeSave('BeforeSaveChanged', req => { const object = req.object; object.set('before', 'save'); testObj = new TestObject(); @@ -991,31 +1127,36 @@ describe('Cloud Code', () => { }); const object = new BeforeSaveObject(); - object.save().then((objectAgain) => { - // Originally it would throw as it would be a non-relation - expect(() => { objectAgain.relation('testsRelation') }).not.toThrow(); - done(); - }).catch((err) => { - jfail(err); - done(); - }) + object + .save() + .then(objectAgain => { + // Originally it would throw as it would be a non-relation + expect(() => { + objectAgain.relation('testsRelation'); + }).not.toThrow(); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); /** * Checks that incrementing a value to a zero in a beforeSave hook * does not result in that key being omitted from the response. */ - it('before save increment does not return undefined', (done) => { - Parse.Cloud.define("cloudIncrementClassFunction", function (req) { - const CloudIncrementClass = Parse.Object.extend("CloudIncrementClass"); + it('before save increment does not return undefined', done => { + Parse.Cloud.define('cloudIncrementClassFunction', function(req) { + const CloudIncrementClass = Parse.Object.extend('CloudIncrementClass'); const obj = new CloudIncrementClass(); obj.id = req.params.objectId; return obj.save(); }); - Parse.Cloud.beforeSave("CloudIncrementClass", function (req) { + Parse.Cloud.beforeSave('CloudIncrementClass', function(req) { const obj = req.object; - if(!req.master) { + if (!req.master) { obj.increment('points', -10); obj.increment('num', -9); } @@ -1025,24 +1166,24 @@ describe('Cloud Code', () => { const obj = new CloudIncrementClass(); obj.set('points', 10); obj.set('num', 10); - obj.save(null, {useMasterKey: true}) - .then(function() { - Parse.Cloud.run('cloudIncrementClassFunction', { objectId: obj.id }) - .then(function(savedObj) { - expect(savedObj.get('num')).toEqual(1); - expect(savedObj.get('points')).toEqual(0); - done(); - }); - }); + obj.save(null, { useMasterKey: true }).then(function() { + Parse.Cloud.run('cloudIncrementClassFunction', { objectId: obj.id }).then( + function(savedObj) { + expect(savedObj.get('num')).toEqual(1); + expect(savedObj.get('points')).toEqual(0); + done(); + } + ); + }); }); /** * Verifies that an afterSave hook throwing an exception * will not prevent a successful save response from being returned */ - it('should succeed on afterSave exception', (done) => { - Parse.Cloud.afterSave("AfterSaveTestClass", function () { - throw "Exception"; + it('should succeed on afterSave exception', done => { + Parse.Cloud.afterSave('AfterSaveTestClass', function() { + throw 'Exception'; }); const AfterSaveTestClass = Parse.Object.extend('AfterSaveTestClass'); const obj = new AfterSaveTestClass(); @@ -1050,7 +1191,7 @@ describe('Cloud Code', () => { }); describe('cloud jobs', () => { - it('should define a job', (done) => { + it('should define a job', done => { expect(() => { Parse.Cloud.job('myJob', () => {}); }).not.toThrow(); @@ -1061,15 +1202,18 @@ describe('Cloud Code', () => { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': Parse.masterKey, }, - }).then(() => { - done(); - }, (err) => { - fail(err); - done(); - }); + }).then( + () => { + done(); + }, + err => { + fail(err); + done(); + } + ); }); - it('should not run without master key', (done) => { + it('should not run without master key', done => { expect(() => { Parse.Cloud.job('myJob', () => {}); }).not.toThrow(); @@ -1080,16 +1224,19 @@ describe('Cloud Code', () => { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-REST-API-Key': 'rest', }, - }).then(() => { - fail('Expected to be unauthorized'); - done(); - }, (err) => { - expect(err.statusCode).toBe(403); - done(); - }); + }).then( + () => { + fail('Expected to be unauthorized'); + done(); + }, + err => { + expect(err.statusCode).toBe(403); + done(); + } + ); }); - it('should run with master key', (done) => { + it('should run with master key', done => { expect(() => { Parse.Cloud.job('myJob', (req, res) => { expect(req.functionName).toBeUndefined(); @@ -1107,14 +1254,16 @@ describe('Cloud Code', () => { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': Parse.masterKey, }, - }).then(() => { - }, (err) => { - fail(err); - done(); - }); + }).then( + () => {}, + err => { + fail(err); + done(); + } + ); }); - it('should run with master key basic auth', (done) => { + it('should run with master key basic auth', done => { expect(() => { Parse.Cloud.job('myJob', (req, res) => { expect(req.functionName).toBeUndefined(); @@ -1127,34 +1276,44 @@ describe('Cloud Code', () => { }).not.toThrow(); rp.post({ - url: `http://${Parse.applicationId}:${Parse.masterKey}@localhost:8378/1/jobs/myJob`, - }).then(() => { - }, (err) => { - fail(err); - done(); - }); + url: `http://${Parse.applicationId}:${ + Parse.masterKey + }@localhost:8378/1/jobs/myJob`, + }).then( + () => {}, + err => { + fail(err); + done(); + } + ); }); - it('should set the message / success on the job', (done) => { - Parse.Cloud.job('myJob', (req) => { + it('should set the message / success on the job', done => { + Parse.Cloud.job('myJob', req => { req.message('hello'); - const promise = req.message().then(() => { - return getJobStatus(req.jobId); - }).then((jobStatus) => { - expect(jobStatus.get('message')).toEqual('hello'); - expect(jobStatus.get('status')).toEqual('running'); - }); - promise.then(() => { - return getJobStatus(req.jobId); - }).then((jobStatus) => { - expect(jobStatus.get('message')).toEqual('hello'); - expect(jobStatus.get('status')).toEqual('succeeded'); - done(); - }).catch(err => { - console.error(err); - jfail(err); - done(); - }); + const promise = req + .message() + .then(() => { + return getJobStatus(req.jobId); + }) + .then(jobStatus => { + expect(jobStatus.get('message')).toEqual('hello'); + expect(jobStatus.get('status')).toEqual('running'); + }); + promise + .then(() => { + return getJobStatus(req.jobId); + }) + .then(jobStatus => { + expect(jobStatus.get('message')).toEqual('hello'); + expect(jobStatus.get('status')).toEqual('succeeded'); + done(); + }) + .catch(err => { + console.error(err); + jfail(err); + done(); + }); return promise; }); @@ -1164,26 +1323,31 @@ describe('Cloud Code', () => { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': Parse.masterKey, }, - }).then(() => { - }, (err) => { - fail(err); - done(); - }); + }).then( + () => {}, + err => { + fail(err); + done(); + } + ); }); - it('should set the failure on the job', (done) => { - Parse.Cloud.job('myJob', (req) => { + it('should set the failure on the job', done => { + Parse.Cloud.job('myJob', req => { const promise = Promise.reject('Something went wrong'); - new Promise((resolve) => setTimeout(resolve, 200)).then(() => { - return getJobStatus(req.jobId); - }).then((jobStatus) => { - expect(jobStatus.get('message')).toEqual('Something went wrong'); - expect(jobStatus.get('status')).toEqual('failed'); - done(); - }).catch(err => { - jfail(err); - done(); - }); + new Promise(resolve => setTimeout(resolve, 200)) + .then(() => { + return getJobStatus(req.jobId); + }) + .then(jobStatus => { + expect(jobStatus.get('message')).toEqual('Something went wrong'); + expect(jobStatus.get('status')).toEqual('failed'); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); return promise; }); @@ -1193,25 +1357,27 @@ describe('Cloud Code', () => { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Master-Key': Parse.masterKey, }, - }).then(() => { - }, (err) => { - fail(err); - done(); - }); + }).then( + () => {}, + err => { + fail(err); + done(); + } + ); }); function getJobStatus(jobId) { const q = new Parse.Query('_JobStatus'); - return q.get(jobId, {useMasterKey: true}); + return q.get(jobId, { useMasterKey: true }); } }); }); describe('cloud functions', () => { - it('Should have request ip', (done) => { - Parse.Cloud.define('myFunction', (req) => { + it('Should have request ip', done => { + Parse.Cloud.define('myFunction', req => { expect(req.ip).toBeDefined(); - return "success"; + return 'success'; }); Parse.Cloud.run('myFunction', {}).then(() => done()); @@ -1219,8 +1385,8 @@ describe('cloud functions', () => { }); describe('beforeSave hooks', () => { - it('should have request headers', (done) => { - Parse.Cloud.beforeSave('MyObject', (req) => { + it('should have request headers', done => { + Parse.Cloud.beforeSave('MyObject', req => { expect(req.headers).toBeDefined(); }); @@ -1229,8 +1395,8 @@ describe('beforeSave hooks', () => { myObject.save().then(() => done()); }); - it('should have request ip', (done) => { - Parse.Cloud.beforeSave('MyObject', (req) => { + it('should have request ip', done => { + Parse.Cloud.beforeSave('MyObject', req => { expect(req.ip).toBeDefined(); }); @@ -1241,19 +1407,18 @@ describe('beforeSave hooks', () => { }); describe('afterSave hooks', () => { - it('should have request headers', (done) => { - Parse.Cloud.afterSave('MyObject', (req) => { + it('should have request headers', done => { + Parse.Cloud.afterSave('MyObject', req => { expect(req.headers).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); const myObject = new MyObject(); - myObject.save() - .then(() => done()); + myObject.save().then(() => done()); }); - it('should have request ip', (done) => { - Parse.Cloud.afterSave('MyObject', (req) => { + it('should have request ip', done => { + Parse.Cloud.afterSave('MyObject', req => { expect(req.ip).toBeDefined(); }); @@ -1264,65 +1429,69 @@ describe('afterSave hooks', () => { }); describe('beforeDelete hooks', () => { - it('should have request headers', (done) => { - Parse.Cloud.beforeDelete('MyObject', (req) => { + it('should have request headers', done => { + Parse.Cloud.beforeDelete('MyObject', req => { expect(req.headers).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); const myObject = new MyObject(); - myObject.save() + myObject + .save() .then(myObj => myObj.destroy()) .then(() => done()); }); - it('should have request ip', (done) => { - Parse.Cloud.beforeDelete('MyObject', (req) => { + it('should have request ip', done => { + Parse.Cloud.beforeDelete('MyObject', req => { expect(req.ip).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); const myObject = new MyObject(); - myObject.save() + myObject + .save() .then(myObj => myObj.destroy()) .then(() => done()); }); }); describe('afterDelete hooks', () => { - it('should have request headers', (done) => { - Parse.Cloud.afterDelete('MyObject', (req) => { + it('should have request headers', done => { + Parse.Cloud.afterDelete('MyObject', req => { expect(req.headers).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); const myObject = new MyObject(); - myObject.save() + myObject + .save() .then(myObj => myObj.destroy()) .then(() => done()); }); - it('should have request ip', (done) => { - Parse.Cloud.afterDelete('MyObject', (req) => { + it('should have request ip', done => { + Parse.Cloud.afterDelete('MyObject', req => { expect(req.ip).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); const myObject = new MyObject(); - myObject.save() + myObject + .save() .then(myObj => myObj.destroy()) .then(() => done()); }); }); describe('beforeFind hooks', () => { - it('should add beforeFind trigger', (done) => { - Parse.Cloud.beforeFind('MyObject', (req) => { + it('should add beforeFind trigger', done => { + Parse.Cloud.beforeFind('MyObject', req => { const q = req.query; expect(q instanceof Parse.Query).toBe(true); const jsonQuery = q.toJSON(); expect(jsonQuery.where.key).toEqual('value'); - expect(jsonQuery.where.some).toEqual({'$gt': 10}); + expect(jsonQuery.where.some).toEqual({ $gt: 10 }); expect(jsonQuery.include).toEqual('otherKey,otherValue'); expect(jsonQuery.limit).toEqual(100); expect(jsonQuery.skip).toBe(undefined); @@ -1339,8 +1508,8 @@ describe('beforeFind hooks', () => { }); }); - it('should use modify', (done) => { - Parse.Cloud.beforeFind('MyObject', (req) => { + it('should use modify', done => { + Parse.Cloud.beforeFind('MyObject', req => { const q = req.query; q.equalTo('forced', true); }); @@ -1353,7 +1522,7 @@ describe('beforeFind hooks', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { const query = new Parse.Query('MyObject'); query.equalTo('forced', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); const firstResult = results[0]; expect(firstResult.get('forced')).toBe(true); @@ -1362,8 +1531,8 @@ describe('beforeFind hooks', () => { }); }); - it('should use the modified the query', (done) => { - Parse.Cloud.beforeFind('MyObject', (req) => { + it('should use the modified the query', done => { + Parse.Cloud.beforeFind('MyObject', req => { const q = req.query; const otherQuery = new Parse.Query('MyObject'); otherQuery.equalTo('forced', true); @@ -1378,31 +1547,34 @@ describe('beforeFind hooks', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { const query = new Parse.Query('MyObject'); query.equalTo('forced', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(2); done(); }); }); }); - it('should reject queries', (done) => { + it('should reject queries', done => { Parse.Cloud.beforeFind('MyObject', () => { return Promise.reject('Do not run that query'); }); const query = new Parse.Query('MyObject'); - query.find().then(() => { - fail('should not succeed'); - done(); - }, (err) => { - expect(err.code).toBe(1); - expect(err.message).toEqual('Do not run that query'); - done(); - }); + query.find().then( + () => { + fail('should not succeed'); + done(); + }, + err => { + expect(err.code).toBe(1); + expect(err.message).toEqual('Do not run that query'); + done(); + } + ); }); - it('should handle empty where', (done) => { - Parse.Cloud.beforeFind('MyObject', (req) => { + it('should handle empty where', done => { + Parse.Cloud.beforeFind('MyObject', req => { const otherQuery = new Parse.Query('MyObject'); otherQuery.equalTo('some', true); return Parse.Query.or(req.query, otherQuery); @@ -1414,16 +1586,19 @@ describe('beforeFind hooks', () => { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-REST-API-Key': 'rest', }, - }).then(() => { - done(); - }, (err) => { - fail(err); - done(); - }); + }).then( + () => { + done(); + }, + err => { + fail(err); + done(); + } + ); }); - it('should handle sorting where', (done) => { - Parse.Cloud.beforeFind('MyObject', (req) => { + it('should handle sorting where', done => { + Parse.Cloud.beforeFind('MyObject', req => { const query = req.query; query.ascending('score'); return query; @@ -1436,24 +1611,28 @@ describe('beforeFind hooks', () => { object.set('score', Math.floor(Math.random() * 100)); objects.push(object); } - Parse.Object.saveAll(objects).then(() => { - const query = new Parse.Query('MyObject'); - return query.find(); - }).then((objects) => { - let lastScore = -1; - objects.forEach((element) => { - expect(element.get('score') >= lastScore).toBe(true); - lastScore = element.get('score'); - }); - }).then(done).catch(done.fail); + Parse.Object.saveAll(objects) + .then(() => { + const query = new Parse.Query('MyObject'); + return query.find(); + }) + .then(objects => { + let lastScore = -1; + objects.forEach(element => { + expect(element.get('score') >= lastScore).toBe(true); + lastScore = element.get('score'); + }); + }) + .then(done) + .catch(done.fail); }); - it('should add beforeFind trigger using get API',(done) => { + it('should add beforeFind trigger using get API', done => { const hook = { method: function(req) { expect(req.isGet).toEqual(true); return Promise.resolve(); - } + }, }; spyOn(hook, 'method').and.callThrough(); Parse.Cloud.beforeFind('MyObject', hook.method); @@ -1465,7 +1644,7 @@ describe('beforeFind hooks', () => { uri: 'http://localhost:8378/1/classes/MyObject/' + obj.id, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }, json: true, }).then(body => { @@ -1476,102 +1655,108 @@ describe('beforeFind hooks', () => { }); }); - it('should have request headers', (done) => { - Parse.Cloud.beforeFind('MyObject', (req) => { + it('should have request headers', done => { + Parse.Cloud.beforeFind('MyObject', req => { expect(req.headers).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); const myObject = new MyObject(); - myObject.save() - .then((myObj) => { + myObject + .save() + .then(myObj => { const query = new Parse.Query('MyObject'); query.equalTo('objectId', myObj.id); - return Promise.all([ - query.get(myObj.id), - query.first(), - query.find(), - ]); + return Promise.all([query.get(myObj.id), query.first(), query.find()]); }) .then(() => done()); }); - it('should have request ip', (done) => { - Parse.Cloud.beforeFind('MyObject', (req) => { + it('should have request ip', done => { + Parse.Cloud.beforeFind('MyObject', req => { expect(req.ip).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); const myObject = new MyObject(); - myObject.save() - .then((myObj) => { + myObject + .save() + .then(myObj => { const query = new Parse.Query('MyObject'); query.equalTo('objectId', myObj.id); - return Promise.all([ - query.get(myObj.id), - query.first(), - query.find(), - ]); + return Promise.all([query.get(myObj.id), query.first(), query.find()]); }) .then(() => done()); }); }); describe('afterFind hooks', () => { - it('should add afterFind trigger using get',(done) => { - Parse.Cloud.afterFind('MyObject', (req) => { - for(let i = 0 ; i < req.objects.length ; i++){ - req.objects[i].set("secretField","###"); + it('should add afterFind trigger using get', done => { + Parse.Cloud.afterFind('MyObject', req => { + for (let i = 0; i < req.objects.length; i++) { + req.objects[i].set('secretField', '###'); } return req.objects; }); const obj = new Parse.Object('MyObject'); obj.set('secretField', 'SSID'); - obj.save().then(function() { - const query = new Parse.Query('MyObject'); - query.get(obj.id).then(function(result) { - expect(result.get('secretField')).toEqual('###'); - done(); - }, function(error) { + obj.save().then( + function() { + const query = new Parse.Query('MyObject'); + query.get(obj.id).then( + function(result) { + expect(result.get('secretField')).toEqual('###'); + done(); + }, + function(error) { + fail(error); + done(); + } + ); + }, + function(error) { fail(error); done(); - }); - }, function(error) { - fail(error); - done(); - }); + } + ); }); - it('should add afterFind trigger using find',(done) => { - Parse.Cloud.afterFind('MyObject', (req) => { - for(let i = 0 ; i < req.objects.length ; i++){ - req.objects[i].set("secretField","###"); + it('should add afterFind trigger using find', done => { + Parse.Cloud.afterFind('MyObject', req => { + for (let i = 0; i < req.objects.length; i++) { + req.objects[i].set('secretField', '###'); } return req.objects; }); const obj = new Parse.Object('MyObject'); obj.set('secretField', 'SSID'); - obj.save().then(function() { - const query = new Parse.Query('MyObject'); - query.equalTo('objectId',obj.id); - query.find().then(function(results) { - expect(results[0].get('secretField')).toEqual('###'); - done(); - }, function(error) { + obj.save().then( + function() { + const query = new Parse.Query('MyObject'); + query.equalTo('objectId', obj.id); + query.find().then( + function(results) { + expect(results[0].get('secretField')).toEqual('###'); + done(); + }, + function(error) { + fail(error); + done(); + } + ); + }, + function(error) { fail(error); done(); - }); - }, function(error) { - fail(error); - done(); - }); + } + ); }); - it('should filter out results',(done) => { - Parse.Cloud.afterFind('MyObject', (req) => { + it('should filter out results', done => { + Parse.Cloud.afterFind('MyObject', req => { const filteredResults = []; - for(let i = 0 ; i < req.objects.length ; i++){ - if(req.objects[i].get("secretField") === "SSID1") { + for (let i = 0; i < req.objects.length; i++) { + if (req.objects[i].get('secretField') === 'SSID1') { filteredResults.push(req.objects[i]); } } @@ -1581,48 +1766,60 @@ describe('afterFind hooks', () => { obj0.set('secretField', 'SSID1'); const obj1 = new Parse.Object('MyObject'); obj1.set('secretField', 'SSID2'); - Parse.Object.saveAll([obj0, obj1]).then(function() { - const query = new Parse.Query('MyObject'); - query.find().then(function(results) { - expect(results[0].get('secretField')).toEqual('SSID1'); - expect(results.length).toEqual(1); - done(); - }, function(error) { + Parse.Object.saveAll([obj0, obj1]).then( + function() { + const query = new Parse.Query('MyObject'); + query.find().then( + function(results) { + expect(results[0].get('secretField')).toEqual('SSID1'); + expect(results.length).toEqual(1); + done(); + }, + function(error) { + fail(error); + done(); + } + ); + }, + function(error) { fail(error); done(); - }); - }, function(error) { - fail(error); - done(); - }); + } + ); }); - it('should handle failures',(done) => { + it('should handle failures', done => { Parse.Cloud.afterFind('MyObject', () => { - throw new Parse.Error(Parse.Error.SCRIPT_FAILED, "It should fail"); + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'It should fail'); }); const obj = new Parse.Object('MyObject'); obj.set('secretField', 'SSID'); - obj.save().then(function() { - const query = new Parse.Query('MyObject'); - query.equalTo('objectId',obj.id); - query.find().then(function() { - fail("AfterFind should handle response failure correctly"); - done(); - }, function() { + obj.save().then( + function() { + const query = new Parse.Query('MyObject'); + query.equalTo('objectId', obj.id); + query.find().then( + function() { + fail('AfterFind should handle response failure correctly'); + done(); + }, + function() { + done(); + } + ); + }, + function() { done(); - }); - }, function() { - done(); - }); + } + ); }); - it('should also work with promise',(done) => { - Parse.Cloud.afterFind('MyObject', (req) => { - return new Promise((resolve) => { - setTimeout(function(){ - for(let i = 0 ; i < req.objects.length ; i++){ - req.objects[i].set("secretField","###"); + it('should also work with promise', done => { + Parse.Cloud.afterFind('MyObject', req => { + return new Promise(resolve => { + setTimeout(function() { + for (let i = 0; i < req.objects.length; i++) { + req.objects[i].set('secretField', '###'); } resolve(req.objects); }, 1000); @@ -1630,22 +1827,28 @@ describe('afterFind hooks', () => { }); const obj = new Parse.Object('MyObject'); obj.set('secretField', 'SSID'); - obj.save().then(function() { - const query = new Parse.Query('MyObject'); - query.equalTo('objectId',obj.id); - query.find().then(function(results) { - expect(results[0].get('secretField')).toEqual('###'); - done(); - }, function(error) { + obj.save().then( + function() { + const query = new Parse.Query('MyObject'); + query.equalTo('objectId', obj.id); + query.find().then( + function(results) { + expect(results[0].get('secretField')).toEqual('###'); + done(); + }, + function(error) { + fail(error); + } + ); + }, + function(error) { fail(error); - }); - }, function(error) { - fail(error); - }); + } + ); }); - it('should alter select', (done) => { - Parse.Cloud.beforeFind('MyObject', (req) => { + it('should alter select', done => { + Parse.Cloud.beforeFind('MyObject', req => { req.query.select('white'); return req.query; }); @@ -1653,104 +1856,93 @@ describe('afterFind hooks', () => { const obj0 = new Parse.Object('MyObject') .set('white', true) .set('black', true); - obj0.save() - .then(() => { - new Parse.Query('MyObject') - .first() - .then(result => { - expect(result.get('white')).toBe(true); - expect(result.get('black')).toBe(undefined); - done(); - }); + obj0.save().then(() => { + new Parse.Query('MyObject').first().then(result => { + expect(result.get('white')).toBe(true); + expect(result.get('black')).toBe(undefined); + done(); }); + }); }); - it('should not alter select', (done) => { + it('should not alter select', done => { const obj0 = new Parse.Object('MyObject') .set('white', true) .set('black', true); - obj0.save() - .then(() => { - new Parse.Query('MyObject') - .first() - .then(result => { - expect(result.get('white')).toBe(true); - expect(result.get('black')).toBe(true); - done(); - }); + obj0.save().then(() => { + new Parse.Query('MyObject').first().then(result => { + expect(result.get('white')).toBe(true); + expect(result.get('black')).toBe(true); + done(); }); + }); }); - it('should set count to true on beforeFind hooks if query is count', (done) => { + it('should set count to true on beforeFind hooks if query is count', done => { const hook = { method: function(req) { expect(req.count).toBe(true); return Promise.resolve(); - } + }, }; spyOn(hook, 'method').and.callThrough(); Parse.Cloud.beforeFind('Stuff', hook.method); - new Parse.Query('Stuff').count().then((count) => { + new Parse.Query('Stuff').count().then(count => { expect(count).toBe(0); expect(hook.method).toHaveBeenCalled(); done(); }); }); - it('should set count to false on beforeFind hooks if query is not count', (done) => { + it('should set count to false on beforeFind hooks if query is not count', done => { const hook = { method: function(req) { expect(req.count).toBe(false); return Promise.resolve(); - } + }, }; spyOn(hook, 'method').and.callThrough(); Parse.Cloud.beforeFind('Stuff', hook.method); - new Parse.Query('Stuff').find().then((res) => { + new Parse.Query('Stuff').find().then(res => { expect(res.length).toBe(0); expect(hook.method).toHaveBeenCalled(); done(); }); }); - it('should have request headers', (done) => { - Parse.Cloud.afterFind('MyObject', (req) => { + it('should have request headers', done => { + Parse.Cloud.afterFind('MyObject', req => { expect(req.headers).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); const myObject = new MyObject(); - myObject.save() - .then((myObj) => { + myObject + .save() + .then(myObj => { const query = new Parse.Query('MyObject'); query.equalTo('objectId', myObj.id); - return Promise.all([ - query.get(myObj.id), - query.first(), - query.find(), - ]); + return Promise.all([query.get(myObj.id), query.first(), query.find()]); }) .then(() => done()); }); - it('should have request ip', (done) => { - Parse.Cloud.afterFind('MyObject', (req) => { + it('should have request ip', done => { + Parse.Cloud.afterFind('MyObject', req => { expect(req.ip).toBeDefined(); }); const MyObject = Parse.Object.extend('MyObject'); const myObject = new MyObject(); - myObject.save() - .then((myObj) => { + myObject + .save() + .then(myObj => { const query = new Parse.Query('MyObject'); query.equalTo('objectId', myObj.id); - return Promise.all([ - query.get(myObj.id), - query.first(), - query.find(), - ]); + return Promise.all([query.get(myObj.id), query.first(), query.find()]); }) - .then(() => done()).catch(done.fail); + .then(() => done()) + .catch(done.fail); }); it('should validate triggers correctly', () => { @@ -1768,59 +1960,67 @@ describe('afterFind hooks', () => { }).not.toThrow(); }); - it('should skip afterFind hooks for aggregate', (done) => { + it('should skip afterFind hooks for aggregate', done => { const hook = { method: function() { return Promise.reject(); - } + }, }; spyOn(hook, 'method').and.callThrough(); Parse.Cloud.afterFind('MyObject', hook.method); - const obj = new Parse.Object('MyObject') - const pipeline = [{ - group: { objectId: {} } - }]; - obj.save().then(() => { - const query = new Parse.Query('MyObject'); - return query.aggregate(pipeline); - }).then((results) => { - expect(results[0].objectId).toEqual(null); - expect(hook.method).not.toHaveBeenCalled(); - done(); - }); + const obj = new Parse.Object('MyObject'); + const pipeline = [ + { + group: { objectId: {} }, + }, + ]; + obj + .save() + .then(() => { + const query = new Parse.Query('MyObject'); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results[0].objectId).toEqual(null); + expect(hook.method).not.toHaveBeenCalled(); + done(); + }); }); - it('should skip afterFind hooks for distinct', (done) => { + it('should skip afterFind hooks for distinct', done => { const hook = { method: function() { return Promise.reject(); - } + }, }; spyOn(hook, 'method').and.callThrough(); Parse.Cloud.afterFind('MyObject', hook.method); - const obj = new Parse.Object('MyObject') + const obj = new Parse.Object('MyObject'); obj.set('score', 10); - obj.save().then(() => { - const query = new Parse.Query('MyObject'); - return query.distinct('score'); - }).then((results) => { - expect(results[0]).toEqual(10); - expect(hook.method).not.toHaveBeenCalled(); - done(); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('MyObject'); + return query.distinct('score'); + }) + .then(results => { + expect(results[0]).toEqual(10); + expect(hook.method).not.toHaveBeenCalled(); + done(); + }); }); it('should expose context in before and afterSave', async () => { let calledBefore = false; let calledAfter = false; - Parse.Cloud.beforeSave('MyClass', (req) => { + Parse.Cloud.beforeSave('MyClass', req => { req.context = { key: 'value', otherKey: 1, - } + }; calledBefore = true; }); - Parse.Cloud.afterSave('MyClass', (req) => { + Parse.Cloud.afterSave('MyClass', req => { expect(req.context.otherKey).toBe(1); expect(req.context.key).toBe('value'); calledAfter = true; @@ -1835,12 +2035,12 @@ describe('afterFind hooks', () => { it('should expose context in before and afterSave and let keys be set individually', async () => { let calledBefore = false; let calledAfter = false; - Parse.Cloud.beforeSave('MyClass', (req) => { + Parse.Cloud.beforeSave('MyClass', req => { req.context.some = 'value'; req.context.yolo = 1; calledBefore = true; }); - Parse.Cloud.afterSave('MyClass', (req) => { + Parse.Cloud.afterSave('MyClass', req => { expect(req.context.yolo).toBe(1); expect(req.context.some).toBe('value'); calledAfter = true; diff --git a/spec/CloudCodeLogger.spec.js b/spec/CloudCodeLogger.spec.js index 1d1e7824dd..60530c66cc 100644 --- a/spec/CloudCodeLogger.spec.js +++ b/spec/CloudCodeLogger.spec.js @@ -1,11 +1,13 @@ -const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; +const LoggerController = require('../lib/Controllers/LoggerController') + .LoggerController; +const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') + .WinstonLoggerAdapter; const fs = require('fs'); const Config = require('../lib/Config'); const loremFile = __dirname + '/support/lorem.txt'; -describe("Cloud Code Logger", () => { +describe('Cloud Code Logger', () => { let user; beforeEach(done => { @@ -15,20 +17,20 @@ describe("Cloud Code Logger", () => { silent: true, }).then(() => { return Parse.User.signUp('tester', 'abc') - .then(loggedInUser => user = loggedInUser) + .then(loggedInUser => (user = loggedInUser)) .then(() => Parse.User.logIn(user.get('username'), 'abc')) - .then(() => done()) + .then(() => done()); }); }); // Note that helpers takes care of logout. // see helpers.js:afterEach - it("should expose log to functions", () => { + it('should expose log to functions', () => { const config = Config.get('test'); const spy = spyOn(config.loggerController, 'log').and.callThrough(); - Parse.Cloud.define("loggerTest", (req) => { + Parse.Cloud.define('loggerTest', req => { req.log.info('logTest', 'info log', { info: 'some log' }); req.log.error('logTest', 'error log', { error: 'there was an error' }); return {}; @@ -41,8 +43,12 @@ describe("Cloud Code Logger", () => { const infoMessage = spy.calls.all()[0]; expect(cloudFunctionMessage.args[0]).toBe('info'); expect(cloudFunctionMessage.args[1][1].params).toEqual({}); - expect(cloudFunctionMessage.args[1][0]).toMatch(/Ran cloud function loggerTest for user [^ ]* with:\n {2}Input: {}\n {2}Result: {}/); - expect(cloudFunctionMessage.args[1][1].functionName).toEqual('loggerTest'); + expect(cloudFunctionMessage.args[1][0]).toMatch( + /Ran cloud function loggerTest for user [^ ]* with:\n {2}Input: {}\n {2}Result: {}/ + ); + expect(cloudFunctionMessage.args[1][1].functionName).toEqual( + 'loggerTest' + ); expect(errorMessage.args[0]).toBe('error'); expect(errorMessage.args[1][2].error).toBe('there was an error'); expect(errorMessage.args[1][0]).toBe('logTest'); @@ -57,13 +63,13 @@ describe("Cloud Code Logger", () => { it('trigger should obfuscate password', done => { const logController = new LoggerController(new WinstonLoggerAdapter()); - Parse.Cloud.beforeSave(Parse.User, (req) => { + Parse.Cloud.beforeSave(Parse.User, req => { return req.object; }); Parse.User.signUp('tester123', 'abc') .then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 })) - .then((res) => { + .then(res => { const entry = res[0]; expect(entry.message).not.toMatch(/password":"abc/); expect(entry.message).toMatch(/\*\*\*\*\*\*\*\*/); @@ -72,36 +78,43 @@ describe("Cloud Code Logger", () => { .then(null, e => done.fail(e)); }); - it("should expose log to trigger", (done) => { + it('should expose log to trigger', done => { const logController = new LoggerController(new WinstonLoggerAdapter()); - Parse.Cloud.beforeSave("MyObject", (req) => { + Parse.Cloud.beforeSave('MyObject', req => { req.log.info('beforeSave MyObject', 'info log', { info: 'some log' }); - req.log.error('beforeSave MyObject', 'error log', { error: 'there was an error' }); + req.log.error('beforeSave MyObject', 'error log', { + error: 'there was an error', + }); return {}; }); const 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); - const lastLogs = res.slice(0, 3); - const cloudTriggerMessage = lastLogs[0]; - const errorMessage = lastLogs[1]; - const infoMessage = lastLogs[2]; - expect(cloudTriggerMessage.level).toBe('info'); - expect(cloudTriggerMessage.triggerType).toEqual('beforeSave'); - expect(cloudTriggerMessage.message).toMatch(/beforeSave triggered for MyObject for user [^ ]*\n {2}Input: {}\n {2}Result: {}/); - expect(cloudTriggerMessage.user).toBe(user.id); - 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(); - }); + obj + .save() + .then(() => { + return logController.getLogs({ from: Date.now() - 500, size: 1000 }); + }) + .then(res => { + expect(res.length).not.toBe(0); + const lastLogs = res.slice(0, 3); + const cloudTriggerMessage = lastLogs[0]; + const errorMessage = lastLogs[1]; + const infoMessage = lastLogs[2]; + expect(cloudTriggerMessage.level).toBe('info'); + expect(cloudTriggerMessage.triggerType).toEqual('beforeSave'); + expect(cloudTriggerMessage.message).toMatch( + /beforeSave triggered for MyObject for user [^ ]*\n {2}Input: {}\n {2}Result: {}/ + ); + expect(cloudTriggerMessage.user).toBe(user.id); + 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(); + }); }); it('should truncate really long lines when asked to', () => { @@ -114,7 +127,7 @@ describe("Cloud Code Logger", () => { it('should truncate input and result of long lines', done => { const logController = new LoggerController(new WinstonLoggerAdapter()); const longString = fs.readFileSync(loremFile, 'utf8'); - Parse.Cloud.define('aFunction', (req) => { + Parse.Cloud.define('aFunction', req => { return req.params; }); @@ -124,7 +137,8 @@ describe("Cloud Code Logger", () => { const log = logs[0]; expect(log.level).toEqual('info'); expect(log.message).toMatch( - /Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {.*?\(truncated\)$/m); + /Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {.*?\(truncated\)$/m + ); done(); }) .then(null, e => done.fail(e)); @@ -132,22 +146,22 @@ describe("Cloud Code Logger", () => { it('should log an afterSave', done => { const logController = new LoggerController(new WinstonLoggerAdapter()); - Parse.Cloud.afterSave("MyObject", () => { }); + Parse.Cloud.afterSave('MyObject', () => {}); new Parse.Object('MyObject') .save() .then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 })) - .then((logs) => { + .then(logs => { const log = logs[0]; expect(log.triggerType).toEqual('afterSave'); done(); }) - // catch errors - not that the error is actually useful :( + // catch errors - not that the error is actually useful :( .then(null, e => done.fail(e)); }); it('should log a denied beforeSave', done => { const logController = new LoggerController(new WinstonLoggerAdapter()); - Parse.Cloud.beforeSave("MyObject", () => { + Parse.Cloud.beforeSave('MyObject', () => { throw 'uh oh!'; }); @@ -162,7 +176,7 @@ describe("Cloud Code Logger", () => { const log = logs[1]; // 0 is the 'uh oh!' from rejection... expect(log.level).toEqual('error'); expect(log.error).toEqual({ code: 141, message: 'uh oh!' }); - done() + done(); }); }); @@ -179,7 +193,8 @@ describe("Cloud Code Logger", () => { const log = logs[0]; expect(log.level).toEqual('info'); expect(log.message).toMatch( - /Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Result: "it worked!/); + /Ran cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Result: "it worked!/ + ); done(); }); }); @@ -192,13 +207,16 @@ describe("Cloud Code Logger", () => { }); Parse.Cloud.run('aFunction', { foo: 'bar' }) - .then(null, () => logController.getLogs({ from: Date.now() - 500, size: 1000 })) + .then(null, () => + logController.getLogs({ from: Date.now() - 500, size: 1000 }) + ) .then(logs => { expect(logs[0].message).toBe('it failed!'); const log = logs[1]; expect(log.level).toEqual('error'); expect(log.message).toMatch( - /Failed running cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Error: {"code":141,"message":"it failed!"}/); + /Failed running cloud function aFunction for user [^ ]* with:\n {2}Input: {"foo":"bar"}\n {2}Error: {"code":141,"message":"it failed!"}/ + ); done(); }); }); @@ -206,7 +224,7 @@ describe("Cloud Code Logger", () => { xit('should log a changed beforeSave indicating a change', done => { const logController = new LoggerController(new WinstonLoggerAdapter()); - Parse.Cloud.beforeSave("MyObject", (req) => { + Parse.Cloud.beforeSave('MyObject', req => { const myObj = req.object; myObj.set('aChange', true); return myObj; @@ -235,9 +253,9 @@ describe("Cloud Code Logger", () => { return 'verify code success'; }); - Parse.Cloud.run('testFunction', {username:'hawk',password:'123456'}) + Parse.Cloud.run('testFunction', { username: 'hawk', password: '123456' }) .then(() => logController.getLogs({ from: Date.now() - 500, size: 1000 })) - .then((res) => { + .then(res => { const entry = res[0]; expect(entry.params.password).toMatch(/\*\*\*\*\*\*\*\*/); done(); @@ -250,9 +268,11 @@ describe("Cloud Code Logger", () => { const spy = spyOn(config.loggerController, 'error').and.callThrough(); try { const object = new Parse.Object('Object'); - object.id = 'invalid' + object.id = 'invalid'; await object.fetch(); - } catch(e) { /**/ } + } catch (e) { + /**/ + } expect(spy).toHaveBeenCalled(); expect(spy.calls.count()).toBe(1); const { args } = spy.calls.mostRecent(); diff --git a/spec/DatabaseController.spec.js b/spec/DatabaseController.spec.js index fe9ee8069e..c1d3dad216 100644 --- a/spec/DatabaseController.spec.js +++ b/spec/DatabaseController.spec.js @@ -2,50 +2,62 @@ const DatabaseController = require('../lib/Controllers/DatabaseController.js'); const validateQuery = DatabaseController._validateQuery; describe('DatabaseController', function() { - describe('validateQuery', function() { - - it('should restructure simple cases of SERVER-13732', (done) => { - const query = {$or: [{a: 1}, {a: 2}], _rperm: {$in: ['a', 'b']}, foo: 3}; + it('should restructure simple cases of SERVER-13732', done => { + const query = { + $or: [{ a: 1 }, { a: 2 }], + _rperm: { $in: ['a', 'b'] }, + foo: 3, + }; validateQuery(query); - expect(query).toEqual({$or: [{a: 1, _rperm: {$in: ['a', 'b']}, foo: 3}, - {a: 2, _rperm: {$in: ['a', 'b']}, foo: 3}]}); + expect(query).toEqual({ + $or: [ + { a: 1, _rperm: { $in: ['a', 'b'] }, foo: 3 }, + { a: 2, _rperm: { $in: ['a', 'b'] }, foo: 3 }, + ], + }); done(); }); - it('should not restructure SERVER-13732 queries with $nears', (done) => { - let query = {$or: [{a: 1}, {b: 1}], c: {$nearSphere: {}}}; + it('should not restructure SERVER-13732 queries with $nears', done => { + let query = { $or: [{ a: 1 }, { b: 1 }], c: { $nearSphere: {} } }; validateQuery(query); - expect(query).toEqual({$or: [{a: 1}, {b: 1}], c: {$nearSphere: {}}}); + expect(query).toEqual({ + $or: [{ a: 1 }, { b: 1 }], + c: { $nearSphere: {} }, + }); - query = {$or: [{a: 1}, {b: 1}], c: {$near: {}}}; + query = { $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } }; validateQuery(query); - expect(query).toEqual({$or: [{a: 1}, {b: 1}], c: {$near: {}}}); + expect(query).toEqual({ $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } }); done(); }); - - it('should push refactored keys down a tree for SERVER-13732', (done) => { - const query = {a: 1, $or: [{$or: [{b: 1}, {b: 2}]}, - {$or: [{c: 1}, {c: 2}]}]}; + it('should push refactored keys down a tree for SERVER-13732', done => { + const query = { + a: 1, + $or: [{ $or: [{ b: 1 }, { b: 2 }] }, { $or: [{ c: 1 }, { c: 2 }] }], + }; validateQuery(query); - expect(query).toEqual({$or: [{$or: [{b: 1, a: 1}, {b: 2, a: 1}]}, - {$or: [{c: 1, a: 1}, {c: 2, a: 1}]}]}); + expect(query).toEqual({ + $or: [ + { $or: [{ b: 1, a: 1 }, { b: 2, a: 1 }] }, + { $or: [{ c: 1, a: 1 }, { c: 2, a: 1 }] }, + ], + }); done(); }); - it('should reject invalid queries', (done) => { - expect(() => validateQuery({$or: {'a': 1}})).toThrow(); + it('should reject invalid queries', done => { + expect(() => validateQuery({ $or: { a: 1 } })).toThrow(); done(); }); - it('should accept valid queries', (done) => { - expect(() => validateQuery({$or: [{'a': 1}, {'b': 2}]})).not.toThrow(); + it('should accept valid queries', done => { + expect(() => validateQuery({ $or: [{ a: 1 }, { b: 2 }] })).not.toThrow(); done(); }); - }); - }); diff --git a/spec/EmailVerificationToken.spec.js b/spec/EmailVerificationToken.spec.js index 0585831aee..052f308873 100644 --- a/spec/EmailVerificationToken.spec.js +++ b/spec/EmailVerificationToken.spec.js @@ -1,11 +1,10 @@ -"use strict"; +'use strict'; const request = require('request'); const requestp = require('request-promise'); const Config = require('../lib/Config'); -describe("Email Verification Token Expiration: ", () => { - +describe('Email Verification Token Expiration: ', () => { it('show the invalid verification link page, if the user clicks on the verify email link after the email verify token expires', done => { const user = new Parse.User(); let sendEmailOptions; @@ -14,34 +13,42 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 0.5, // 0.5 second - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setUsername("testEmailVerifyTokenValidity"); - user.setPassword("expiringToken"); + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); - }).then(() => { - // wait for 1 second - simulate user behavior to some extent + }) + .then(() => { + // wait for 1 second - simulate user behavior to some extent setTimeout(() => { expect(sendEmailOptions).not.toBeUndefined(); - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test'); - done(); - }); + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' + ); + done(); + } + ); }, 1000); - }).catch((err) => { + }) + .catch(err => { jfail(err); done(); }); @@ -55,41 +62,48 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 0.5, // 0.5 second - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setUsername("testEmailVerifyTokenValidity"); - user.setPassword("expiringToken"); + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); - }).then(() => { - // wait for 1 second - simulate user behavior to some extent + }) + .then(() => { + // wait for 1 second - simulate user behavior to some extent setTimeout(() => { expect(sendEmailOptions).not.toBeUndefined(); - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - user.fetch() - .then(() => { - expect(user.get('emailVerified')).toEqual(false); - done(); - }) - .catch(() => { - jfail(error); - done(); - }); - }); + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + user + .fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(false); + done(); + }) + .catch(() => { + jfail(error); + done(); + }); + } + ); }, 1000); - }).catch((error) => { + }) + .catch(error => { jfail(error); done(); }); @@ -103,29 +117,37 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setUsername("testEmailVerifyTokenValidity"); - user.setPassword("expiringToken"); + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); - }).then(() => { - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity'); - done(); - }); - }).catch((error) => { + }) + .then(() => { + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' + ); + done(); + } + ); + }) + .catch(error => { jfail(error); done(); }); @@ -139,36 +161,43 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setUsername("testEmailVerifyTokenValidity"); - user.setPassword("expiringToken"); + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); - }).then(() => { - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - user.fetch() - .then(() => { - expect(user.get('emailVerified')).toEqual(true); - done(); - }) - .catch((error) => { - jfail(error); - done(); - }); - }); - }).catch((error) => { + }) + .then(() => { + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + user + .fetch() + .then(() => { + expect(user.get('emailVerified')).toEqual(true); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + } + ); + }) + .catch(error => { jfail(error); done(); }); @@ -182,37 +211,43 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setUsername("testEmailVerifyTokenValidity"); - user.setPassword("expiringToken"); + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); - }).then(() => { - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - Parse.User.logIn("testEmailVerifyTokenValidity", "expiringToken") - .then(user => { - expect(typeof user).toBe('object'); - expect(user.get('emailVerified')).toBe(true); - done(); - }) - .catch((error) => { - jfail(error); - done(); - }); - }); - }).catch((error) => { + }) + .then(() => { + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') + .then(user => { + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(true); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + } + ); + }) + .catch(error => { jfail(error); done(); }); @@ -226,14 +261,14 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', }) .then(() => { user.setUsername('sets_email_verify_token_expires_at'); @@ -243,7 +278,9 @@ describe("Email Verification Token Expiration: ", () => { }) .then(() => { const config = Config.get('test'); - return config.database.find('_User', {username: 'sets_email_verify_token_expires_at'}); + return config.database.find('_User', { + username: 'sets_email_verify_token_expires_at', + }); }) .then(results => { expect(results.length).toBe(1); @@ -269,43 +306,53 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setUsername("unsets_email_verify_token_expires_at"); - user.setPassword("expiringToken"); + user.setUsername('unsets_email_verify_token_expires_at'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); }) .then(() => { - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - const config = Config.get('test'); - return config.database.find('_User', {username: 'unsets_email_verify_token_expires_at'}).then((results) => { - expect(results.length).toBe(1); - return results[0]; - }) - .then(user => { - expect(typeof user).toBe('object'); - expect(user.emailVerified).toEqual(true); - expect(typeof user._email_verify_token).toBe('undefined'); - expect(typeof user._email_verify_token_expires_at).toBe('undefined'); - done(); - }) - .catch(error => { - jfail(error); - done(); - }); - }); + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + const config = Config.get('test'); + return config.database + .find('_User', { + username: 'unsets_email_verify_token_expires_at', + }) + .then(results => { + expect(results.length).toBe(1); + return results[0]; + }) + .then(user => { + expect(typeof user).toBe('object'); + expect(user.emailVerified).toEqual(true); + expect(typeof user._email_verify_token).toBe('undefined'); + expect(typeof user._email_verify_token_expires_at).toBe( + 'undefined' + ); + done(); + }) + .catch(error => { + jfail(error); + done(); + }); + } + ); }) .catch(error => { jfail(error); @@ -321,28 +368,29 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; const serverConfig = { appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }; // setup server WITHOUT enabling the expire email verify token flag reconfigureServer(serverConfig) .then(() => { - user.setUsername("testEmailVerifyTokenValidity"); - user.setPassword("expiringToken"); + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); }) .then(() => { return new Promise((resolve, reject) => { - request.get(sendEmailOptions.link, { followRedirect: false, }) + request + .get(sendEmailOptions.link, { followRedirect: false }) .on('error', error => reject(error)) - .on('response', (response) => { + .on('response', response => { expect(response.statusCode).toEqual(302); resolve(user.fetch()); }); @@ -355,15 +403,21 @@ describe("Email Verification Token Expiration: ", () => { return reconfigureServer(serverConfig); }) .then(() => { - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity'); - done(); - }); + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=testEmailVerifyTokenValidity' + ); + done(); + } + ); }) - .catch((error) => { + .catch(error => { jfail(error); done(); }); @@ -377,25 +431,25 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; const serverConfig = { appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }; // setup server WITHOUT enabling the expire email verify token flag reconfigureServer(serverConfig) .then(() => { - user.setUsername("testEmailVerifyTokenValidity"); - user.setPassword("expiringToken"); + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); }) .then(() => { - // just get the user again - DO NOT email verify the user + // just get the user again - DO NOT email verify the user return user.fetch(); }) .then(() => { @@ -405,22 +459,27 @@ describe("Email Verification Token Expiration: ", () => { return reconfigureServer(serverConfig); }) .then(() => { - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test'); - done(); - }); + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=testEmailVerifyTokenValidity&appId=test' + ); + done(); + } + ); }) - .catch((error) => { + .catch(error => { jfail(error); done(); }); }); it('setting the email on the user should set a new email verification token and new expiration date for the token when expire email verify token flag is set', done => { - const user = new Parse.User(); let userBeforeEmailReset; @@ -430,28 +489,30 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} + sendMail: () => {}, }; const serverConfig = { appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }; reconfigureServer(serverConfig) .then(() => { - user.setUsername("newEmailVerifyTokenOnEmailReset"); - user.setPassword("expiringToken"); + user.setUsername('newEmailVerifyTokenOnEmailReset'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); }) .then(() => { const config = Config.get('test'); - return config.database.find('_User', {username: 'newEmailVerifyTokenOnEmailReset'}).then((results) => { - return results[0]; - }); + return config.database + .find('_User', { username: 'newEmailVerifyTokenOnEmailReset' }) + .then(results => { + return results[0]; + }); }) .then(userFromDb => { expect(typeof userFromDb).toBe('object'); @@ -459,25 +520,31 @@ describe("Email Verification Token Expiration: ", () => { // trigger another token generation by setting the email user.set('email', 'user@parse.com'); - return new Promise((resolve) => { - // wait for half a sec to get a new expiration time + return new Promise(resolve => { + // wait for half a sec to get a new expiration time setTimeout(() => resolve(user.save()), 500); }); }) .then(() => { const config = Config.get('test'); - return config.database.find('_User', {username: 'newEmailVerifyTokenOnEmailReset'}).then((results) => { - return results[0]; - }); + return config.database + .find('_User', { username: 'newEmailVerifyTokenOnEmailReset' }) + .then(results => { + return results[0]; + }); }) .then(userAfterEmailReset => { expect(typeof userAfterEmailReset).toBe('object'); - expect(userBeforeEmailReset._email_verify_token).not.toEqual(userAfterEmailReset._email_verify_token); - expect(userBeforeEmailReset._email_verify_token_expires_at).not.toEqual(userAfterEmailReset.__email_verify_token_expires_at); + expect(userBeforeEmailReset._email_verify_token).not.toEqual( + userAfterEmailReset._email_verify_token + ); + expect(userBeforeEmailReset._email_verify_token_expires_at).not.toEqual( + userAfterEmailReset.__email_verify_token_expires_at + ); expect(sendEmailOptions).toBeDefined(); done(); }) - .catch((error) => { + .catch(error => { jfail(error); done(); }); @@ -494,14 +561,14 @@ describe("Email Verification Token Expiration: ", () => { sendVerificationEmailCallCount++; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', }) .then(() => { user.setUsername('resends_verification_token'); @@ -511,11 +578,13 @@ describe("Email Verification Token Expiration: ", () => { }) .then(() => { const config = Config.get('test'); - return config.database.find('_User', {username: 'resends_verification_token'}).then((results) => { - return results[0]; - }); + return config.database + .find('_User', { username: 'resends_verification_token' }) + .then(results => { + return results[0]; + }); }) - .then((newUser) => { + .then(newUser => { // store this user before we make our email request userBeforeRequest = newUser; @@ -524,7 +593,7 @@ describe("Email Verification Token Expiration: ", () => { return requestp.post({ uri: 'http://localhost:8378/1/verificationEmailRequest', body: { - email: 'user@parse.com' + email: 'user@parse.com', }, headers: { 'X-Parse-Application-Id': Parse.applicationId, @@ -532,25 +601,31 @@ describe("Email Verification Token Expiration: ", () => { }, json: true, resolveWithFullResponse: true, - simple: false // this promise is only rejected if the call itself failed + simple: false, // this promise is only rejected if the call itself failed }); }) - .then((response) => { + .then(response => { expect(response.statusCode).toBe(200); expect(sendVerificationEmailCallCount).toBe(2); expect(sendEmailOptions).toBeDefined(); // query for this user again const config = Config.get('test'); - return config.database.find('_User', {username: 'resends_verification_token'}).then((results) => { - return results[0]; - }); + return config.database + .find('_User', { username: 'resends_verification_token' }) + .then(results => { + return results[0]; + }); }) - .then((userAfterRequest) => { + .then(userAfterRequest => { // verify that our token & expiration has been changed for this new request expect(typeof userAfterRequest).toBe('object'); - expect(userBeforeRequest._email_verify_token).not.toEqual(userAfterRequest._email_verify_token); - expect(userBeforeRequest._email_verify_token_expires_at).not.toEqual(userAfterRequest.__email_verify_token_expires_at); + expect(userBeforeRequest._email_verify_token).not.toEqual( + userAfterRequest._email_verify_token + ); + expect(userBeforeRequest._email_verify_token_expires_at).not.toEqual( + userAfterRequest.__email_verify_token_expires_at + ); done(); }) .catch(error => { @@ -569,14 +644,14 @@ describe("Email Verification Token Expiration: ", () => { sendVerificationEmailCallCount++; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', }) .then(() => { user.setUsername('no_new_verification_token_once_verified'); @@ -585,33 +660,35 @@ describe("Email Verification Token Expiration: ", () => { return user.signUp(); }) .then(() => { - return requestp.get({ - url: sendEmailOptions.link, - followRedirect: false, - resolveWithFullResponse: true, - simple: false - }) - .then((response) => { + return requestp + .get({ + url: sendEmailOptions.link, + followRedirect: false, + resolveWithFullResponse: true, + simple: false, + }) + .then(response => { expect(response.statusCode).toEqual(302); }); }) .then(() => { expect(sendVerificationEmailCallCount).toBe(1); - return requestp.post({ - uri: 'http://localhost:8378/1/verificationEmailRequest', - body: { - email: 'user@parse.com' - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: true, - resolveWithFullResponse: true, - simple: false // this promise is only rejected if the call itself failed - }) - .then((response) => { + return requestp + .post({ + uri: 'http://localhost:8378/1/verificationEmailRequest', + body: { + email: 'user@parse.com', + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + resolveWithFullResponse: true, + simple: false, // this promise is only rejected if the call itself failed + }) + .then(response => { expect(response.statusCode).toBe(400); expect(sendVerificationEmailCallCount).toBe(1); done(); @@ -632,29 +709,30 @@ describe("Email Verification Token Expiration: ", () => { sendVerificationEmailCallCount++; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - return requestp.post({ - uri: 'http://localhost:8378/1/verificationEmailRequest', - body: { - email: 'user@parse.com' - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: true, - resolveWithFullResponse: true, - simple: false - }) + return requestp + .post({ + uri: 'http://localhost:8378/1/verificationEmailRequest', + body: { + email: 'user@parse.com', + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + resolveWithFullResponse: true, + simple: false, + }) .then(response => { expect(response.statusCode).toBe(400); expect(sendVerificationEmailCallCount).toBe(0); @@ -677,34 +755,37 @@ describe("Email Verification Token Expiration: ", () => { sendVerificationEmailCallCount++; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - request.post({ - uri: 'http://localhost:8378/1/verificationEmailRequest', - body: {}, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', + request.post( + { + uri: 'http://localhost:8378/1/verificationEmailRequest', + body: {}, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + resolveWithFullResponse: true, + simple: false, }, - json: true, - resolveWithFullResponse: true, - simple: false - }, (err, response) => { - expect(response.statusCode).toBe(400); - expect(response.body.code).toBe(Parse.Error.EMAIL_MISSING); - expect(response.body.error).toBe('you must provide an email'); - expect(sendVerificationEmailCallCount).toBe(0); - expect(sendEmailOptions).not.toBeDefined(); - done(); - }); + (err, response) => { + expect(response.statusCode).toBe(400); + expect(response.body.code).toBe(Parse.Error.EMAIL_MISSING); + expect(response.body.error).toBe('you must provide an email'); + expect(sendVerificationEmailCallCount).toBe(0); + expect(sendEmailOptions).not.toBeDefined(); + done(); + } + ); }) .catch(error => { jfail(error); @@ -721,34 +802,39 @@ describe("Email Verification Token Expiration: ", () => { sendVerificationEmailCallCount++; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - request.post({ - uri: 'http://localhost:8378/1/verificationEmailRequest', - body: {email: 3}, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', + request.post( + { + uri: 'http://localhost:8378/1/verificationEmailRequest', + body: { email: 3 }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + resolveWithFullResponse: true, + simple: false, }, - json: true, - resolveWithFullResponse: true, - simple: false - }, (err, response) => { - expect(response.statusCode).toBe(400); - expect(response.body.code).toBe(Parse.Error.INVALID_EMAIL_ADDRESS); - expect(response.body.error).toBe('you must provide a valid email string'); - expect(sendVerificationEmailCallCount).toBe(0); - expect(sendEmailOptions).not.toBeDefined(); - done(); - }); + (err, response) => { + expect(response.statusCode).toBe(400); + expect(response.body.code).toBe(Parse.Error.INVALID_EMAIL_ADDRESS); + expect(response.body.error).toBe( + 'you must provide a valid email string' + ); + expect(sendVerificationEmailCallCount).toBe(0); + expect(sendEmailOptions).not.toBeDefined(); + done(); + } + ); }) .catch(error => { jfail(error); @@ -764,27 +850,29 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setUsername("testEmailVerifyTokenValidity"); - user.setPassword("expiringToken"); + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); }) .then(() => { - - user.fetch() + user + .fetch() .then(() => { expect(user.get('emailVerified')).toEqual(false); - expect(typeof user.get('_email_verify_token_expires_at')).toBe('undefined'); + expect(typeof user.get('_email_verify_token_expires_at')).toBe( + 'undefined' + ); expect(sendEmailOptions).toBeDefined(); done(); }) @@ -792,8 +880,8 @@ describe("Email Verification Token Expiration: ", () => { jfail(error); done(); }); - - }).catch((error) => { + }) + .catch(error => { jfail(error); done(); }); @@ -807,55 +895,64 @@ describe("Email Verification Token Expiration: ", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => { } - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailVerifyToken', verifyUserEmails: true, emailAdapter: emailAdapter, emailVerifyTokenValidityDuration: 5, // 5 seconds - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setUsername("testEmailVerifyTokenValidity"); - user.setPassword("expiringToken"); + user.setUsername('testEmailVerifyTokenValidity'); + user.setPassword('expiringToken'); user.set('email', 'user@parse.com'); return user.signUp(); - }).then(() => { - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - Parse.User.logIn("testEmailVerifyTokenValidity", "expiringToken") - .then(user => { - expect(typeof user).toBe('object'); - expect(user.get('emailVerified')).toBe(true); - - user.set('email', 'newEmail@parse.com'); - return user.save(); - }) - .then(() => user.fetch()) - .then(user => { - expect(typeof user).toBe('object'); - expect(user.get('email')).toBe('newEmail@parse.com'); - expect(user.get('emailVerified')).toBe(false); + }) + .then(() => { + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + Parse.User.logIn('testEmailVerifyTokenValidity', 'expiringToken') + .then(user => { + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(true); - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); + user.set('email', 'newEmail@parse.com'); + return user.save(); + }) + .then(() => user.fetch()) + .then(user => { + expect(typeof user).toBe('object'); + expect(user.get('email')).toBe('newEmail@parse.com'); + expect(user.get('emailVerified')).toBe(false); + + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + done(); + } + ); + }) + .catch(error => { + jfail(error); done(); }); - }) - .catch((error) => { - jfail(error); - done(); - }); - }); - }).catch((error) => { + } + ); + }) + .catch(error => { jfail(error); done(); }); }); - -}) +}); diff --git a/spec/EnableExpressErrorHandler.spec.js b/spec/EnableExpressErrorHandler.spec.js index 648e986c2a..a2ab9f79ce 100644 --- a/spec/EnableExpressErrorHandler.spec.js +++ b/spec/EnableExpressErrorHandler.spec.js @@ -1,66 +1,66 @@ -const ParseServer = require("../lib/index"); +const ParseServer = require('../lib/index'); const express = require('express'); const rp = require('request-promise'); describe('Enable express error handler', () => { - it('should call the default handler in case of error, like updating a non existing object', done => { - const serverUrl = "http://localhost:12667/parse" - const appId = "anOtherTestApp"; - const masterKey = "anOtherTestMasterKey"; + const serverUrl = 'http://localhost:12667/parse'; + const appId = 'anOtherTestApp'; + const masterKey = 'anOtherTestMasterKey'; let server; let lastError; - const parseServer = ParseServer.ParseServer(Object.assign({}, - defaultConfiguration, { + const parseServer = ParseServer.ParseServer( + Object.assign({}, defaultConfiguration, { appId: appId, masterKey: masterKey, serverURL: serverUrl, enableExpressErrorHandler: true, __indexBuildCompletionCallbackForTests: promise => { promise.then(() => { - expect(Parse.applicationId).toEqual("anOtherTestApp"); + expect(Parse.applicationId).toEqual('anOtherTestApp'); const app = express(); app.use('/parse', parseServer); server = app.listen(12667); - app.use(function (err, req, res, next) { + app.use(function(err, req, res, next) { next; lastError = err; - }) + }); rp({ method: 'PUT', uri: serverUrl + '/classes/AnyClass/nonExistingId', headers: { 'X-Parse-Application-Id': appId, - 'X-Parse-Master-Key': masterKey + 'X-Parse-Master-Key': masterKey, }, - body: { someField: "blablabla" }, - json: true - }).then(() => { - fail('Should throw error'); - }).catch(e => { - expect(e).toBeDefined(); - const reqError = e.error; - expect(reqError).toBeDefined(); - expect(lastError).toBeDefined(); + body: { someField: 'blablabla' }, + json: true, + }) + .then(() => { + fail('Should throw error'); + }) + .catch(e => { + expect(e).toBeDefined(); + const reqError = e.error; + expect(reqError).toBeDefined(); + expect(lastError).toBeDefined(); - expect(lastError.code).toEqual(101) - expect(lastError.message).toEqual('Object not found.') + expect(lastError.code).toEqual(101); + expect(lastError.message).toEqual('Object not found.'); - expect(lastError.code).toEqual(reqError.code); - expect(lastError.message).toEqual(reqError.error); - }).then(() => { - server.close(done); - }); - }) - } - } - )); + expect(lastError.code).toEqual(reqError.code); + expect(lastError.message).toEqual(reqError.error); + }) + .then(() => { + server.close(done); + }); + }); + }, + }) + ); }); - }); - diff --git a/spec/EnableSingleSchemaCache.spec.js b/spec/EnableSingleSchemaCache.spec.js index 386d186a3e..6e8e5a2305 100644 --- a/spec/EnableSingleSchemaCache.spec.js +++ b/spec/EnableSingleSchemaCache.spec.js @@ -3,44 +3,53 @@ const Config = require('../lib/Config'); const rest = require('../lib/rest'); describe('Enable single schema cache', () => { - beforeEach((done) => { + beforeEach(done => { reconfigureServer({ enableSingleSchemaCache: true, - schemaCacheTTL: 30000 + schemaCacheTTL: 30000, }).then(() => { done(); }); }); - it('can perform multiple create and query operations', (done) => { + it('can perform multiple create and query operations', done => { let config = fakeRequestForConfig(); let nobody = auth.nobody(config); - rest.create(config, nobody, 'Foo', {type: 1}).then(() => { - config = fakeRequestForConfig(); - nobody = auth.nobody(config); - return rest.create(config, nobody, 'Foo', {type: 2}); - }).then(() => { - config = fakeRequestForConfig(); - nobody = auth.nobody(config); - return rest.create(config, nobody, 'Bar'); - }).then(() => { - config = fakeRequestForConfig(); - nobody = auth.nobody(config); - return rest.find(config, nobody, 'Bar', {type: 1}); - }).then(() => { - fail('Should throw error'); - done(); - }, (error) => { - config = fakeRequestForConfig(); - nobody = auth.nobody(config); - expect(error).toBeDefined(); - return rest.find(config, nobody, 'Foo', {type: 1}); - }).then((response) => { - config = fakeRequestForConfig(); - nobody = auth.nobody(config); - expect(response.results.length).toEqual(1); - done(); - }); + rest + .create(config, nobody, 'Foo', { type: 1 }) + .then(() => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + return rest.create(config, nobody, 'Foo', { type: 2 }); + }) + .then(() => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + return rest.create(config, nobody, 'Bar'); + }) + .then(() => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + return rest.find(config, nobody, 'Bar', { type: 1 }); + }) + .then( + () => { + fail('Should throw error'); + done(); + }, + error => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + expect(error).toBeDefined(); + return rest.find(config, nobody, 'Foo', { type: 1 }); + } + ) + .then(response => { + config = fakeRequestForConfig(); + nobody = auth.nobody(config); + expect(response.results.length).toEqual(1); + done(); + }); }); }); diff --git a/spec/EventEmitterPubSub.spec.js b/spec/EventEmitterPubSub.spec.js index 99b4f51df4..9117662b20 100644 --- a/spec/EventEmitterPubSub.spec.js +++ b/spec/EventEmitterPubSub.spec.js @@ -1,4 +1,5 @@ -const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; +const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') + .EventEmitterPubSub; describe('EventEmitterPubSub', function() { it('can publish and subscribe', function() { diff --git a/spec/FilesController.spec.js b/spec/FilesController.spec.js index dd88399a70..e1c1eff1be 100644 --- a/spec/FilesController.spec.js +++ b/spec/FilesController.spec.js @@ -1,39 +1,43 @@ -const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; -const GridStoreAdapter = require("../lib/Adapters/Files/GridStoreAdapter").GridStoreAdapter; -const Config = require("../lib/Config"); +const LoggerController = require('../lib/Controllers/LoggerController') + .LoggerController; +const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') + .WinstonLoggerAdapter; +const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter') + .GridStoreAdapter; +const Config = require('../lib/Config'); const FilesController = require('../lib/Controllers/FilesController').default; const mockAdapter = { createFile: () => { return Promise.reject(new Error('it failed')); }, - deleteFile: () => { }, - getFileData: () => { }, - getFileLocation: () => 'xyz' -} + deleteFile: () => {}, + getFileData: () => {}, + getFileLocation: () => 'xyz', +}; // Small additional tests to improve overall coverage -describe("FilesController", () => { - it("should properly expand objects", (done) => { - +describe('FilesController', () => { + it('should properly expand objects', done => { const config = Config.get(Parse.applicationId); - const gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse'); - const filesController = new FilesController(gridStoreAdapter) - const result = filesController.expandFilesInObject(config, function () { }); + const gridStoreAdapter = new GridStoreAdapter( + 'mongodb://localhost:27017/parse' + ); + const filesController = new FilesController(gridStoreAdapter); + const result = filesController.expandFilesInObject(config, function() {}); expect(result).toBeUndefined(); const fullFile = { type: '__type', - url: "http://an.url" - } + url: 'http://an.url', + }; const anObject = { - aFile: fullFile - } + aFile: fullFile, + }; filesController.expandFilesInObject(config, anObject); - expect(anObject.aFile.url).toEqual("http://an.url"); + expect(anObject.aFile.url).toEqual('http://an.url'); done(); }); @@ -43,14 +47,16 @@ describe("FilesController", () => { reconfigureServer({ filesAdapter: mockAdapter }) .then(() => new Promise(resolve => setTimeout(resolve, 1000))) - .then(() => new Parse.File("yolo.txt", [1, 2, 3], "text/plain").save()) + .then(() => new Parse.File('yolo.txt', [1, 2, 3], 'text/plain').save()) .then( () => done.fail('should not succeed'), () => setImmediate(() => Promise.resolve('done')) ) .then(() => new Promise(resolve => setTimeout(resolve, 200))) - .then(() => logController.getLogs({ from: Date.now() - 1000, size: 1000 })) - .then((logs) => { + .then(() => + logController.getLogs({ from: Date.now() - 1000, size: 1000 }) + ) + .then(logs => { // we get two logs here: 1. the source of the failure to save the file // and 2 the message that will be sent back to the client. const log1 = logs.pop(); @@ -64,39 +70,48 @@ describe("FilesController", () => { }); }); - it("should add a unique hash to the file name when the preserveFileName option is false", (done) => { - - const config = Config.get(Parse.applicationId) - const gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse') - spyOn(gridStoreAdapter, 'createFile') - gridStoreAdapter.createFile.and.returnValue(Promise.resolve()) - const fileName = 'randomFileName.pdf' - const regexEscapedFileName = fileName.replace(/\./g, "\\$&") - const filesController = new FilesController(gridStoreAdapter, null, { preserveFileName: false }) - - filesController.createFile(config, fileName) - - expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1) - expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toMatch(`^.{32}_${regexEscapedFileName}$`) + it('should add a unique hash to the file name when the preserveFileName option is false', done => { + const config = Config.get(Parse.applicationId); + const gridStoreAdapter = new GridStoreAdapter( + 'mongodb://localhost:27017/parse' + ); + spyOn(gridStoreAdapter, 'createFile'); + gridStoreAdapter.createFile.and.returnValue(Promise.resolve()); + const fileName = 'randomFileName.pdf'; + const regexEscapedFileName = fileName.replace(/\./g, '\\$&'); + const filesController = new FilesController(gridStoreAdapter, null, { + preserveFileName: false, + }); + + filesController.createFile(config, fileName); + + expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1); + expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toMatch( + `^.{32}_${regexEscapedFileName}$` + ); done(); }); - it("should not add a unique hash to the file name when the preserveFileName option is true", (done) => { - - const config = Config.get(Parse.applicationId) - const gridStoreAdapter = new GridStoreAdapter('mongodb://localhost:27017/parse') - spyOn(gridStoreAdapter, 'createFile') - gridStoreAdapter.createFile.and.returnValue(Promise.resolve()) - const fileName = 'randomFileName.pdf' - const filesController = new FilesController(gridStoreAdapter, null, { preserveFileName: true }) - - filesController.createFile(config, fileName) - - expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1) - expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toEqual(fileName) + it('should not add a unique hash to the file name when the preserveFileName option is true', done => { + const config = Config.get(Parse.applicationId); + const gridStoreAdapter = new GridStoreAdapter( + 'mongodb://localhost:27017/parse' + ); + spyOn(gridStoreAdapter, 'createFile'); + gridStoreAdapter.createFile.and.returnValue(Promise.resolve()); + const fileName = 'randomFileName.pdf'; + const filesController = new FilesController(gridStoreAdapter, null, { + preserveFileName: true, + }); + + filesController.createFile(config, fileName); + + expect(gridStoreAdapter.createFile).toHaveBeenCalledTimes(1); + expect(gridStoreAdapter.createFile.calls.mostRecent().args[0]).toEqual( + fileName + ); done(); }); - }); diff --git a/spec/GridStoreAdapter.js b/spec/GridStoreAdapter.js index d408430dfd..87bc3d3004 100644 --- a/spec/GridStoreAdapter.js +++ b/spec/GridStoreAdapter.js @@ -1,15 +1,14 @@ -const MongoClient = require("mongodb").MongoClient; -const GridStore = require("mongodb").GridStore; +const MongoClient = require('mongodb').MongoClient; +const GridStore = require('mongodb').GridStore; -const GridStoreAdapter = require("../lib/Adapters/Files/GridStoreAdapter").GridStoreAdapter; -const Config = require("../lib/Config"); +const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter') + .GridStoreAdapter; +const Config = require('../lib/Config'); const FilesController = require('../lib/Controllers/FilesController').default; - // Small additional tests to improve overall coverage -describe_only_db('mongo')("GridStoreAdapter",() =>{ - it("should properly instanciate the GridStore when deleting a file", (done) => { - +describe_only_db('mongo')('GridStoreAdapter', () => { + it('should properly instanciate the GridStore when deleting a file', done => { const databaseURI = 'mongodb://localhost:27017/parse'; const config = Config.get(Parse.applicationId); const gridStoreAdapter = new GridStoreAdapter(databaseURI); @@ -22,7 +21,6 @@ describe_only_db('mongo')("GridStoreAdapter",() =>{ // new unlink method that will capture the mode in which GridStore was opened GridStore.prototype.unlink = function() { - // restore original unlink during first call GridStore.prototype.unlink = originalUnlink; @@ -31,48 +29,55 @@ describe_only_db('mongo')("GridStoreAdapter",() =>{ return originalUnlink.call(this); }; - - filesController.createFile(config, 'myFilename.txt', 'my file content', 'text/plain') + filesController + .createFile(config, 'myFilename.txt', 'my file content', 'text/plain') .then(myFile => { - return MongoClient.connect(databaseURI) .then(database => { - // Verify the existance of the fs.files document - return database.collection('fs.files').count().then(count => { - expect(count).toEqual(1); - return database; - }); + return database + .collection('fs.files') + .count() + .then(count => { + expect(count).toEqual(1); + return database; + }); }) .then(database => { - // Verify the existance of the fs.files document - return database.collection('fs.chunks').count().then(count => { - expect(count).toEqual(1); - return database.close(); - }); + return database + .collection('fs.chunks') + .count() + .then(count => { + expect(count).toEqual(1); + return database.close(); + }); }) .then(() => { return filesController.deleteFile(config, myFile.name); }); }) .then(() => { - return MongoClient.connect(databaseURI) + return MongoClient.connect(databaseURI) .then(database => { - // Verify the existance of the fs.files document - return database.collection('fs.files').count().then(count => { - expect(count).toEqual(0); - return database; - }); + return database + .collection('fs.files') + .count() + .then(count => { + expect(count).toEqual(0); + return database; + }); }) .then(database => { - // Verify the existance of the fs.files document - return database.collection('fs.chunks').count().then(count => { - expect(count).toEqual(0); - return database.close(); - }); + return database + .collection('fs.chunks') + .count() + .then(count => { + expect(count).toEqual(0); + return database.close(); + }); }); }) .then(() => { @@ -82,6 +87,5 @@ describe_only_db('mongo')("GridStoreAdapter",() =>{ done(); }) .catch(fail); - - }) + }); }); diff --git a/spec/HTTPRequest.spec.js b/spec/HTTPRequest.spec.js index 80100cdc1e..ce38d17cc1 100644 --- a/spec/HTTPRequest.spec.js +++ b/spec/HTTPRequest.spec.js @@ -1,123 +1,137 @@ 'use strict'; -const httpRequest = require("../lib/cloud-code/httpRequest"), +const httpRequest = require('../lib/cloud-code/httpRequest'), HTTPResponse = require('../lib/cloud-code/HTTPResponse').default, bodyParser = require('body-parser'), - express = require("express"); + express = require('express'); const port = 13371; -const httpRequestServer = "http://localhost:" + port; +const httpRequestServer = 'http://localhost:' + port; function startServer(done) { const app = express(); - app.use(bodyParser.json({ 'type': '*/*' })); - app.get("/hello", function(req, res){ - res.json({response: "OK"}); + app.use(bodyParser.json({ type: '*/*' })); + app.get('/hello', function(req, res) { + res.json({ response: 'OK' }); }); - app.get("/404", function(req, res){ + app.get('/404', function(req, res) { res.status(404); - res.send("NO"); + res.send('NO'); }); - app.get("/301", function(req, res){ + app.get('/301', function(req, res) { res.status(301); - res.location("/hello"); + res.location('/hello'); res.send(); }); - app.post('/echo', function(req, res){ + app.post('/echo', function(req, res) { res.json(req.body); }); - app.get('/qs', function(req, res){ + app.get('/qs', function(req, res) { res.json(req.query); }); return app.listen(13371, undefined, done); } -describe("httpRequest", () => { +describe('httpRequest', () => { let server; - beforeAll((done) => { + beforeAll(done => { server = startServer(done); }); - afterAll((done) => { + afterAll(done => { server.close(done); }); - it("should do /hello", (done) => { + it('should do /hello', done => { httpRequest({ - url: httpRequestServer + "/hello" - }).then(function(httpResponse){ - expect(httpResponse.status).toBe(200); - expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}')); - expect(httpResponse.text).toEqual('{"response":"OK"}'); - expect(httpResponse.data.response).toEqual("OK"); - done(); - }, function(){ - fail("should not fail"); - done(); - }) + url: httpRequestServer + '/hello', + }).then( + function(httpResponse) { + expect(httpResponse.status).toBe(200); + expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}')); + expect(httpResponse.text).toEqual('{"response":"OK"}'); + expect(httpResponse.data.response).toEqual('OK'); + done(); + }, + function() { + fail('should not fail'); + done(); + } + ); }); - it("should do /hello with callback and promises", (done) => { + it('should do /hello with callback and promises', done => { let calls = 0; httpRequest({ - url: httpRequestServer + "/hello", - success: function() { calls++; }, - error: function() { calls++; } - }).then(function(httpResponse){ - expect(calls).toBe(1); - expect(httpResponse.status).toBe(200); - expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}')); - expect(httpResponse.text).toEqual('{"response":"OK"}'); - expect(httpResponse.data.response).toEqual("OK"); - done(); - }, function(){ - fail("should not fail"); - done(); - }) + url: httpRequestServer + '/hello', + success: function() { + calls++; + }, + error: function() { + calls++; + }, + }).then( + function(httpResponse) { + expect(calls).toBe(1); + expect(httpResponse.status).toBe(200); + expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}')); + expect(httpResponse.text).toEqual('{"response":"OK"}'); + expect(httpResponse.data.response).toEqual('OK'); + done(); + }, + function() { + fail('should not fail'); + done(); + } + ); }); - it("should do not follow redirects by default", (done) => { - + it('should do not follow redirects by default', done => { httpRequest({ - url: httpRequestServer + "/301" - }).then(function(httpResponse){ - expect(httpResponse.status).toBe(301); - done(); - }, function(){ - fail("should not fail"); - done(); - }) + url: httpRequestServer + '/301', + }).then( + function(httpResponse) { + expect(httpResponse.status).toBe(301); + done(); + }, + function() { + fail('should not fail'); + done(); + } + ); }); - it("should follow redirects when set", (done) => { - + it('should follow redirects when set', done => { httpRequest({ - url: httpRequestServer + "/301", - followRedirects: true - }).then(function(httpResponse){ - expect(httpResponse.status).toBe(200); - expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}')); - expect(httpResponse.text).toEqual('{"response":"OK"}'); - expect(httpResponse.data.response).toEqual("OK"); - done(); - }, function(){ - fail("should not fail"); - done(); - }) + url: httpRequestServer + '/301', + followRedirects: true, + }).then( + function(httpResponse) { + expect(httpResponse.status).toBe(200); + expect(httpResponse.buffer).toEqual(new Buffer('{"response":"OK"}')); + expect(httpResponse.text).toEqual('{"response":"OK"}'); + expect(httpResponse.data.response).toEqual('OK'); + done(); + }, + function() { + fail('should not fail'); + done(); + } + ); }); - it("should fail on 404", (done) => { + it('should fail on 404', done => { let calls = 0; httpRequest({ - url: httpRequestServer + "/404", + url: httpRequestServer + '/404', success: function() { calls++; - fail("should not succeed"); + fail('should not succeed'); done(); }, error: function(httpResponse) { @@ -128,117 +142,130 @@ describe("httpRequest", () => { expect(httpResponse.text).toEqual('NO'); expect(httpResponse.data).toBe(undefined); done(); - } + }, }); - }) + }); - it("should post on echo", (done) => { + it('should post on echo', done => { let calls = 0; httpRequest({ - method: "POST", - url: httpRequestServer + "/echo", + method: 'POST', + url: httpRequestServer + '/echo', body: { - foo: "bar" + foo: 'bar', }, headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + }, + success: function() { + calls++; + }, + error: function() { + calls++; + }, + }).then( + function(httpResponse) { + expect(calls).toBe(1); + expect(httpResponse.status).toBe(200); + expect(httpResponse.data).toEqual({ foo: 'bar' }); + done(); }, - success: function() { calls++; }, - error: function() { calls++; } - }).then(function(httpResponse){ - expect(calls).toBe(1); - expect(httpResponse.status).toBe(200); - expect(httpResponse.data).toEqual({foo: "bar"}); - done(); - }, function(){ - fail("should not fail"); - done(); - }) + function() { + fail('should not fail'); + done(); + } + ); }); - it("should encode a query string body by default", (done) => { + it('should encode a query string body by default', done => { const options = { - body: {"foo": "bar"}, - } + body: { foo: 'bar' }, + }; const result = httpRequest.encodeBody(options); expect(result.body).toEqual('foo=bar'); - expect(result.headers['Content-Type']).toEqual('application/x-www-form-urlencoded'); + expect(result.headers['Content-Type']).toEqual( + 'application/x-www-form-urlencoded' + ); done(); + }); - }) - - it("should encode a JSON body", (done) => { + it('should encode a JSON body', done => { const options = { - body: {"foo": "bar"}, - headers: {'Content-Type': 'application/json'} - } + body: { foo: 'bar' }, + headers: { 'Content-Type': 'application/json' }, + }; const result = httpRequest.encodeBody(options); expect(result.body).toEqual('{"foo":"bar"}'); done(); - - }) - it("should encode a www-form body", (done) => { + }); + it('should encode a www-form body', done => { const options = { - body: {"foo": "bar", "bar": "baz"}, - headers: {'cOntent-tYpe': 'application/x-www-form-urlencoded'} - } + body: { foo: 'bar', bar: 'baz' }, + headers: { 'cOntent-tYpe': 'application/x-www-form-urlencoded' }, + }; const result = httpRequest.encodeBody(options); - expect(result.body).toEqual("foo=bar&bar=baz"); + expect(result.body).toEqual('foo=bar&bar=baz'); done(); }); - it("should not encode a wrong content type", (done) => { + it('should not encode a wrong content type', done => { const options = { - body:{"foo": "bar", "bar": "baz"}, - headers: {'cOntent-tYpe': 'mime/jpeg'} - } + body: { foo: 'bar', bar: 'baz' }, + headers: { 'cOntent-tYpe': 'mime/jpeg' }, + }; const result = httpRequest.encodeBody(options); - expect(result.body).toEqual({"foo": "bar", "bar": "baz"}); + expect(result.body).toEqual({ foo: 'bar', bar: 'baz' }); done(); }); - it("should fail gracefully", (done) => { + it('should fail gracefully', done => { httpRequest({ - url: "http://not a good url", + url: 'http://not a good url', success: function() { - fail("should not succeed"); + fail('should not succeed'); done(); }, error: function(error) { expect(error).not.toBeUndefined(); expect(error).not.toBeNull(); done(); - } + }, }); }); - it("should params object to query string", (done) => { + it('should params object to query string', done => { httpRequest({ - url: httpRequestServer + "/qs", + url: httpRequestServer + '/qs', params: { - foo: "bar" + foo: 'bar', + }, + }).then( + function(httpResponse) { + expect(httpResponse.status).toBe(200); + expect(httpResponse.data).toEqual({ foo: 'bar' }); + done(); + }, + function() { + fail('should not fail'); + done(); } - }).then(function(httpResponse){ - expect(httpResponse.status).toBe(200); - expect(httpResponse.data).toEqual({foo: "bar"}); - done(); - }, function(){ - fail("should not fail"); - done(); - }) + ); }); - it("should params string to query string", (done) => { + it('should params string to query string', done => { httpRequest({ - url: httpRequestServer + "/qs", - params: "foo=bar&foo2=bar2" - }).then(function(httpResponse){ - expect(httpResponse.status).toBe(200); - expect(httpResponse.data).toEqual({foo: "bar", foo2: 'bar2'}); - done(); - }, function(){ - fail("should not fail"); - done(); - }) + url: httpRequestServer + '/qs', + params: 'foo=bar&foo2=bar2', + }).then( + function(httpResponse) { + expect(httpResponse.status).toBe(200); + expect(httpResponse.data).toEqual({ foo: 'bar', foo2: 'bar2' }); + done(); + }, + function() { + fail('should not fail'); + done(); + } + ); }); it('should not crash with undefined body', () => { @@ -263,17 +290,17 @@ describe("httpRequest", () => { }); it('serialized httpResponse correctly with body object', () => { - const httpResponse = new HTTPResponse({}, {foo: "bar"}); + const httpResponse = new HTTPResponse({}, { foo: 'bar' }); Parse._encode(httpResponse); const serialized = JSON.stringify(httpResponse); const result = JSON.parse(serialized); expect(httpResponse.text).toEqual('{"foo":"bar"}'); - expect(httpResponse.data).toEqual({foo: 'bar'}); - expect(httpResponse.body).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.data).toEqual({ foo: 'bar' }); expect(result.body).toEqual(undefined); }); @@ -294,15 +321,17 @@ describe("httpRequest", () => { const serialized = JSON.stringify(httpResponse); const result = JSON.parse(serialized); expect(result.text).toEqual('{"foo":"bar"}'); - expect(result.data).toEqual({foo: 'bar'}); + expect(result.data).toEqual({ foo: 'bar' }); }); it('serialized httpResponse with Parse._encode should be allright', () => { const json = '{"foo":"bar"}'; const httpResponse = new HTTPResponse({}, new Buffer(json)); const encoded = Parse._encode(httpResponse); - let foundData, foundText, foundBody = false; - for(const key in encoded) { + let foundData, + foundText, + foundBody = false; + for (const key in encoded) { if (key == 'data') { foundData = true; } @@ -317,5 +346,4 @@ describe("httpRequest", () => { expect(foundText).toBe(true); expect(foundBody).toBe(false); }); - }); diff --git a/spec/InMemoryCache.spec.js b/spec/InMemoryCache.spec.js index c8f015497a..d3aeeb03be 100644 --- a/spec/InMemoryCache.spec.js +++ b/spec/InMemoryCache.spec.js @@ -1,26 +1,24 @@ const InMemoryCache = require('../lib/Adapters/Cache/InMemoryCache').default; - describe('InMemoryCache', function() { const BASE_TTL = { - ttl: 100 + ttl: 100, }; const NO_EXPIRE_TTL = { - ttl: NaN + ttl: NaN, }; const KEY = 'hello'; const KEY_2 = KEY + '_2'; const VALUE = 'world'; - function wait(sleep) { return new Promise(function(resolve) { setTimeout(resolve, sleep); - }) + }); } - it('should destroy a expire items in the cache', (done) => { + it('should destroy a expire items in the cache', done => { const cache = new InMemoryCache(BASE_TTL); cache.put(KEY, VALUE); @@ -29,13 +27,13 @@ describe('InMemoryCache', function() { expect(value).toEqual(VALUE); wait(BASE_TTL.ttl * 10).then(() => { - value = cache.get(KEY) + value = cache.get(KEY); expect(value).toEqual(null); done(); }); }); - it('should delete items', (done) => { + it('should delete items', done => { const cache = new InMemoryCache(NO_EXPIRE_TTL); cache.put(KEY, VALUE); cache.put(KEY_2, VALUE); @@ -52,7 +50,7 @@ describe('InMemoryCache', function() { done(); }); - it('should clear all items', (done) => { + it('should clear all items', done => { const cache = new InMemoryCache(NO_EXPIRE_TTL); cache.put(KEY, VALUE); cache.put(KEY_2, VALUE); @@ -70,5 +68,4 @@ describe('InMemoryCache', function() { const cache = new InMemoryCache({}); expect(cache.ttl).toEqual(5 * 1000); }); - }); diff --git a/spec/InMemoryCacheAdapter.spec.js b/spec/InMemoryCacheAdapter.spec.js index 9bf49f087c..e03527822e 100644 --- a/spec/InMemoryCacheAdapter.spec.js +++ b/spec/InMemoryCacheAdapter.spec.js @@ -1,4 +1,5 @@ -const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter').default; +const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter') + .default; describe('InMemoryCacheAdapter', function() { const KEY = 'hello'; @@ -7,12 +8,12 @@ describe('InMemoryCacheAdapter', function() { function wait(sleep) { return new Promise(function(resolve) { setTimeout(resolve, sleep); - }) + }); } - it('should expose promisifyed methods', (done) => { + it('should expose promisifyed methods', done => { const cache = new InMemoryCacheAdapter({ - ttl: NaN + ttl: NaN, }); // Verify all methods return promises. @@ -20,38 +21,39 @@ describe('InMemoryCacheAdapter', function() { cache.put(KEY, VALUE), cache.del(KEY), cache.get(KEY), - cache.clear() + cache.clear(), ]).then(() => { done(); }); }); - it('should get/set/clear', (done) => { + it('should get/set/clear', done => { const cache = new InMemoryCacheAdapter({ - ttl: NaN + ttl: NaN, }); - cache.put(KEY, VALUE) + cache + .put(KEY, VALUE) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(VALUE)) + .then(value => expect(value).toEqual(VALUE)) .then(() => cache.clear()) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(null)) + .then(value => expect(value).toEqual(null)) .then(done); }); - it('should expire after ttl', (done) => { + it('should expire after ttl', done => { const cache = new InMemoryCacheAdapter({ - ttl: 10 + ttl: 10, }); - cache.put(KEY, VALUE) + cache + .put(KEY, VALUE) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(VALUE)) + .then(value => expect(value).toEqual(VALUE)) .then(wait.bind(null, 50)) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(null)) + .then(value => expect(value).toEqual(null)) .then(done); - }) - + }); }); diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 7337096735..18103762e6 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -1,56 +1,71 @@ const auth = require('../lib/Auth'); const Config = require('../lib/Config'); const rest = require('../lib/rest'); -const InstallationsRouter = require('../lib/Routers/InstallationsRouter').InstallationsRouter; +const InstallationsRouter = require('../lib/Routers/InstallationsRouter') + .InstallationsRouter; describe('InstallationsRouter', () => { - it('uses find condition from request.body', (done) => { + it('uses find condition from request.body', done => { const config = Config.get('test'); const androidDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abc', - 'deviceType': 'android' + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', }; const iosDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abd', - 'deviceType': 'ios' + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', }; const request = { config: config, auth: auth.master(config), body: { where: { - deviceType: 'android' - } + deviceType: 'android', + }, }, query: {}, - info: {} + info: {}, }; const router = new InstallationsRouter(); - rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + rest + .create( + config, + auth.nobody(config), + '_Installation', + androidDeviceRequest + ) + .then(() => { + return rest.create( + config, + auth.nobody(config), + '_Installation', + iosDeviceRequest + ); + }) .then(() => { - return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); - }).then(() => { return router.handleFind(request); - }).then((res) => { + }) + .then(res => { const results = res.response.results; expect(results.length).toEqual(1); done(); - }).catch((err) => { + }) + .catch(err => { fail(JSON.stringify(err)); done(); }); }); - it('uses find condition from request.query', (done) => { + it('uses find condition from request.query', done => { const config = Config.get('test'); const androidDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abc', - 'deviceType': 'android' + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', }; const iosDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abd', - 'deviceType': 'ios' + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', }; const request = { config: config, @@ -58,60 +73,88 @@ describe('InstallationsRouter', () => { body: {}, query: { where: { - deviceType: 'android' - } + deviceType: 'android', + }, }, - info: {} + info: {}, }; const router = new InstallationsRouter(); - rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + rest + .create( + config, + auth.nobody(config), + '_Installation', + androidDeviceRequest + ) + .then(() => { + return rest.create( + config, + auth.nobody(config), + '_Installation', + iosDeviceRequest + ); + }) .then(() => { - return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); - }).then(() => { return router.handleFind(request); - }).then((res) => { + }) + .then(res => { const results = res.response.results; expect(results.length).toEqual(1); done(); - }).catch((err) => { + }) + .catch(err => { jfail(err); done(); }); }); - it('query installations with limit = 0', (done) => { + it('query installations with limit = 0', done => { const config = Config.get('test'); const androidDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abc', - 'deviceType': 'android' + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', }; const iosDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abd', - 'deviceType': 'ios' + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', }; const request = { config: config, auth: auth.master(config), body: {}, query: { - limit: 0 + limit: 0, }, - info: {} + info: {}, }; Config.get('test'); const router = new InstallationsRouter(); - rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + rest + .create( + config, + auth.nobody(config), + '_Installation', + androidDeviceRequest + ) + .then(() => { + return rest.create( + config, + auth.nobody(config), + '_Installation', + iosDeviceRequest + ); + }) .then(() => { - return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); - }).then(() => { return router.handleFind(request); - }).then((res) => { + }) + .then(res => { const response = res.response; expect(response.results.length).toEqual(0); done(); - }).catch((err) => { + }) + .catch(err => { fail(JSON.stringify(err)); done(); }); @@ -120,28 +163,41 @@ describe('InstallationsRouter', () => { it('query installations with count = 1', done => { const config = Config.get('test'); const androidDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abc', - 'deviceType': 'android' + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', }; const iosDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abd', - 'deviceType': 'ios' + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', }; const request = { config: config, auth: auth.master(config), body: {}, query: { - count: 1 + count: 1, }, - info: {} + info: {}, }; const router = new InstallationsRouter(); - rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) - .then(() => rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest)) + rest + .create( + config, + auth.nobody(config), + '_Installation', + androidDeviceRequest + ) + .then(() => + rest.create( + config, + auth.nobody(config), + '_Installation', + iosDeviceRequest + ) + ) .then(() => router.handleFind(request)) - .then((res) => { + .then(res => { const response = res.response; expect(response.results.length).toEqual(2); expect(response.count).toEqual(2); @@ -150,18 +206,18 @@ describe('InstallationsRouter', () => { .catch(error => { fail(JSON.stringify(error)); done(); - }) + }); }); - it('query installations with limit = 0 and count = 1', (done) => { + it('query installations with limit = 0 and count = 1', done => { const config = Config.get('test'); const androidDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abc', - 'deviceType': 'android' + installationId: '12345678-abcd-abcd-abcd-123456789abc', + deviceType: 'android', }; const iosDeviceRequest = { - 'installationId': '12345678-abcd-abcd-abcd-123456789abd', - 'deviceType': 'ios' + installationId: '12345678-abcd-abcd-abcd-123456789abd', + deviceType: 'ios', }; const request = { config: config, @@ -169,23 +225,37 @@ describe('InstallationsRouter', () => { body: {}, query: { limit: 0, - count: 1 + count: 1, }, - info: {} + info: {}, }; const router = new InstallationsRouter(); - rest.create(config, auth.nobody(config), '_Installation', androidDeviceRequest) + rest + .create( + config, + auth.nobody(config), + '_Installation', + androidDeviceRequest + ) + .then(() => { + return rest.create( + config, + auth.nobody(config), + '_Installation', + iosDeviceRequest + ); + }) .then(() => { - return rest.create(config, auth.nobody(config), '_Installation', iosDeviceRequest); - }).then(() => { return router.handleFind(request); - }).then((res) => { + }) + .then(res => { const response = res.response; expect(response.results.length).toEqual(0); expect(response.count).toEqual(2); done(); - }).catch((err) => { + }) + .catch(err => { fail(JSON.stringify(err)); done(); }); diff --git a/spec/JobSchedule.spec.js b/spec/JobSchedule.spec.js index def48e180f..ce14a16894 100644 --- a/spec/JobSchedule.spec.js +++ b/spec/JobSchedule.spec.js @@ -1,122 +1,146 @@ const rp = require('request-promise'); const defaultHeaders = { 'X-Parse-Application-Id': 'test', - 'X-Parse-Rest-API-Key': 'rest' -} + 'X-Parse-Rest-API-Key': 'rest', +}; const masterKeyHeaders = { 'X-Parse-Application-Id': 'test', 'X-Parse-Rest-API-Key': 'rest', - 'X-Parse-Master-Key': 'test' -} + 'X-Parse-Master-Key': 'test', +}; const defaultOptions = { headers: defaultHeaders, - json: true -} + json: true, +}; const masterKeyOptions = { headers: masterKeyHeaders, - json: true -} + json: true, +}; describe('JobSchedule', () => { - it('should create _JobSchedule with masterKey', (done) => { + it('should create _JobSchedule with masterKey', done => { const jobSchedule = new Parse.Object('_JobSchedule'); jobSchedule.set({ - 'jobName': 'MY Cool Job' + jobName: 'MY Cool Job', }); - jobSchedule.save(null, {useMasterKey: true}).then(() => { - done(); - }) + jobSchedule + .save(null, { useMasterKey: true }) + .then(() => { + done(); + }) .catch(done.fail); }); - it('should fail creating _JobSchedule without masterKey', (done) => { + it('should fail creating _JobSchedule without masterKey', done => { const jobSchedule = new Parse.Object('_JobSchedule'); jobSchedule.set({ - 'jobName': 'SomeJob' + jobName: 'SomeJob', }); - jobSchedule.save(null).then(done.fail) + jobSchedule + .save(null) + .then(done.fail) .catch(() => done()); }); - it('should reject access when not using masterKey (/jobs)', (done) => { - rp.get(Parse.serverURL + '/cloud_code/jobs', defaultOptions).then(done.fail, () => done()); + it('should reject access when not using masterKey (/jobs)', done => { + rp.get(Parse.serverURL + '/cloud_code/jobs', defaultOptions).then( + done.fail, + () => done() + ); }); - it('should reject access when not using masterKey (/jobs/data)', (done) => { - rp.get(Parse.serverURL + '/cloud_code/jobs/data', defaultOptions).then(done.fail, () => done()); + it('should reject access when not using masterKey (/jobs/data)', done => { + rp.get(Parse.serverURL + '/cloud_code/jobs/data', defaultOptions).then( + done.fail, + () => done() + ); }); - it('should reject access when not using masterKey (PUT /jobs/id)', (done) => { - rp.put(Parse.serverURL + '/cloud_code/jobs/jobId', defaultOptions).then(done.fail, () => done()); + it('should reject access when not using masterKey (PUT /jobs/id)', done => { + rp.put(Parse.serverURL + '/cloud_code/jobs/jobId', defaultOptions).then( + done.fail, + () => done() + ); }); - it('should reject access when not using masterKey (DELETE /jobs/id)', (done) => { - rp.del(Parse.serverURL + '/cloud_code/jobs/jobId', defaultOptions).then(done.fail, () => done()); + it('should reject access when not using masterKey (DELETE /jobs/id)', done => { + rp.del(Parse.serverURL + '/cloud_code/jobs/jobId', defaultOptions).then( + done.fail, + () => done() + ); }); - it('should allow access when using masterKey (GET /jobs)', (done) => { - rp.get(Parse.serverURL + '/cloud_code/jobs', masterKeyOptions).then(done, done.fail); + it('should allow access when using masterKey (GET /jobs)', done => { + rp.get(Parse.serverURL + '/cloud_code/jobs', masterKeyOptions).then( + done, + done.fail + ); }); - it('should create a job schedule', (done) => { + it('should create a job schedule', done => { Parse.Cloud.job('job', () => {}); const options = Object.assign({}, masterKeyOptions, { body: { job_schedule: { - jobName: 'job' - } - } + jobName: 'job', + }, + }, }); - rp.post(Parse.serverURL + '/cloud_code/jobs', options).then((res) => { - expect(res.objectId).not.toBeUndefined(); - }) + rp.post(Parse.serverURL + '/cloud_code/jobs', options) + .then(res => { + expect(res.objectId).not.toBeUndefined(); + }) .then(() => { return rp.get(Parse.serverURL + '/cloud_code/jobs', masterKeyOptions); }) - .then((res) => { + .then(res => { expect(res.length).toBe(1); }) .then(done) .catch(done.fail); }); - it('should fail creating a job with an invalid name', (done) => { + it('should fail creating a job with an invalid name', done => { const options = Object.assign({}, masterKeyOptions, { body: { job_schedule: { - jobName: 'job' - } - } + jobName: 'job', + }, + }, }); rp.post(Parse.serverURL + '/cloud_code/jobs', options) .then(done.fail) .catch(() => done()); }); - it('should update a job', (done) => { + it('should update a job', done => { Parse.Cloud.job('job1', () => {}); Parse.Cloud.job('job2', () => {}); const options = Object.assign({}, masterKeyOptions, { body: { job_schedule: { - jobName: 'job1' - } - } + jobName: 'job1', + }, + }, }); - rp.post(Parse.serverURL + '/cloud_code/jobs', options).then((res) => { - expect(res.objectId).not.toBeUndefined(); - return rp.put(Parse.serverURL + '/cloud_code/jobs/' + res.objectId, Object.assign(options, { - body: { - job_schedule: { - jobName: 'job2' - } - } - })); - }) + rp.post(Parse.serverURL + '/cloud_code/jobs', options) + .then(res => { + expect(res.objectId).not.toBeUndefined(); + return rp.put( + Parse.serverURL + '/cloud_code/jobs/' + res.objectId, + Object.assign(options, { + body: { + job_schedule: { + jobName: 'job2', + }, + }, + }) + ); + }) .then(() => { return rp.get(Parse.serverURL + '/cloud_code/jobs', masterKeyOptions); }) - .then((res) => { + .then(res => { expect(res.length).toBe(1); expect(res[0].jobName).toBe('job2'); }) @@ -124,69 +148,81 @@ describe('JobSchedule', () => { .catch(done.fail); }); - it('should fail updating a job with an invalid name', (done) => { + it('should fail updating a job with an invalid name', done => { Parse.Cloud.job('job1', () => {}); const options = Object.assign({}, masterKeyOptions, { body: { job_schedule: { - jobName: 'job1' - } - } + jobName: 'job1', + }, + }, }); - rp.post(Parse.serverURL + '/cloud_code/jobs', options).then((res) => { - expect(res.objectId).not.toBeUndefined(); - return rp.put(Parse.serverURL + '/cloud_code/jobs/' + res.objectId, Object.assign(options, { - body: { - job_schedule: { - jobName: 'job2' - } - } - })); - }) + rp.post(Parse.serverURL + '/cloud_code/jobs', options) + .then(res => { + expect(res.objectId).not.toBeUndefined(); + return rp.put( + Parse.serverURL + '/cloud_code/jobs/' + res.objectId, + Object.assign(options, { + body: { + job_schedule: { + jobName: 'job2', + }, + }, + }) + ); + }) .then(done.fail) .catch(() => done()); }); - it('should destroy a job', (done) => { + it('should destroy a job', done => { Parse.Cloud.job('job', () => {}); const options = Object.assign({}, masterKeyOptions, { body: { job_schedule: { - jobName: 'job' - } - } + jobName: 'job', + }, + }, }); - rp.post(Parse.serverURL + '/cloud_code/jobs', options).then((res) => { - expect(res.objectId).not.toBeUndefined(); - return rp.del(Parse.serverURL + '/cloud_code/jobs/' + res.objectId, masterKeyOptions); - }) + rp.post(Parse.serverURL + '/cloud_code/jobs', options) + .then(res => { + expect(res.objectId).not.toBeUndefined(); + return rp.del( + Parse.serverURL + '/cloud_code/jobs/' + res.objectId, + masterKeyOptions + ); + }) .then(() => { return rp.get(Parse.serverURL + '/cloud_code/jobs', masterKeyOptions); }) - .then((res) => { + .then(res => { expect(res.length).toBe(0); }) .then(done) .catch(done.fail); }); - it('should properly return job data', (done) => { + it('should properly return job data', done => { Parse.Cloud.job('job1', () => {}); Parse.Cloud.job('job2', () => {}); const options = Object.assign({}, masterKeyOptions, { body: { job_schedule: { - jobName: 'job1' - } - } + jobName: 'job1', + }, + }, }); - rp.post(Parse.serverURL + '/cloud_code/jobs', options).then((res) => { - expect(res.objectId).not.toBeUndefined(); - }) + rp.post(Parse.serverURL + '/cloud_code/jobs', options) + .then(res => { + expect(res.objectId).not.toBeUndefined(); + }) .then(() => { - return rp.get(Parse.serverURL + '/cloud_code/jobs/data', masterKeyOptions); + return rp.get( + Parse.serverURL + '/cloud_code/jobs/data', + masterKeyOptions + ); }) - .then((res) => { + .then(res => { expect(res.in_use).toEqual(['job1']); expect(res.jobs).toContain('job1'); expect(res.jobs).toContain('job2'); diff --git a/spec/Logger.spec.js b/spec/Logger.spec.js index bd268fa78e..d95c0ab105 100644 --- a/spec/Logger.spec.js +++ b/spec/Logger.spec.js @@ -9,8 +9,8 @@ class TestTransport extends winston.Transport { describe('Logger', () => { it('should add transport', () => { - const testTransport = new (TestTransport)({ - name: 'test' + const testTransport = new TestTransport({ + name: 'test', }); spyOn(testTransport, 'log'); logging.addTransport(testTransport); @@ -21,7 +21,7 @@ describe('Logger', () => { expect(Object.keys(logging.logger.transports).length).toBe(3); }); - it('should have files transports', (done) => { + it('should have files transports', done => { reconfigureServer().then(() => { const transports = logging.logger.transports; const transportKeys = Object.keys(transports); @@ -30,9 +30,9 @@ describe('Logger', () => { }); }); - it('should disable files logs', (done) => { + it('should disable files logs', done => { reconfigureServer({ - logsFolder: null + logsFolder: null, }).then(() => { const transports = logging.logger.transports; const transportKeys = Object.keys(transports); @@ -41,23 +41,27 @@ describe('Logger', () => { }); }); - it('should enable JSON logs', (done) => { + it('should enable JSON logs', done => { // Force console transport reconfigureServer({ logsFolder: null, jsonLogs: true, - silent: false - }).then(() => { - spyOn(process.stdout, 'write'); - logging.logger.info('hi', {key: 'value'}); - expect(process.stdout.write).toHaveBeenCalled(); - const firstLog = process.stdout.write.calls.first().args[0]; - expect(firstLog).toEqual(JSON.stringify({key: 'value', level: 'info', message: 'hi' }) + '\n'); - return reconfigureServer({ - jsonLogs: false + silent: false, + }) + .then(() => { + spyOn(process.stdout, 'write'); + logging.logger.info('hi', { key: 'value' }); + expect(process.stdout.write).toHaveBeenCalled(); + const firstLog = process.stdout.write.calls.first().args[0]; + expect(firstLog).toEqual( + JSON.stringify({ key: 'value', level: 'info', message: 'hi' }) + '\n' + ); + return reconfigureServer({ + jsonLogs: false, + }); + }) + .then(() => { + done(); }); - }).then(() => { - done(); - }); }); }); diff --git a/spec/LoggerController.spec.js b/spec/LoggerController.spec.js index 36306b662a..7b983d319e 100644 --- a/spec/LoggerController.spec.js +++ b/spec/LoggerController.spec.js @@ -1,33 +1,40 @@ -const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; +const LoggerController = require('../lib/Controllers/LoggerController') + .LoggerController; +const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') + .WinstonLoggerAdapter; describe('LoggerController', () => { - it('can process an empty query without throwing', (done) => { + it('can process an empty query without throwing', done => { // Make mock request const query = {}; const loggerController = new LoggerController(new WinstonLoggerAdapter()); expect(() => { - loggerController.getLogs(query).then(function(res) { - expect(res.length).not.toBe(0); - done(); - }).catch((err) => { - jfail(err); - done(); - }) + loggerController + .getLogs(query) + .then(function(res) { + expect(res.length).not.toBe(0); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }).not.toThrow(); }); - it('properly validates dateTimes', (done) => { + it('properly validates dateTimes', done => { expect(LoggerController.validDateTime()).toBe(null); - expect(LoggerController.validDateTime("String")).toBe(null); + expect(LoggerController.validDateTime('String')).toBe(null); expect(LoggerController.validDateTime(123456).getTime()).toBe(123456); - expect(LoggerController.validDateTime("2016-01-01Z00:00:00").getTime()).toBe(1451606400000); + expect( + LoggerController.validDateTime('2016-01-01Z00:00:00').getTime() + ).toBe(1451606400000); done(); }); - it('can set the proper default values', (done) => { + it('can set the proper default values', done => { // Make mock request const result = LoggerController.parseOptions(); expect(result.size).toEqual(10); @@ -37,14 +44,14 @@ describe('LoggerController', () => { done(); }); - it('can process an ascending query without throwing', (done) => { + it('can process an ascending query without throwing', done => { // Make mock request const query = { - from: "2016-01-01Z00:00:00", - until: "2016-01-01Z00:00:00", + from: '2016-01-01Z00:00:00', + until: '2016-01-01Z00:00:00', size: 5, order: 'asc', - level: 'error' + level: 'error', }; const result = LoggerController.parseOptions(query); @@ -58,50 +65,53 @@ describe('LoggerController', () => { done(); }); - it('can process a descending query without throwing', (done) => { + it('can process a descending query without throwing', done => { // Make mock request const query = { - from: "2016-01-01", - until: "2016-01-30", + from: '2016-01-01', + until: '2016-01-30', size: 5, order: 'desc', - level: 'error' + level: 'error', }; const loggerController = new LoggerController(new WinstonLoggerAdapter()); expect(() => { - loggerController.getLogs(query).then(function(res) { - expect(res.length).toBe(0); - done(); - }).catch((err) => { - jfail(err); - fail("should not fail"); - done(); - }) + loggerController + .getLogs(query) + .then(function(res) { + expect(res.length).toBe(0); + done(); + }) + .catch(err => { + jfail(err); + fail('should not fail'); + done(); + }); }).not.toThrow(); }); - it('should throw without an adapter', (done) => { + it('should throw without an adapter', done => { expect(() => { new LoggerController(); }).toThrow(); done(); }); - it('should replace implementations with verbose', (done) => { + it('should replace implementations with verbose', done => { const adapter = new WinstonLoggerAdapter(); - const logger = new LoggerController(adapter, null, {verbose: true }); - spyOn(adapter, "log"); + const logger = new LoggerController(adapter, null, { verbose: true }); + spyOn(adapter, 'log'); logger.silly('yo!'); expect(adapter.log).not.toHaveBeenCalled(); done(); }); - it('should replace implementations with logLevel', (done) => { + it('should replace implementations with logLevel', done => { const adapter = new WinstonLoggerAdapter(); const logger = new LoggerController(adapter, null, { logLevel: 'error' }); - spyOn(adapter, "log"); + spyOn(adapter, 'log'); logger.warn('yo!'); logger.info('yo!'); logger.debug('yo!'); diff --git a/spec/LogsRouter.spec.js b/spec/LogsRouter.spec.js index 388079dee1..37a111a84d 100644 --- a/spec/LogsRouter.spec.js +++ b/spec/LogsRouter.spec.js @@ -2,22 +2,24 @@ const request = require('request'); const LogsRouter = require('../lib/Routers/LogsRouter').LogsRouter; -const LoggerController = require('../lib/Controllers/LoggerController').LoggerController; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; +const LoggerController = require('../lib/Controllers/LoggerController') + .LoggerController; +const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') + .WinstonLoggerAdapter; const loggerController = new LoggerController(new WinstonLoggerAdapter()); describe('LogsRouter', () => { - it('can check valid master key of request', (done) => { + it('can check valid master key of request', done => { // Make mock request const request = { auth: { - isMaster: true + isMaster: true, }, query: {}, config: { - loggerController: loggerController - } + loggerController: loggerController, + }, }; const router = new LogsRouter(); @@ -28,16 +30,16 @@ describe('LogsRouter', () => { done(); }); - it('can check invalid construction of controller', (done) => { + it('can check invalid construction of controller', done => { // Make mock request const request = { auth: { - isMaster: true + isMaster: true, }, query: {}, config: { - loggerController: undefined // missing controller - } + loggerController: undefined, // missing controller + }, }; const router = new LogsRouter(); @@ -49,24 +51,27 @@ describe('LogsRouter', () => { }); it('can check invalid master key of request', done => { - request.get({ - url: 'http://localhost:8378/1/scriptlog', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + request.get( + { + url: 'http://localhost:8378/1/scriptlog', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized: master key is required'); + done(); } - }, (error, response, body) => { - expect(response.statusCode).toEqual(403); - expect(body.error).toEqual('unauthorized: master key is required'); - done(); - }); + ); }); const headers = { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Master-Key': 'test' + 'X-Parse-Master-Key': 'test', }; /** @@ -74,24 +79,35 @@ describe('LogsRouter', () => { */ it('does scrub simple passwords on GET login', done => { reconfigureServer({ - verbose: true + verbose: true, }).then(function() { - request.get({ - headers: headers, - url: 'http://localhost:8378/1/login?username=test&password=simplepass.com' - }, () => { - request.get({ - url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', - json: true, - headers: headers - }, (error, response, body) => { - expect(response.statusCode).toEqual(200); - // 4th entry is our actual GET request - expect(body[2].url).toEqual('/1/login?username=test&password=********'); - expect(body[2].message).toEqual('REQUEST for [GET] /1/login?username=test&password=********: {}'); - done(); - }); - }); + request.get( + { + headers: headers, + url: + 'http://localhost:8378/1/login?username=test&password=simplepass.com', + }, + () => { + request.get( + { + url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', + json: true, + headers: headers, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(200); + // 4th entry is our actual GET request + expect(body[2].url).toEqual( + '/1/login?username=test&password=********' + ); + expect(body[2].message).toEqual( + 'REQUEST for [GET] /1/login?username=test&password=********: {}' + ); + done(); + } + ); + } + ); }); }); @@ -100,25 +116,36 @@ describe('LogsRouter', () => { */ it('does scrub complex passwords on GET login', done => { reconfigureServer({ - verbose: true + verbose: true, }).then(function() { - request.get({ - headers: headers, - // using urlencoded password, 'simple @,/?:&=+$#pass.com' - url: 'http://localhost:8378/1/login?username=test&password=simple%20%40%2C%2F%3F%3A%26%3D%2B%24%23pass.com' - }, () => { - request.get({ - url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', - json: true, - headers: headers - }, (error, response, body) => { - expect(response.statusCode).toEqual(200); - // 4th entry is our actual GET request - expect(body[2].url).toEqual('/1/login?username=test&password=********'); - expect(body[2].message).toEqual('REQUEST for [GET] /1/login?username=test&password=********: {}'); - done(); - }); - }); + request.get( + { + headers: headers, + // using urlencoded password, 'simple @,/?:&=+$#pass.com' + url: + 'http://localhost:8378/1/login?username=test&password=simple%20%40%2C%2F%3F%3A%26%3D%2B%24%23pass.com', + }, + () => { + request.get( + { + url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', + json: true, + headers: headers, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(200); + // 4th entry is our actual GET request + expect(body[2].url).toEqual( + '/1/login?username=test&password=********' + ); + expect(body[2].message).toEqual( + 'REQUEST for [GET] /1/login?username=test&password=********: {}' + ); + done(); + } + ); + } + ); }); }); @@ -127,28 +154,36 @@ describe('LogsRouter', () => { */ it('does not have password field in POST login', done => { reconfigureServer({ - verbose: true + verbose: true, }).then(function() { - request.post({ - headers: headers, - url: 'http://localhost:8378/1/login', - data: { - username: 'test', - password: 'simplepass.com' + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/login', + data: { + username: 'test', + password: 'simplepass.com', + }, + }, + () => { + request.get( + { + url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', + json: true, + headers: headers, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(200); + // 4th entry is our actual GET request + expect(body[2].url).toEqual('/1/login'); + expect(body[2].message).toEqual( + 'REQUEST for [POST] /1/login: {}' + ); + done(); + } + ); } - }, () => { - request.get({ - url: 'http://localhost:8378/1/scriptlog?size=4&level=verbose', - json: true, - headers: headers - }, (error, response, body) => { - expect(response.statusCode).toEqual(200); - // 4th entry is our actual GET request - expect(body[2].url).toEqual('/1/login'); - expect(body[2].message).toEqual('REQUEST for [POST] /1/login: {}'); - done(); - }); - }); + ); }); }); }); diff --git a/spec/Middlewares.spec.js b/spec/Middlewares.spec.js index 32e44a3cc8..2ff8467731 100644 --- a/spec/Middlewares.spec.js +++ b/spec/Middlewares.spec.js @@ -2,7 +2,6 @@ const middlewares = require('../lib/middlewares'); const AppCache = require('../lib/cache').AppCache; describe('middlewares', () => { - let fakeReq, fakeRes; beforeEach(() => { @@ -10,12 +9,12 @@ describe('middlewares', () => { originalUrl: 'http://example.com/parse/', url: 'http://example.com/', body: { - _ApplicationId: 'FakeAppId' + _ApplicationId: 'FakeAppId', }, headers: {}, - get: (key) => { - return fakeReq.headers[key.toLowerCase()] - } + get: key => { + return fakeReq.headers[key.toLowerCase()]; + }, }; fakeRes = jasmine.createSpyObj('fakeRes', ['end', 'status']); AppCache.put(fakeReq.body._ApplicationId, {}); @@ -25,21 +24,21 @@ describe('middlewares', () => { AppCache.del(fakeReq.body._ApplicationId); }); - it('should use _ContentType if provided', (done) => { + it('should use _ContentType if provided', done => { expect(fakeReq.headers['content-type']).toEqual(undefined); const 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() + done(); }); }); it('should give invalid response when keys are configured but no key supplied', () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - restAPIKey: 'restAPIKey' + restAPIKey: 'restAPIKey', }); middlewares.handleParseHeaders(fakeReq, fakeRes); expect(fakeRes.status).toHaveBeenCalledWith(403); @@ -48,7 +47,7 @@ describe('middlewares', () => { it('should give invalid response when keys are configured but supplied key is incorrect', () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - restAPIKey: 'restAPIKey' + restAPIKey: 'restAPIKey', }); fakeReq.headers['x-parse-rest-api-key'] = 'wrongKey'; middlewares.handleParseHeaders(fakeReq, fakeRes); @@ -58,19 +57,18 @@ describe('middlewares', () => { it('should give invalid response when keys are configured but different key is supplied', () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - restAPIKey: 'restAPIKey' + restAPIKey: 'restAPIKey', }); fakeReq.headers['x-parse-client-key'] = 'clientKey'; middlewares.handleParseHeaders(fakeReq, fakeRes); expect(fakeRes.status).toHaveBeenCalledWith(403); }); - - it('should succeed when any one of the configured keys supplied', (done) => { + it('should succeed when any one of the configured keys supplied', done => { AppCache.put(fakeReq.body._ApplicationId, { clientKey: 'clientKey', masterKey: 'masterKey', - restAPIKey: 'restAPIKey' + restAPIKey: 'restAPIKey', }); fakeReq.headers['x-parse-rest-api-key'] = 'restAPIKey'; middlewares.handleParseHeaders(fakeReq, fakeRes, () => { @@ -79,11 +77,11 @@ describe('middlewares', () => { }); }); - it('should succeed when client key supplied but empty', (done) => { + it('should succeed when client key supplied but empty', done => { AppCache.put(fakeReq.body._ApplicationId, { clientKey: '', masterKey: 'masterKey', - restAPIKey: 'restAPIKey' + restAPIKey: 'restAPIKey', }); fakeReq.headers['x-parse-client-key'] = ''; middlewares.handleParseHeaders(fakeReq, fakeRes, () => { @@ -92,9 +90,9 @@ describe('middlewares', () => { }); }); - it('should succeed when no keys are configured and none supplied', (done) => { + it('should succeed when no keys are configured and none supplied', done => { AppCache.put(fakeReq.body._ApplicationId, { - masterKey: 'masterKey' + masterKey: 'masterKey', }); middlewares.handleParseHeaders(fakeReq, fakeRes, () => { expect(fakeRes.status).not.toHaveBeenCalled(); @@ -107,25 +105,27 @@ describe('middlewares', () => { installationId: '_InstallationId', sessionToken: '_SessionToken', masterKey: '_MasterKey', - javascriptKey: '_JavaScriptKey' + javascriptKey: '_JavaScriptKey', }; const BodyKeys = Object.keys(BodyParams); - BodyKeys.forEach((infoKey) => { + 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'); + const otherKeys = BodyKeys.filter( + otherKey => otherKey !== infoKey && otherKey !== 'javascriptKey' + ); - it(`it should pull ${bodyKey} into req.info`, (done) => { + 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) => { + otherKeys.forEach(otherKey => { expect(fakeReq.info[otherKey]).toEqual(undefined); }); @@ -137,7 +137,7 @@ describe('middlewares', () => { it('should not succeed if the ip does not belong to masterKeyIps list', () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1','ip2'] + masterKeyIps: ['ip1', 'ip2'], }); fakeReq.ip = 'ip3'; fakeReq.headers['x-parse-master-key'] = 'masterKey'; @@ -145,14 +145,14 @@ describe('middlewares', () => { expect(fakeRes.status).toHaveBeenCalledWith(403); }); - it('should succeed if the ip does belong to masterKeyIps list', (done) => { + it('should succeed if the ip does belong to masterKeyIps list', done => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1','ip2'] + masterKeyIps: ['ip1', 'ip2'], }); fakeReq.ip = 'ip1'; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes,() => { + middlewares.handleParseHeaders(fakeReq, fakeRes, () => { expect(fakeRes.status).not.toHaveBeenCalled(); done(); }); @@ -161,22 +161,22 @@ describe('middlewares', () => { it('should not succeed if the connection.remoteAddress does not belong to masterKeyIps list', () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1','ip2'] + masterKeyIps: ['ip1', 'ip2'], }); - fakeReq.connection = {remoteAddress : 'ip3'}; + fakeReq.connection = { remoteAddress: 'ip3' }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; middlewares.handleParseHeaders(fakeReq, fakeRes); expect(fakeRes.status).toHaveBeenCalledWith(403); }); - it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', (done) => { + it('should succeed if the connection.remoteAddress does belong to masterKeyIps list', done => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1','ip2'] + masterKeyIps: ['ip1', 'ip2'], }); - fakeReq.connection = {remoteAddress : 'ip1'}; + fakeReq.connection = { remoteAddress: 'ip1' }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes,() => { + middlewares.handleParseHeaders(fakeReq, fakeRes, () => { expect(fakeRes.status).not.toHaveBeenCalled(); done(); }); @@ -185,22 +185,22 @@ describe('middlewares', () => { it('should not succeed if the socket.remoteAddress does not belong to masterKeyIps list', () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1','ip2'] + masterKeyIps: ['ip1', 'ip2'], }); - fakeReq.socket = {remoteAddress : 'ip3'}; + fakeReq.socket = { remoteAddress: 'ip3' }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; middlewares.handleParseHeaders(fakeReq, fakeRes); expect(fakeRes.status).toHaveBeenCalledWith(403); }); - it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', (done) => { + it('should succeed if the socket.remoteAddress does belong to masterKeyIps list', done => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1','ip2'] + masterKeyIps: ['ip1', 'ip2'], }); - fakeReq.socket = {remoteAddress : 'ip1'}; + fakeReq.socket = { remoteAddress: 'ip1' }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes,() => { + middlewares.handleParseHeaders(fakeReq, fakeRes, () => { expect(fakeRes.status).not.toHaveBeenCalled(); done(); }); @@ -209,61 +209,61 @@ describe('middlewares', () => { it('should not succeed if the connection.socket.remoteAddress does not belong to masterKeyIps list', () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1','ip2'] + masterKeyIps: ['ip1', 'ip2'], }); - fakeReq.connection = { socket : {remoteAddress : 'ip3'}}; + fakeReq.connection = { socket: { remoteAddress: 'ip3' } }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; middlewares.handleParseHeaders(fakeReq, fakeRes); expect(fakeRes.status).toHaveBeenCalledWith(403); }); - it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', (done) => { + it('should succeed if the connection.socket.remoteAddress does belong to masterKeyIps list', done => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1','ip2'] + masterKeyIps: ['ip1', 'ip2'], }); - fakeReq.connection = { socket : {remoteAddress : 'ip1'}}; + fakeReq.connection = { socket: { remoteAddress: 'ip1' } }; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes,() => { + middlewares.handleParseHeaders(fakeReq, fakeRes, () => { expect(fakeRes.status).not.toHaveBeenCalled(); done(); }); }); - it('should allow any ip to use masterKey if masterKeyIps is empty', (done) => { + it('should allow any ip to use masterKey if masterKeyIps is empty', done => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: [] + masterKeyIps: [], }); fakeReq.ip = 'ip1'; fakeReq.headers['x-parse-master-key'] = 'masterKey'; - middlewares.handleParseHeaders(fakeReq, fakeRes,() => { + middlewares.handleParseHeaders(fakeReq, fakeRes, () => { expect(fakeRes.status).not.toHaveBeenCalled(); done(); }); }); - it('should succeed if xff header does belong to masterKeyIps', (done) => { + it('should succeed if xff header does belong to masterKeyIps', done => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1'] + masterKeyIps: ['ip1'], }); fakeReq.headers['x-parse-master-key'] = 'masterKey'; fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3'; - middlewares.handleParseHeaders(fakeReq, fakeRes,() => { + middlewares.handleParseHeaders(fakeReq, fakeRes, () => { expect(fakeRes.status).not.toHaveBeenCalled(); done(); }); }); - it('should succeed if xff header with one ip does belong to masterKeyIps', (done) => { + it('should succeed if xff header with one ip does belong to masterKeyIps', done => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1'] + masterKeyIps: ['ip1'], }); fakeReq.headers['x-parse-master-key'] = 'masterKey'; fakeReq.headers['x-forwarded-for'] = 'ip1'; - middlewares.handleParseHeaders(fakeReq, fakeRes,() => { + middlewares.handleParseHeaders(fakeReq, fakeRes, () => { expect(fakeRes.status).not.toHaveBeenCalled(); done(); }); @@ -272,7 +272,7 @@ describe('middlewares', () => { it('should not succeed if xff header does not belong to masterKeyIps', () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip4'] + masterKeyIps: ['ip4'], }); fakeReq.headers['x-parse-master-key'] = 'masterKey'; fakeReq.headers['x-forwarded-for'] = 'ip1, ip2, ip3'; @@ -283,7 +283,7 @@ describe('middlewares', () => { it('should not succeed if xff header is empty and masterKeyIps is set', () => { AppCache.put(fakeReq.body._ApplicationId, { masterKey: 'masterKey', - masterKeyIps: ['ip1'] + masterKeyIps: ['ip1'], }); fakeReq.headers['x-parse-master-key'] = 'masterKey'; fakeReq.headers['x-forwarded-for'] = ''; @@ -295,11 +295,13 @@ describe('middlewares', () => { const headers = {}; const res = { header: (key, value) => { - headers[key] = value - } + headers[key] = value; + }, }; middlewares.allowCrossDomain({}, res, () => {}); expect(Object.keys(headers).length).toBe(4); - expect(headers['Access-Control-Expose-Headers']).toBe('X-Parse-Job-Status-Id, X-Parse-Push-Status-Id'); + expect(headers['Access-Control-Expose-Headers']).toBe( + 'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id' + ); }); }); diff --git a/spec/MockAdapter.js b/spec/MockAdapter.js index c3f557849d..3deaaaae74 100644 --- a/spec/MockAdapter.js +++ b/spec/MockAdapter.js @@ -1,5 +1,5 @@ module.exports = function(options) { return { - options: options + options: options, }; }; diff --git a/spec/MockEmailAdapter.js b/spec/MockEmailAdapter.js index b143e37e6e..295e6c6c91 100644 --- a/spec/MockEmailAdapter.js +++ b/spec/MockEmailAdapter.js @@ -1,5 +1,5 @@ module.exports = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() -} + sendMail: () => Promise.resolve(), +}; diff --git a/spec/MockEmailAdapterWithOptions.js b/spec/MockEmailAdapterWithOptions.js index 5de84d0521..71d23892ef 100644 --- a/spec/MockEmailAdapterWithOptions.js +++ b/spec/MockEmailAdapterWithOptions.js @@ -1,21 +1,21 @@ module.exports = options => { if (!options) { - throw "Options were not provided" + throw 'Options were not provided'; } const adapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() + sendMail: () => Promise.resolve(), }; if (options.sendMail) { - adapter.sendMail = options.sendMail + adapter.sendMail = options.sendMail; } if (options.sendPasswordResetEmail) { - adapter.sendPasswordResetEmail = options.sendPasswordResetEmail + adapter.sendPasswordResetEmail = options.sendPasswordResetEmail; } if (options.sendVerificationEmail) { adapter.sendVerificationEmail = options.sendVerificationEmail; } return adapter; -} +}; diff --git a/spec/MockPushAdapter.js b/spec/MockPushAdapter.js index 3350e9df55..6c7c2e3da1 100644 --- a/spec/MockPushAdapter.js +++ b/spec/MockPushAdapter.js @@ -4,6 +4,6 @@ module.exports = function(options) { send: function() {}, getValidPushTypes: function() { return Object.keys(options.options); - } + }, }; }; diff --git a/spec/MongoSchemaCollectionAdapter.spec.js b/spec/MongoSchemaCollectionAdapter.spec.js index fe3d842a53..2c5e889afc 100644 --- a/spec/MongoSchemaCollectionAdapter.spec.js +++ b/spec/MongoSchemaCollectionAdapter.spec.js @@ -1,46 +1,49 @@ 'use strict'; -const MongoSchemaCollection = require('../lib/Adapters/Storage/Mongo/MongoSchemaCollection').default; +const MongoSchemaCollection = require('../lib/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}, + expect( + MongoSchemaCollection._TESTmongoSchemaToParseSchema({ + _id: '_Installation', + _client_permissions: { + get: true, + find: true, + update: true, + create: true, + delete: true, }, - "indexes": { - "name1":{"deviceToken":1} - } - }, - "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({ + _metadata: { + class_permissions: { + get: { '*': true }, + find: { '*': true }, + update: { '*': true }, + create: { '*': true }, + delete: { '*': true }, + addField: { '*': true }, + }, + indexes: { + name1: { deviceToken: 1 }, + }, + }, + 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' }, @@ -71,7 +74,7 @@ describe('MongoSchemaCollection', () => { addField: { '*': true }, }, indexes: { - name1: {deviceToken: 1} + name1: { deviceToken: 1 }, }, }); done(); diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index d26b822eb7..97459f6466 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -1,8 +1,10 @@ 'use strict'; -const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; +const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter') + .default; const { MongoClient } = require('mongodb'); -const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; +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 @@ -16,7 +18,8 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { it('auto-escapes symbols in auth information', () => { spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null)); new MongoStorageAdapter({ - uri: 'mongodb://user!with@+ symbols:password!with@+ symbols@localhost:1234/parse' + 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', @@ -27,7 +30,8 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { it("doesn't double escape already URI-encoded information", () => { spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null)); new MongoStorageAdapter({ - uri: 'mongodb://user!with%40%2B%20symbols:password!with%40%2B%20symbols@localhost:1234/parse' + 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', @@ -39,7 +43,8 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { it('preserves replica sets', () => { spyOn(MongoClient, 'connect').and.returnValue(Promise.resolve(null)); new MongoStorageAdapter({ - uri: 'mongodb://test:testpass@ds056315-a0.mongolab.com:59325,ds059315-a1.mongolab.com:59315/testDBname?replicaSet=rs-ds059415' + 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', @@ -49,7 +54,8 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { it('stores objectId in _id', done => { const adapter = new MongoStorageAdapter({ uri: databaseURI }); - adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' }) + adapter + .createObject('Foo', { fields: {} }, { objectId: 'abcde' }) .then(() => adapter._rawFind('Foo', {})) .then(results => { expect(results.length).toEqual(1); @@ -60,35 +66,41 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); }); - it('find succeeds when query is within maxTimeMS', (done) => { + it('find succeeds when query is within maxTimeMS', done => { const maxTimeMS = 250; const adapter = new MongoStorageAdapter({ uri: databaseURI, mongoOptions: { maxTimeMS }, }); - adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' }) - .then(() => adapter._rawFind('Foo', { '$where': `sleep(${maxTimeMS / 2})` })) + adapter + .createObject('Foo', { fields: {} }, { objectId: 'abcde' }) + .then(() => + adapter._rawFind('Foo', { $where: `sleep(${maxTimeMS / 2})` }) + ) .then( () => done(), - (err) => { + err => { done.fail(`maxTimeMS should not affect fast queries ${err}`); } ); - }) + }); - it('find fails when query exceeds maxTimeMS', (done) => { + it('find fails when query exceeds maxTimeMS', done => { const maxTimeMS = 250; const adapter = new MongoStorageAdapter({ uri: databaseURI, mongoOptions: { maxTimeMS }, }); - adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' }) - .then(() => adapter._rawFind('Foo', { '$where': `sleep(${maxTimeMS * 2})` })) + adapter + .createObject('Foo', { fields: {} }, { objectId: 'abcde' }) + .then(() => + adapter._rawFind('Foo', { $where: `sleep(${maxTimeMS * 2})` }) + ) .then( () => { done.fail('Find succeeded despite taking too long!'); }, - (err) => { + err => { expect(err.name).toEqual('MongoError'); expect(err.code).toEqual(50); expect(err.message).toEqual('operation exceeded time limit'); @@ -97,20 +109,27 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { ); }); - it('stores pointers with a _p_ prefix', (done) => { + it('stores pointers with a _p_ prefix', done => { const obj = { objectId: 'bar', aPointer: { __type: 'Pointer', className: 'JustThePointer', - objectId: 'qwerty' - } + objectId: 'qwerty', + }, }; const adapter = new MongoStorageAdapter({ uri: databaseURI }); - adapter.createObject('APointerDarkly', { fields: { - objectId: { type: 'String' }, - aPointer: { type: 'Pointer', targetClass: 'JustThePointer' }, - }}, obj) + adapter + .createObject( + 'APointerDarkly', + { + fields: { + objectId: { type: 'String' }, + aPointer: { type: 'Pointer', targetClass: 'JustThePointer' }, + }, + }, + obj + ) .then(() => adapter._rawFind('APointerDarkly', {})) .then(results => { expect(results.length).toEqual(1); @@ -125,9 +144,10 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { it('handles object and subdocument', done => { const adapter = new MongoStorageAdapter({ uri: databaseURI }); - const schema = { fields : { subdoc: { type: 'Object' } } }; - const obj = { subdoc: {foo: 'bar', wu: 'tan'} }; - adapter.createObject('MyClass', schema, obj) + const schema = { fields: { subdoc: { type: 'Object' } } }; + const obj = { subdoc: { foo: 'bar', wu: 'tan' } }; + adapter + .createObject('MyClass', schema, obj) .then(() => adapter._rawFind('MyClass', {})) .then(results => { expect(results.length).toEqual(1); @@ -149,22 +169,25 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); }); - it('handles creating an array, object, date', (done) => { + it('handles creating an array, object, date', done => { const adapter = new MongoStorageAdapter({ uri: databaseURI }); const obj = { array: [1, 2, 3], - object: {foo: 'bar'}, + object: { foo: 'bar' }, date: { __type: 'Date', iso: '2016-05-26T20:55:01.154Z', }, }; - const schema = { fields: { - array: { type: 'Array' }, - object: { type: 'Object' }, - date: { type: 'Date' }, - } }; - adapter.createObject('MyClass', schema, obj) + const 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); @@ -190,30 +213,32 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); }); - it("handles updating a single object with array, object date", (done) => { + it('handles updating a single object with array, object date', done => { const adapter = new MongoStorageAdapter({ uri: databaseURI }); - const schema = { fields: { - array: { type: 'Array' }, - object: { type: 'Object' }, - date: { type: 'Date' }, - } }; - + const schema = { + fields: { + array: { type: 'Array' }, + object: { type: 'Object' }, + date: { type: 'Date' }, + }, + }; - adapter.createObject('MyClass', schema, {}) + adapter + .createObject('MyClass', schema, {}) .then(() => adapter._rawFind('MyClass', {})) .then(results => { expect(results.length).toEqual(1); const update = { array: [1, 2, 3], - object: {foo: 'bar'}, + object: { foo: 'bar' }, date: { __type: 'Date', iso: '2016-05-26T20:55:01.154Z', }, }; const query = {}; - return adapter.findOneAndUpdate('MyClass', schema, query, update) + return adapter.findOneAndUpdate('MyClass', schema, query, update); }) .then(results => { const mob = results; @@ -238,7 +263,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { }); }); - it('handleShutdown, close connection', (done) => { + it('handleShutdown, close connection', done => { const adapter = new MongoStorageAdapter({ uri: databaseURI }); const schema = { @@ -246,12 +271,12 @@ describe_only_db('mongo')('MongoStorageAdapter', () => { array: { type: 'Array' }, object: { type: 'Object' }, date: { type: 'Date' }, - } + }, }; adapter.createObject('MyClass', schema, {}).then(() => { expect(adapter.database.serverConfig.isConnected()).toEqual(true); - adapter.handleShutdown() + adapter.handleShutdown(); expect(adapter.database.serverConfig.isConnected()).toEqual(false); done(); }); diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 24d1eb965f..d1449550f0 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -1,149 +1,184 @@ // These tests are unit tests designed to only test transform.js. -"use strict"; +'use strict'; const transform = require('../lib/Adapters/Storage/Mongo/MongoTransform'); const dd = require('deep-diff'); const mongodb = require('mongodb'); describe('parseObjectToMongoObjectForCreate', () => { - it('a basic number', (done) => { - const input = {five: 5}; + it('a basic number', done => { + const input = { five: 5 }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { - fields: {five: {type: 'Number'}} + fields: { five: { type: 'Number' } }, }); jequal(input, output); done(); }); - it('an object with null values', (done) => { - const input = {objectWithNullValues: {isNull: null, notNull: 3}}; + it('an object with null values', done => { + const input = { objectWithNullValues: { isNull: null, notNull: 3 } }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { - fields: {objectWithNullValues: {type: 'object'}} + fields: { objectWithNullValues: { type: 'object' } }, }); jequal(input, output); done(); }); - it('built-in timestamps with date', (done) => { + it('built-in timestamps with date', done => { const input = { - createdAt: "2015-10-06T21:24:50.332Z", - updatedAt: "2015-10-06T21:24:50.332Z" + createdAt: '2015-10-06T21:24:50.332Z', + updatedAt: '2015-10-06T21:24:50.332Z', }; - const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }); + const 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) => { + it('array of pointers', done => { const pointer = { __type: 'Pointer', objectId: 'myId', className: 'Blah', }; - const out = transform.parseObjectToMongoObjectForCreate(null, {pointers: [pointer]},{ - fields: {pointers: {type: 'Array'}} - }); + const 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) => { - const input = {deleteMe: {__op: 'Delete'}}; - const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }); + xit('a delete op', done => { + const input = { deleteMe: { __op: 'Delete' } }; + const output = transform.parseObjectToMongoObjectForCreate(null, input, { + fields: {}, + }); jequal(output, {}); done(); }); it('Doesnt allow ACL, as Parse Server should tranform ACL to _wperm + _rperm', done => { - const input = {ACL: {'0123': {'read': true, 'write': true}}}; - expect(() => transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} })).toThrow(); + const input = { ACL: { '0123': { read: true, write: true } } }; + expect(() => + transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }) + ).toThrow(); done(); }); - it('parse geopoint to mongo', (done) => { + it('parse geopoint to mongo', done => { const lat = -45; const lng = 45; - const geoPoint = {__type: 'GeoPoint', latitude: lat, longitude: lng}; - const out = transform.parseObjectToMongoObjectForCreate(null, {location: geoPoint},{ - fields: {location: {type: 'GeoPoint'}} - }); + const geoPoint = { __type: 'GeoPoint', latitude: lat, longitude: lng }; + const out = transform.parseObjectToMongoObjectForCreate( + null, + { location: geoPoint }, + { + fields: { location: { type: 'GeoPoint' } }, + } + ); expect(out.location).toEqual([lng, lat]); done(); }); - it('parse polygon to mongo', (done) => { + it('parse polygon to mongo', done => { const lat1 = -45; const lng1 = 45; const lat2 = -55; const lng2 = 55; const lat3 = -65; const lng3 = 65; - const polygon = {__type: 'Polygon', coordinates: [[lat1, lng1],[lat2, lng2],[lat3, lng3]]} - const out = transform.parseObjectToMongoObjectForCreate(null, {location: polygon},{ - fields: {location: {type: 'Polygon'}} - }); - expect(out.location.coordinates).toEqual([[[lng1, lat1],[lng2, lat2],[lng3, lat3],[lng1, lat1]]]); + const polygon = { + __type: 'Polygon', + coordinates: [[lat1, lng1], [lat2, lng2], [lat3, lng3]], + }; + const out = transform.parseObjectToMongoObjectForCreate( + null, + { location: polygon }, + { + fields: { location: { type: 'Polygon' } }, + } + ); + expect(out.location.coordinates).toEqual([ + [[lng1, lat1], [lng2, lat2], [lng3, lat3], [lng1, lat1]], + ]); done(); }); - it('in array', (done) => { - const geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180}; - const out = transform.parseObjectToMongoObjectForCreate(null, {locations: [geoPoint, geoPoint]},{ - fields: {locations: {type: 'Array'}} - }); + it('in array', done => { + const geoPoint = { __type: 'GeoPoint', longitude: 180, latitude: -180 }; + const out = transform.parseObjectToMongoObjectForCreate( + null, + { locations: [geoPoint, geoPoint] }, + { + fields: { locations: { type: 'Array' } }, + } + ); expect(out.locations).toEqual([geoPoint, geoPoint]); done(); }); - it('in sub-object', (done) => { - const geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180}; - const out = transform.parseObjectToMongoObjectForCreate(null, { locations: { start: geoPoint }},{ - fields: {locations: {type: 'Object'}} - }); + it('in sub-object', done => { + const geoPoint = { __type: 'GeoPoint', longitude: 180, latitude: -180 }; + const out = transform.parseObjectToMongoObjectForCreate( + null, + { locations: { start: geoPoint } }, + { + fields: { locations: { type: 'Object' } }, + } + ); expect(out).toEqual({ locations: { start: geoPoint } }); done(); }); - it('objectId', (done) => { - const out = transform.transformWhere(null, {objectId: 'foo'}); + it('objectId', done => { + const out = transform.transformWhere(null, { objectId: 'foo' }); expect(out._id).toEqual('foo'); done(); }); - it('objectId in a list', (done) => { + it('objectId in a list', done => { const input = { - objectId: {'$in': ['one', 'two', 'three']}, + objectId: { $in: ['one', 'two', 'three'] }, }; const output = transform.transformWhere(null, input); jequal(input.objectId, output._id); done(); }); - it('built-in timestamps', (done) => { - const input = {createdAt: new Date(), updatedAt: new Date()}; - const output = transform.mongoObjectToParseObject(null, input, { fields: {} }); + it('built-in timestamps', done => { + const input = { createdAt: new Date(), updatedAt: new Date() }; + const output = transform.mongoObjectToParseObject(null, input, { + fields: {}, + }); expect(typeof output.createdAt).toEqual('string'); expect(typeof output.updatedAt).toEqual('string'); done(); }); - it('pointer', (done) => { - const input = {_p_userPointer: '_User$123'}; + it('pointer', done => { + const input = { _p_userPointer: '_User$123' }; const 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'} - ); + expect(output.userPointer).toEqual({ + __type: 'Pointer', + className: '_User', + objectId: '123', + }); done(); }); - it('null pointer', (done) => { - const input = {_p_userPointer: null}; + it('null pointer', done => { + const input = { _p_userPointer: null }; const output = transform.mongoObjectToParseObject(null, input, { fields: { userPointer: { type: 'Pointer', targetClass: '_User' } }, }); @@ -151,117 +186,134 @@ describe('parseObjectToMongoObjectForCreate', () => { done(); }); - it('file', (done) => { - const input = {picture: 'pic.jpg'}; + it('file', done => { + const input = { picture: 'pic.jpg' }; const output = transform.mongoObjectToParseObject(null, input, { - fields: { picture: { type: 'File' }}, + fields: { picture: { type: 'File' } }, }); expect(typeof output.picture).toEqual('object'); - expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'}); + expect(output.picture).toEqual({ __type: 'File', name: 'pic.jpg' }); done(); }); - it('mongo geopoint to parse', (done) => { + it('mongo geopoint to parse', done => { const lat = -45; const lng = 45; - const input = {location: [lng, lat]}; + const input = { location: [lng, lat] }; const output = transform.mongoObjectToParseObject(null, input, { - fields: { location: { type: 'GeoPoint' }}, + fields: { location: { type: 'GeoPoint' } }, }); expect(typeof output.location).toEqual('object'); - expect(output.location).toEqual( - {__type: 'GeoPoint', latitude: lat, longitude: lng} - ); + expect(output.location).toEqual({ + __type: 'GeoPoint', + latitude: lat, + longitude: lng, + }); done(); }); - it('mongo polygon to parse', (done) => { + it('mongo polygon to parse', done => { const lat = -45; const lng = 45; // Mongo stores polygon in WGS84 lng/lat - const input = {location: { type: 'Polygon', coordinates: [[[lat, lng],[lat, lng]]]}}; + const input = { + location: { type: 'Polygon', coordinates: [[[lat, lng], [lat, lng]]] }, + }; const output = transform.mongoObjectToParseObject(null, input, { - fields: { location: { type: 'Polygon' }}, + fields: { location: { type: 'Polygon' } }, }); expect(typeof output.location).toEqual('object'); - expect(output.location).toEqual( - {__type: 'Polygon', coordinates: [[lng, lat],[lng, lat]]} - ); + expect(output.location).toEqual({ + __type: 'Polygon', + coordinates: [[lng, lat], [lng, lat]], + }); done(); }); - it('bytes', (done) => { - const input = {binaryData: "aGVsbG8gd29ybGQ="}; + it('bytes', done => { + const input = { binaryData: 'aGVsbG8gd29ybGQ=' }; const output = transform.mongoObjectToParseObject(null, input, { - fields: { binaryData: { type: 'Bytes' }}, + fields: { binaryData: { type: 'Bytes' } }, }); expect(typeof output.binaryData).toEqual('object'); - expect(output.binaryData).toEqual( - {__type: 'Bytes', base64: "aGVsbG8gd29ybGQ="} - ); + expect(output.binaryData).toEqual({ + __type: 'Bytes', + base64: 'aGVsbG8gd29ybGQ=', + }); done(); }); - it('nested array', (done) => { - const input = {arr: [{_testKey: 'testValue' }]}; + it('nested array', done => { + const input = { arr: [{ _testKey: 'testValue' }] }; const output = transform.mongoObjectToParseObject(null, input, { fields: { arr: { type: 'Array' } }, }); expect(Array.isArray(output.arr)).toEqual(true); - expect(output.arr).toEqual([{ _testKey: 'testValue'}]); + expect(output.arr).toEqual([{ _testKey: 'testValue' }]); done(); }); it('untransforms objects containing nested special keys', done => { - const 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", - }]} + const 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', + }, + ], + }; const output = transform.mongoObjectToParseObject(null, input, { - fields: { array: { type: 'Array' }}, + fields: { array: { type: 'Array' } }, }); expect(dd(output, input)).toEqual(undefined); done(); }); - it('changes new pointer key', (done) => { + it('changes new pointer key', done => { const input = { - somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'} + somePointer: { __type: 'Pointer', className: 'Micro', objectId: 'oft' }, }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { - fields: {somePointer: {type: 'Pointer'}} + 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) => { + it('changes existing pointer keys', done => { const input = { - userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'} + userPointer: { + __type: 'Pointer', + className: '_User', + objectId: 'qwerty', + }, }; const output = transform.parseObjectToMongoObjectForCreate(null, input, { - fields: {userPointer: {type: 'Pointer'}} + 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 on create', (done) => { + it('writes the old ACL format in addition to rperm and wperm on create', done => { const input = { _rperm: ['*'], _wperm: ['Kevin'], }; - const output = transform.parseObjectToMongoObjectForCreate(null, input, { fields: {} }); + const output = transform.parseObjectToMongoObjectForCreate(null, input, { + fields: {}, + }); expect(typeof output._acl).toEqual('object'); expect(output._acl['Kevin'].w).toBeTruthy(); expect(output._acl['Kevin'].r).toBeUndefined(); @@ -270,7 +322,7 @@ describe('parseObjectToMongoObjectForCreate', () => { done(); }); - it('removes Relation types', (done) => { + it('removes Relation types', done => { const input = { aRelation: { __type: 'Relation', className: 'Stuff' }, }; @@ -283,10 +335,10 @@ describe('parseObjectToMongoObjectForCreate', () => { done(); }); - it('writes the old ACL format in addition to rperm and wperm on update', (done) => { + it('writes the old ACL format in addition to rperm and wperm on update', done => { const input = { _rperm: ['*'], - _wperm: ['Kevin'] + _wperm: ['Kevin'], }; const output = transform.transformUpdate(null, input, { fields: {} }); @@ -300,23 +352,25 @@ describe('parseObjectToMongoObjectForCreate', () => { done(); }); - it('untransforms from _rperm and _wperm to ACL', (done) => { + it('untransforms from _rperm and _wperm to ACL', done => { const input = { - _rperm: ["*"], - _wperm: ["Kevin"] + _rperm: ['*'], + _wperm: ['Kevin'], }; - const output = transform.mongoObjectToParseObject(null, input, { fields: {} }); + const output = transform.mongoObjectToParseObject(null, input, { + fields: {}, + }); expect(output._rperm).toEqual(['*']); expect(output._wperm).toEqual(['Kevin']); - expect(output.ACL).toBeUndefined() + expect(output.ACL).toBeUndefined(); done(); }); - it('untransforms mongodb number types', (done) => { + it('untransforms mongodb number types', done => { const input = { long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER), - double: new mongodb.Double(Number.MAX_VALUE) - } + double: new mongodb.Double(Number.MAX_VALUE), + }; const output = transform.mongoObjectToParseObject(null, input, { fields: { long: { type: 'Number' }, @@ -328,27 +382,27 @@ describe('parseObjectToMongoObjectForCreate', () => { done(); }); - it('Date object where iso attribute is of type Date', (done) => { + it('Date object where iso attribute is of type Date', done => { const input = { - ts : { __type: 'Date', iso: new Date('2017-01-18T00:00:00.000Z') } - } + ts: { __type: 'Date', iso: new Date('2017-01-18T00:00:00.000Z') }, + }; const output = transform.mongoObjectToParseObject(null, input, { - fields : { - ts : { type : 'Date' } - } + fields: { + ts: { type: 'Date' }, + }, }); expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z'); done(); }); - it('Date object where iso attribute is of type String', (done) => { + it('Date object where iso attribute is of type String', done => { const input = { - ts : { __type: 'Date', iso: '2017-01-18T00:00:00.000Z' } - } + ts: { __type: 'Date', iso: '2017-01-18T00:00:00.000Z' }, + }; const output = transform.mongoObjectToParseObject(null, input, { - fields : { - ts : { type : 'Date' } - } + fields: { + ts: { type: 'Date' }, + }, }); expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z'); done(); @@ -357,35 +411,44 @@ describe('parseObjectToMongoObjectForCreate', () => { it('object with undefined nested values', () => { const input = { _id: 'vQHyinCW1l', - urls: { firstUrl: 'https://', secondUrl: undefined }, }; + urls: { firstUrl: 'https://', secondUrl: undefined }, + }; const output = transform.mongoObjectToParseObject(null, input, { fields: { - urls: { type: 'Object' } - } + urls: { type: 'Object' }, + }, }); expect(output.urls).toEqual({ - firstUrl: 'https://', secondUrl: undefined + firstUrl: 'https://', + secondUrl: undefined, }); }); it('undefined objects', () => { const input = { _id: 'vQHyinCW1l', - urls: undefined, }; + urls: undefined, + }; const output = transform.mongoObjectToParseObject(null, input, { fields: { - urls: { type: 'Object' } - } + urls: { type: 'Object' }, + }, }); expect(output.urls).toBeUndefined(); }); - it('$regex in $all list', (done) => { + it('$regex in $all list', done => { const input = { - arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$regex: '^\\Qtwo\\E'}, {$regex: '^\\Qthree\\E'}]}, + arrayField: { + $all: [ + { $regex: '^\\Qone\\E' }, + { $regex: '^\\Qtwo\\E' }, + { $regex: '^\\Qthree\\E' }, + ], + }, }; const outputValue = { - arrayField: {'$all': [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/]}, + arrayField: { $all: [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/] }, }; const output = transform.transformWhere(null, input); @@ -393,31 +456,33 @@ describe('parseObjectToMongoObjectForCreate', () => { done(); }); - it('$regex in $all list must be { $regex: "string" }', (done) => { + it('$regex in $all list must be { $regex: "string" }', done => { const input = { - arrayField: {'$all': [{$regex: 1}]}, + arrayField: { $all: [{ $regex: 1 }] }, }; expect(() => { - transform.transformWhere(null, input) + transform.transformWhere(null, input); }).toThrow(); done(); }); - it('all values in $all must be $regex (start with string) or non $regex (start with string)', (done) => { + it('all values in $all must be $regex (start with string) or non $regex (start with string)', done => { const input = { - arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$unknown: '^\\Qtwo\\E'}]}, + arrayField: { + $all: [{ $regex: '^\\Qone\\E' }, { $unknown: '^\\Qtwo\\E' }], + }, }; expect(() => { - transform.transformWhere(null, input) + transform.transformWhere(null, input); }).toThrow(); done(); }); }); describe('transformUpdate', () => { - it('removes Relation types', (done) => { + it('removes Relation types', done => { const input = { aRelation: { __type: 'Relation', className: 'Stuff' }, }; @@ -439,8 +504,8 @@ describe('transformConstraint', () => { $eq: { ttl: { $relativeTime: '12 days ago', - } - } + }, + }, }); }).toThrow(); @@ -449,8 +514,8 @@ describe('transformConstraint', () => { $ne: { ttl: { $relativeTime: '12 days ago', - } - } + }, + }, }); }).toThrow(); @@ -458,11 +523,11 @@ describe('transformConstraint', () => { transform.transformConstraint({ $exists: { $relativeTime: '12 days ago', - } + }, }); }).toThrow(); }); - }) + }); }); describe('relativeTimeToDate', () => { @@ -500,7 +565,9 @@ describe('relativeTimeToDate', () => { describe('Error cases', () => { it('should error if string is completely gibberish', () => { - expect(transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123')).toEqual({ + expect( + transform.relativeTimeToDate('gibberishasdnklasdnjklasndkl123j123') + ).toEqual({ status: 'error', info: "Time should either start with 'in' or end with 'ago'", }); @@ -554,4 +621,3 @@ describe('relativeTimeToDate', () => { }); }); }); - diff --git a/spec/NullCacheAdapter.spec.js b/spec/NullCacheAdapter.spec.js index 04d4cdcd68..8c240eef9b 100644 --- a/spec/NullCacheAdapter.spec.js +++ b/spec/NullCacheAdapter.spec.js @@ -1,12 +1,13 @@ -const NullCacheAdapter = require('../lib/Adapters/Cache/NullCacheAdapter').default; +const NullCacheAdapter = require('../lib/Adapters/Cache/NullCacheAdapter') + .default; describe('NullCacheAdapter', function() { const KEY = 'hello'; const VALUE = 'world'; - it('should expose promisifyed methods', (done) => { + it('should expose promisifyed methods', done => { const cache = new NullCacheAdapter({ - ttl: NaN + ttl: NaN, }); // Verify all methods return promises. @@ -14,24 +15,24 @@ describe('NullCacheAdapter', function() { cache.put(KEY, VALUE), cache.del(KEY), cache.get(KEY), - cache.clear() + cache.clear(), ]).then(() => { done(); }); }); - it('should get/set/clear', (done) => { + it('should get/set/clear', done => { const cache = new NullCacheAdapter({ - ttl: NaN + ttl: NaN, }); - cache.put(KEY, VALUE) + cache + .put(KEY, VALUE) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(null)) + .then(value => expect(value).toEqual(null)) .then(() => cache.clear()) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(null)) + .then(value => expect(value).toEqual(null)) .then(done); }); - }); diff --git a/spec/OAuth1.spec.js b/spec/OAuth1.spec.js index ed74bfb9e8..8d256f755f 100644 --- a/spec/OAuth1.spec.js +++ b/spec/OAuth1.spec.js @@ -1,140 +1,153 @@ -const OAuth = require("../lib/Adapters/Auth/OAuth1Client"); +const OAuth = require('../lib/Adapters/Auth/OAuth1Client'); describe('OAuth', function() { - it("Nonce should have right length", (done) => { + it('Nonce should have right length', done => { jequal(OAuth.nonce().length, 30); done(); }); - it("Should properly build parameter string", (done) => { - const string = OAuth.buildParameterString({c:1, a:2, b:3}) - jequal(string, "a=2&b=3&c=1"); + it('Should properly build parameter string', done => { + const string = OAuth.buildParameterString({ c: 1, a: 2, b: 3 }); + jequal(string, 'a=2&b=3&c=1'); done(); }); - it("Should properly build empty parameter string", (done) => { - const string = OAuth.buildParameterString() - jequal(string, ""); + it('Should properly build empty parameter string', done => { + const string = OAuth.buildParameterString(); + jequal(string, ''); done(); }); - it("Should properly build signature string", (done) => { - const string = OAuth.buildSignatureString("get", "http://dummy.com", ""); - jequal(string, "GET&http%3A%2F%2Fdummy.com&"); + it('Should properly build signature string', done => { + const string = OAuth.buildSignatureString('get', 'http://dummy.com', ''); + jequal(string, 'GET&http%3A%2F%2Fdummy.com&'); done(); }); - it("Should properly generate request signature", (done) => { + it('Should properly generate request signature', done => { let request = { - host: "dummy.com", - path: "path" + host: 'dummy.com', + path: 'path', }; const oauth_params = { oauth_timestamp: 123450000, - oauth_nonce: "AAAAAAAAAAAAAAAAA", - oauth_consumer_key: "hello", - oauth_token: "token" + oauth_nonce: 'AAAAAAAAAAAAAAAAA', + oauth_consumer_key: 'hello', + oauth_token: 'token', }; - const consumer_secret = "world"; - const auth_token_secret = "secret"; - request = OAuth.signRequest(request, oauth_params, consumer_secret, auth_token_secret); - jequal(request.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"'); + const consumer_secret = 'world'; + const auth_token_secret = 'secret'; + request = OAuth.signRequest( + request, + oauth_params, + consumer_secret, + auth_token_secret + ); + jequal( + request.headers['Authorization'], + 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"' + ); done(); }); - it("Should properly build request", (done) => { + it('Should properly build request', done => { const options = { - host: "dummy.com", - consumer_key: "hello", - consumer_secret: "world", - auth_token: "token", - auth_token_secret: "secret", + host: 'dummy.com', + consumer_key: 'hello', + consumer_secret: 'world', + auth_token: 'token', + auth_token_secret: 'secret', // Custom oauth params for tests oauth_params: { oauth_timestamp: 123450000, - oauth_nonce: "AAAAAAAAAAAAAAAAA" - } + oauth_nonce: 'AAAAAAAAAAAAAAAAA', + }, }; - const path = "path"; - const method = "get"; + const path = 'path'; + const method = 'get'; const oauthClient = new OAuth(options); - const req = oauthClient.buildRequest(method, path, {"query": "param"}); + const req = oauthClient.buildRequest(method, path, { query: 'param' }); jequal(req.host, options.host); - jequal(req.path, "/" + path + "?query=param"); - jequal(req.method, "GET"); + jequal(req.path, '/' + path + '?query=param'); + jequal(req.method, 'GET'); jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded'); - jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"') + jequal( + req.headers['Authorization'], + 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"' + ); done(); }); - function validateCannotAuthenticateError(data, done) { - jequal(typeof data, "object"); - jequal(typeof data.errors, "object"); + jequal(typeof data, 'object'); + jequal(typeof data.errors, 'object'); const errors = data.errors; - jequal(typeof errors[0], "object"); + jequal(typeof errors[0], 'object'); // Cannot authenticate error jequal(errors[0].code, 32); done(); } - it("Should fail a GET request", (done) => { + it('Should fail a GET request', done => { const options = { - host: "api.twitter.com", - consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX", - consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + host: 'api.twitter.com', + consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXX', + consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', }; - const path = "/1.1/help/configuration.json"; - const params = {"lang": "en"}; + const path = '/1.1/help/configuration.json'; + const params = { lang: 'en' }; const oauthClient = new OAuth(options); - oauthClient.get(path, params).then(function(data){ + oauthClient.get(path, params).then(function(data) { validateCannotAuthenticateError(data, done); - }) + }); }); - it("Should fail a POST request", (done) => { + it('Should fail a POST request', done => { const options = { - host: "api.twitter.com", - consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX", - consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + host: 'api.twitter.com', + consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXX', + consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', }; const body = { - lang: "en" + lang: 'en', }; - const path = "/1.1/account/settings.json"; + const path = '/1.1/account/settings.json'; const oauthClient = new OAuth(options); - oauthClient.post(path, null, body).then(function(data){ + oauthClient.post(path, null, body).then(function(data) { validateCannotAuthenticateError(data, done); - }) + }); }); - it("Should fail a request", (done) => { + it('Should fail a request', done => { const options = { - host: "localhost", - consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX", - consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + host: 'localhost', + consumer_key: 'XXXXXXXXXXXXXXXXXXXXXXXXX', + consumer_secret: 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', }; const body = { - lang: "en" + lang: 'en', }; - const path = "/"; + const path = '/'; const oauthClient = new OAuth(options); - oauthClient.post(path, null, body).then(function(){ - jequal(false, true); - done(); - }).catch(function(){ - jequal(true, true); - done(); - }) + oauthClient + .post(path, null, body) + .then(function() { + jequal(false, true); + done(); + }) + .catch(function() { + jequal(true, true); + done(); + }); }); - it("Should fail with missing options", (done) => { + it('Should fail with missing options', done => { const options = undefined; try { new OAuth(options); diff --git a/spec/Parse.Push.spec.js b/spec/Parse.Push.spec.js index 90558284d6..3d789f3b17 100644 --- a/spec/Parse.Push.spec.js +++ b/spec/Parse.Push.spec.js @@ -2,11 +2,11 @@ const request = require('request'); -const delayPromise = (delay) => { - return new Promise((resolve) => { +const delayPromise = delay => { + return new Promise(resolve => { setTimeout(resolve, delay); }); -} +}; describe('Parse.Push', () => { const setup = function() { @@ -15,10 +15,10 @@ describe('Parse.Push', () => { const pushAdapter = { send: function(body, installations) { const badge = body.data.badge; - const promises = installations.map((installation) => { + const promises = installations.map(installation => { sendToInstallationSpy(installation); - if (installation.deviceType == "ios") { + if (installation.deviceType == 'ios') { expect(installation.badge).toEqual(badge); expect(installation.originalBadge + 1).toEqual(installation.badge); } else { @@ -27,33 +27,39 @@ describe('Parse.Push', () => { return Promise.resolve({ err: null, device: installation, - transmitted: true - }) + transmitted: true, + }); }); return Promise.all(promises); }, getValidPushTypes: function() { - return ["ios", "android"]; - } - } + return ['ios', 'android']; + }, + }; return reconfigureServer({ appId: Parse.applicationId, masterKey: Parse.masterKey, serverURL: Parse.serverURL, push: { - adapter: pushAdapter - } + adapter: pushAdapter, + }, }) .then(() => { const installations = []; - while(installations.length != 10) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set( + 'deviceToken', + 'device_token_' + installations.length + ); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } return Parse.Object.saveAll(installations); @@ -63,83 +69,105 @@ describe('Parse.Push', () => { sendToInstallationSpy, }; }) - .catch((err) => { + .catch(err => { console.error(err); throw err; - }) - } + }); + }; - it('should properly send push', (done) => { - return setup().then(({ sendToInstallationSpy }) => { - return Parse.Push.send({ - where: { - deviceType: 'ios' - }, - data: { - badge: 'Increment', - alert: 'Hello world!' - } - }, {useMasterKey: true}) - .then(() => { - return delayPromise(500); - }) - .then(() => { - expect(sendToInstallationSpy.calls.count()).toEqual(10); - }) - }).then(() => { - done(); - }).catch((err) => { - jfail(err); - done(); - }); + it('should properly send push', done => { + return setup() + .then(({ sendToInstallationSpy }) => { + return Parse.Push.send( + { + where: { + deviceType: 'ios', + }, + data: { + badge: 'Increment', + alert: 'Hello world!', + }, + }, + { useMasterKey: true } + ) + .then(() => { + return delayPromise(500); + }) + .then(() => { + expect(sendToInstallationSpy.calls.count()).toEqual(10); + }); + }) + .then(() => { + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); - it('should properly send push with lowercaseIncrement', (done) => { - return setup().then(() => { - return Parse.Push.send({ - where: { - deviceType: 'ios' - }, - data: { - badge: 'increment', - alert: 'Hello world!' - } - }, {useMasterKey: true}) - }).then(() => { - return delayPromise(500); - }).then(() => { - done(); - }).catch((err) => { - jfail(err); - done(); - }); + it('should properly send push with lowercaseIncrement', done => { + return setup() + .then(() => { + return Parse.Push.send( + { + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', + }, + }, + { useMasterKey: true } + ); + }) + .then(() => { + return delayPromise(500); + }) + .then(() => { + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); it('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(() => + Parse.Push.send( + { + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', + }, + }, + { useMasterKey: true } + ) + ) .then(() => delayPromise(500)) .then(() => { - request.get({ - url: 'http://localhost:8378/1/classes/_PushStatus', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', + 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(); - }); - }).catch((err) => { + (error, response, body) => { + expect(body.error).toEqual('unauthorized'); + done(); + } + ); + }) + .catch(err => { jfail(err); done(); }); @@ -147,86 +175,108 @@ describe('Parse.Push', () => { it('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(() => + Parse.Push.send( + { + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', + }, + }, + { useMasterKey: true } + ) + ) .then(() => delayPromise(500)) // put a delay as we keep writing .then(() => { - request.get({ - url: 'http://localhost:8378/1/classes/_PushStatus', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', + 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) => { - try { - 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!"}'); - } catch(e) { - jfail(e); + (error, response, body) => { + try { + 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!"}' + ); + } catch (e) { + jfail(e); + } + done(); } - done(); - }); - }).catch((err) => { + ); + }) + .catch(err => { jfail(err); done(); }); }); it('should throw error if missing push configuration', done => { - reconfigureServer({push: null}) + reconfigureServer({ push: null }) .then(() => { - return Parse.Push.send({ - where: { - deviceType: 'ios' + return Parse.Push.send( + { + where: { + deviceType: 'ios', + }, + data: { + badge: 'increment', + alert: 'Hello world!', + }, }, - data: { - badge: 'increment', - alert: 'Hello world!' - } - }, {useMasterKey: true}) - }).then(() => { - fail('should not succeed'); - }, (err) => { - expect(err.code).toEqual(Parse.Error.PUSH_MISCONFIGURED); - done(); - }).catch((err) => { + { useMasterKey: true } + ); + }) + .then( + () => { + fail('should not succeed'); + }, + err => { + expect(err.code).toEqual(Parse.Error.PUSH_MISCONFIGURED); + done(); + } + ) + .catch(err => { jfail(err); done(); }); }); const successfulAny = function(body, installations) { - const promises = installations.map((device) => { + const promises = installations.map(device => { return Promise.resolve({ transmitted: true, device: device, - }) + }); }); return Promise.all(promises); }; const provideInstallations = function(num) { - if(!num) { + if (!num) { num = 2; } const installations = []; - while(installations.length !== num) { + while (installations.length !== num) { // add Android installations - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length); - installation.set("deviceType", "android"); + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('deviceType', 'android'); installations.push(installation); } @@ -242,8 +292,8 @@ describe('Parse.Push', () => { return successfulAny(body, installations); }, getValidPushTypes: function() { - return ["android"]; - } + return ['android']; + }, }; /** @@ -251,39 +301,44 @@ describe('Parse.Push', () => { * Simulates a simple push where 1 installation is removed between _PushStatus * count being set and the pushes being sent */ - it('does not get stuck with _PushStatus \'running\' on 1 installation lost', (done) => { + it("does not get stuck with _PushStatus 'running' on 1 installation lost", done => { reconfigureServer({ - push: {adapter: losingAdapter} - }).then(() => { - return Parse.Object.saveAll(provideInstallations()); - }).then(() => { - return Parse.Push.send( - { - data: {alert: "We fixed our status!"}, - where: {deviceType: 'android'} - }, - { useMasterKey: true } - ); - }).then(() => { - // it is enqueued so it can take time - return new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 1000); + push: { adapter: losingAdapter }, + }) + .then(() => { + return Parse.Object.saveAll(provideInstallations()); + }) + .then(() => { + return Parse.Push.send( + { + data: { alert: 'We fixed our status!' }, + where: { deviceType: 'android' }, + }, + { useMasterKey: true } + ); + }) + .then(() => { + // it is enqueued so it can take time + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + // query for push status + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + // verify status is NOT broken + expect(results.length).toBe(1); + const result = results[0]; + expect(result.get('status')).toEqual('succeeded'); + expect(result.get('numSent')).toEqual(1); + expect(result.get('count')).toEqual(undefined); + done(); }); - }).then(() => { - // query for push status - const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}); - }).then((results) => { - // verify status is NOT broken - expect(results.length).toBe(1); - const result = results[0]; - expect(result.get('status')).toEqual('succeeded'); - expect(result.get('numSent')).toEqual(1); - expect(result.get('count')).toEqual(undefined); - done(); - }); }); /** @@ -291,14 +346,17 @@ describe('Parse.Push', () => { * Simulates a simple push where 1 installation is added between _PushStatus * count being set and the pushes being sent */ - it('does not get stuck with _PushStatus \'running\' on 1 installation added', (done) => { + it("does not get stuck with _PushStatus 'running' on 1 installation added", done => { const installations = provideInstallations(); // add 1 iOS installation which we will omit & add later on - const iOSInstallation = new Parse.Object("_Installation"); - iOSInstallation.set("installationId", "installation_" + installations.length); - iOSInstallation.set("deviceToken","device_token_" + installations.length); - iOSInstallation.set("deviceType", "ios"); + const iOSInstallation = new Parse.Object('_Installation'); + iOSInstallation.set( + 'installationId', + 'installation_' + installations.length + ); + iOSInstallation.set('deviceToken', 'device_token_' + installations.length); + iOSInstallation.set('deviceType', 'ios'); installations.push(iOSInstallation); reconfigureServer({ @@ -312,40 +370,45 @@ describe('Parse.Push', () => { return successfulAny(body, installations); }, getValidPushTypes: function() { - return ["android"]; - } - } - } - }).then(() => { - return Parse.Object.saveAll(installations); - }).then(() => { - return Parse.Push.send( - { - data: {alert: "We fixed our status!"}, - where: {deviceType: {'$ne' : 'random'}} + return ['android']; + }, }, - { useMasterKey: true } - ); - }).then(() => { - // it is enqueued so it can take time - return new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 1000); + }, + }) + .then(() => { + return Parse.Object.saveAll(installations); + }) + .then(() => { + return Parse.Push.send( + { + data: { alert: 'We fixed our status!' }, + where: { deviceType: { $ne: 'random' } }, + }, + { useMasterKey: true } + ); + }) + .then(() => { + // it is enqueued so it can take time + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + // query for push status + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + // verify status is NOT broken + expect(results.length).toBe(1); + const result = results[0]; + expect(result.get('status')).toEqual('succeeded'); + expect(result.get('numSent')).toEqual(3); + expect(result.get('count')).toEqual(undefined); + done(); }); - }).then(() => { - // query for push status - const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}); - }).then((results) => { - // verify status is NOT broken - expect(results.length).toBe(1); - const result = results[0]; - expect(result.get('status')).toEqual('succeeded'); - expect(result.get('numSent')).toEqual(3); - expect(result.get('count')).toEqual(undefined); - done(); - }); }); /** @@ -353,43 +416,48 @@ describe('Parse.Push', () => { * Simulates an extended push, where some installations may be removed, * resulting in a non-zero count */ - it('does not get stuck with _PushStatus \'running\' on many installations removed', (done) => { + it("does not get stuck with _PushStatus 'running' on many installations removed", done => { const devices = 1000; const installations = provideInstallations(devices); reconfigureServer({ - push: {adapter: losingAdapter} - }).then(() => { - return Parse.Object.saveAll(installations); - }).then(() => { - return Parse.Push.send( - { - data: {alert: "We fixed our status!"}, - where: {deviceType: 'android'} - }, - { useMasterKey: true } - ); - }).then(() => { - // it is enqueued so it can take time - return new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 1000); + push: { adapter: losingAdapter }, + }) + .then(() => { + return Parse.Object.saveAll(installations); + }) + .then(() => { + return Parse.Push.send( + { + data: { alert: 'We fixed our status!' }, + where: { deviceType: 'android' }, + }, + { useMasterKey: true } + ); + }) + .then(() => { + // it is enqueued so it can take time + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + // query for push status + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + // verify status is NOT broken + expect(results.length).toBe(1); + const result = results[0]; + expect(result.get('status')).toEqual('succeeded'); + // expect # less than # of batches used, assuming each batch is 100 pushes + expect(result.get('numSent')).toEqual(devices - devices / 100); + expect(result.get('count')).toEqual(undefined); + done(); }); - }).then(() => { - // query for push status - const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}); - }).then((results) => { - // verify status is NOT broken - expect(results.length).toBe(1); - const result = results[0]; - expect(result.get('status')).toEqual('succeeded'); - // expect # less than # of batches used, assuming each batch is 100 pushes - expect(result.get('numSent')).toEqual(devices - (devices / 100)); - expect(result.get('count')).toEqual(undefined); - done(); - }); }); /** @@ -397,18 +465,24 @@ describe('Parse.Push', () => { * Simulates an extended push, where some installations may be added, * resulting in a non-zero count */ - it('does not get stuck with _PushStatus \'running\' on many installations added', (done) => { + it("does not get stuck with _PushStatus 'running' on many installations added", done => { const devices = 1000; const installations = provideInstallations(devices); // add 1 iOS installation which we will omit & add later on const iOSInstallations = []; - while(iOSInstallations.length !== (devices / 100)) { - const iOSInstallation = new Parse.Object("_Installation"); - iOSInstallation.set("installationId", "installation_" + installations.length); - iOSInstallation.set("deviceToken", "device_token_" + installations.length); - iOSInstallation.set("deviceType", "ios"); + while (iOSInstallations.length !== devices / 100) { + const iOSInstallation = new Parse.Object('_Installation'); + iOSInstallation.set( + 'installationId', + 'installation_' + installations.length + ); + iOSInstallation.set( + 'deviceToken', + 'device_token_' + installations.length + ); + iOSInstallation.set('deviceType', 'ios'); installations.push(iOSInstallation); iOSInstallations.push(iOSInstallation); } @@ -424,40 +498,45 @@ describe('Parse.Push', () => { return successfulAny(body, installations); }, getValidPushTypes: function() { - return ["android"]; - } - } - } - }).then(() => { - return Parse.Object.saveAll(installations); - }).then(() => { - return Parse.Push.send( - { - data: {alert: "We fixed our status!"}, - where: {deviceType: {'$ne' : 'random'}} + return ['android']; + }, }, - { useMasterKey: true } - ); - }).then(() => { - // it is enqueued so it can take time - return new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 1000); + }, + }) + .then(() => { + return Parse.Object.saveAll(installations); + }) + .then(() => { + return Parse.Push.send( + { + data: { alert: 'We fixed our status!' }, + where: { deviceType: { $ne: 'random' } }, + }, + { useMasterKey: true } + ); + }) + .then(() => { + // it is enqueued so it can take time + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + // query for push status + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + // verify status is NOT broken + expect(results.length).toBe(1); + const result = results[0]; + expect(result.get('status')).toEqual('succeeded'); + // expect # less than # of batches used, assuming each batch is 100 pushes + expect(result.get('numSent')).toEqual(devices + devices / 100); + expect(result.get('count')).toEqual(undefined); + done(); }); - }).then(() => { - // query for push status - const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}); - }).then((results) => { - // verify status is NOT broken - expect(results.length).toBe(1); - const result = results[0]; - expect(result.get('status')).toEqual('succeeded'); - // expect # less than # of batches used, assuming each batch is 100 pushes - expect(result.get('numSent')).toEqual(devices + (devices / 100)); - expect(result.get('count')).toEqual(undefined); - done(); - }); }); }); diff --git a/spec/ParseACL.spec.js b/spec/ParseACL.spec.js index 24c4d6a825..80e5a40081 100644 --- a/spec/ParseACL.spec.js +++ b/spec/ParseACL.spec.js @@ -5,21 +5,24 @@ const Config = require('../lib/Config'); const auth = require('../lib/Auth'); describe('Parse.ACL', () => { - it("acl must be valid", (done) => { + it('acl must be valid', done => { const user = new Parse.User(); - ok(!user.setACL("Ceci n'est pas un ACL.", { - error: function(user, error) { - equal(error.code, -1); - done(); - } - }), "setACL should have returned false."); + ok( + !user.setACL("Ceci n'est pas un ACL.", { + error: function(user, error) { + equal(error.code, -1); + done(); + }, + }), + 'setACL should have returned false.' + ); }); - it("refresh object with acl", async (done) => { + it('refresh object with acl', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(null); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -29,11 +32,11 @@ describe('Parse.ACL', () => { done(); }); - it("acl an object owned by one user and public get", async (done) => { + it('acl an object owned by one user and public get', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -43,37 +46,37 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); await Parse.User.logOut(); const query = new Parse.Query(TestObject); try { await query.get(object.id); done.fail('Should not have retrieved the object.'); - } catch(error) { + } catch (error) { equal(error.code, Parse.Error.OBJECT_NOT_FOUND); done(); } }); - it("acl an object owned by one user and public find", async (done) => { + it('acl an object owned by one user and public find', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); object.setACL(acl); - await object.save() + await object.save(); equal(object.getACL().getReadAccess(user), true); equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Start making requests by the public, which should all fail. - await Parse.User.logOut() + await Parse.User.logOut(); // Find const query = new Parse.Query(TestObject); const results = await query.find(); @@ -81,11 +84,11 @@ describe('Parse.ACL', () => { done(); }); - it("acl an object owned by one user and public update", async (done) => { + it('acl an object owned by one user and public update', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); @@ -96,27 +99,27 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Start making requests by the public, which should all fail. - await Parse.User.logOut() + await Parse.User.logOut(); // Update - object.set("foo", "bar"); + object.set('foo', 'bar'); try { - await object.save() + await object.save(); done.fail('Should not have been able to update the object.'); - } catch(err) { + } catch (err) { equal(err.code, Parse.Error.OBJECT_NOT_FOUND); done(); } }); - it("acl an object owned by one user and public delete", async (done) => { + it('acl an object owned by one user and public delete', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); - await user.signUp() + user.set('username', 'alice'); + user.set('password', 'wonderland'); + await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -127,25 +130,25 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Start making requests by the public, which should all fail. - await Parse.User.logOut() + await Parse.User.logOut(); try { - await object.destroy() + await object.destroy(); done.fail('destroy should fail'); - } catch(error) { + } catch (error) { expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); done(); } }); - it("acl an object owned by one user and logged in get", async (done) => { + it('acl an object owned by one user and logged in get', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); - await user.signUp() + user.set('username', 'alice'); + user.set('password', 'wonderland'); + await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); object.setACL(acl); @@ -154,10 +157,10 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); await Parse.User.logOut(); - await Parse.User.logIn("alice", "wonderland"); + await Parse.User.logIn('alice', 'wonderland'); // Get const query = new Parse.Query(TestObject); const result = await query.get(object.id); @@ -167,15 +170,15 @@ describe('Parse.ACL', () => { equal(result.getACL().getWriteAccess(user), true); equal(result.getACL().getPublicReadAccess(), false); equal(result.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); done(); }); - it("acl an object owned by one user and logged in find", async (done) => { + it('acl an object owned by one user and logged in find', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -185,9 +188,9 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); - await Parse.User.logOut() - await Parse.User.logIn("alice", "wonderland"); + ok(object.get('ACL')); + await Parse.User.logOut(); + await Parse.User.logIn('alice', 'wonderland'); // Find const query = new Parse.Query(TestObject); const results = await query.find(); @@ -202,15 +205,15 @@ describe('Parse.ACL', () => { equal(result.getACL().getWriteAccess(user), true); equal(result.getACL().getPublicReadAccess(), false); equal(result.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); done(); }); - it("acl an object owned by one user and logged in update", async (done) => { + it('acl an object owned by one user and logged in update', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -220,43 +223,43 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); await Parse.User.logOut(); - await Parse.User.logIn("alice", "wonderland"); + await Parse.User.logIn('alice', 'wonderland'); // Update - object.set("foo", "bar"); + object.set('foo', 'bar'); await object.save(); done(); }); - it("acl an object owned by one user and logged in delete", async (done) => { + it('acl an object owned by one user and logged in delete', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); object.setACL(acl); - await object.save() + await object.save(); equal(object.getACL().getReadAccess(user), true); equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); - await Parse.User.logOut() - await Parse.User.logIn("alice", "wonderland"); + ok(object.get('ACL')); + await Parse.User.logOut(); + await Parse.User.logIn('alice', 'wonderland'); // Delete await object.destroy(); done(); }); - it("acl making an object publicly readable and public get", async (done) => { + it('acl making an object publicly readable and public get', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -266,7 +269,7 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Now make it public. object.getACL().setPublicReadAccess(true); @@ -275,9 +278,9 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), true); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); - await Parse.User.logOut() + await Parse.User.logOut(); // Get const query = new Parse.Query(TestObject); const result = await query.get(object.id); @@ -286,11 +289,11 @@ describe('Parse.ACL', () => { done(); }); - it("acl making an object publicly readable and public find", async (done) => { + it('acl making an object publicly readable and public find', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -300,7 +303,7 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Now make it public. object.getACL().setPublicReadAccess(true); @@ -309,9 +312,9 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), true); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); - await Parse.User.logOut() + await Parse.User.logOut(); // Find const query = new Parse.Query(TestObject); const results = await query.find(); @@ -322,11 +325,11 @@ describe('Parse.ACL', () => { done(); }); - it("acl making an object publicly readable and public update", async (done) => { + it('acl making an object publicly readable and public update', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -336,7 +339,7 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Now make it public. object.getACL().setPublicReadAccess(true); @@ -345,23 +348,26 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), true); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); - await Parse.User.logOut() - object.set("foo", "bar"); - object.save().then(() => { - fail('the save should fail'); - }, error => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); + await Parse.User.logOut(); + object.set('foo', 'bar'); + object.save().then( + () => { + fail('the save should fail'); + }, + error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + } + ); }); - it("acl making an object publicly readable and public delete", async (done) => { + it('acl making an object publicly readable and public delete', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -371,7 +377,7 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Now make it public. object.getACL().setPublicReadAccess(true); @@ -380,23 +386,26 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), true); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); Parse.User.logOut() .then(() => object.destroy()) - .then(() => { - fail('expected failure'); - }, error => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); + .then( + () => { + fail('expected failure'); + }, + error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + } + ); }); - it("acl making an object publicly writable and public get", async (done) => { + it('acl making an object publicly writable and public get', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -406,7 +415,7 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Now make it public. object.getACL().setPublicWriteAccess(true); @@ -415,24 +424,25 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), true); - ok(object.get("ACL")); + ok(object.get('ACL')); - await Parse.User.logOut() + await Parse.User.logOut(); // Get const query = new Parse.Query(TestObject); - query.get(object.id) + query + .get(object.id) .then(done.fail) - .catch((error) => { + .catch(error => { equal(error.code, Parse.Error.OBJECT_NOT_FOUND); done(); }); }); - it("acl making an object publicly writable and public find", async (done) => { + it('acl making an object publicly writable and public find', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -442,7 +452,7 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Now make it public. object.getACL().setPublicWriteAccess(true); @@ -451,9 +461,9 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), true); - ok(object.get("ACL")); + ok(object.get('ACL')); - await Parse.User.logOut() + await Parse.User.logOut(); // Find const query = new Parse.Query(TestObject); query.find().then(function(results) { @@ -462,11 +472,11 @@ describe('Parse.ACL', () => { }); }); - it("acl making an object publicly writable and public update", async (done) => { + it('acl making an object publicly writable and public update', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -476,7 +486,7 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Now make it public. object.getACL().setPublicWriteAccess(true); @@ -485,21 +495,20 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), true); - ok(object.get("ACL")); + ok(object.get('ACL')); - Parse.User.logOut() - .then(() => { + Parse.User.logOut().then(() => { // Update - object.set("foo", "bar"); - object.save().then(done); - }); + object.set('foo', 'bar'); + object.save().then(done); + }); }); - it("acl making an object publicly writable and public delete", async (done) => { + it('acl making an object publicly writable and public delete', async done => { // Create an object owned by Alice. const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); + user.set('username', 'alice'); + user.set('password', 'wonderland'); await user.signUp(); const object = new TestObject(); const acl = new Parse.ACL(user); @@ -509,7 +518,7 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); - ok(object.get("ACL")); + ok(object.get('ACL')); // Now make it public. object.getACL().setPublicWriteAccess(true); @@ -518,53 +527,60 @@ describe('Parse.ACL', () => { equal(object.getACL().getWriteAccess(user), true); equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), true); - ok(object.get("ACL")); + ok(object.get('ACL')); - Parse.User.logOut() - .then(() => { - // Delete - object.destroy().then(done); - }); + Parse.User.logOut().then(() => { + // Delete + object.destroy().then(done); + }); }); - it("acl making an object privately writable (#3194)", (done) => { + it('acl making an object privately writable (#3194)', done => { // Create an object owned by Alice. let object; let user2; const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "wonderland"); - user.signUp().then(() => { - object = new TestObject(); - const acl = new Parse.ACL(user); - acl.setPublicWriteAccess(false); - acl.setPublicReadAccess(true); - object.setACL(acl); - return object.save().then(() => { - return Parse.User.logOut(); + user.set('username', 'alice'); + user.set('password', 'wonderland'); + user + .signUp() + .then(() => { + object = new TestObject(); + const acl = new Parse.ACL(user); + acl.setPublicWriteAccess(false); + acl.setPublicReadAccess(true); + object.setACL(acl); + return object.save().then(() => { + return Parse.User.logOut(); + }); }) - }).then(() => { - user2 = new Parse.User(); - user2.set("username", "bob"); - user2.set("password", "burger"); - return user2.signUp(); - }).then(() => { - return object.destroy({sessionToken: user2.getSessionToken() }); - }).then(() => { - fail('should not be able to destroy the object'); - done(); - }, (err) => { - expect(err).not.toBeUndefined(); - done(); - }); + .then(() => { + user2 = new Parse.User(); + user2.set('username', 'bob'); + user2.set('password', 'burger'); + return user2.signUp(); + }) + .then(() => { + return object.destroy({ sessionToken: user2.getSessionToken() }); + }) + .then( + () => { + fail('should not be able to destroy the object'); + done(); + }, + err => { + expect(err).not.toBeUndefined(); + done(); + } + ); }); - it("acl sharing with another user and get", async (done) => { + it('acl sharing with another user and get', async done => { // Sign in as Bob. - const bob = await Parse.User.signUp("bob", "pass"); - await Parse.User.logOut() + const bob = await Parse.User.signUp('bob', 'pass'); + await Parse.User.logOut(); - const alice = await Parse.User.signUp("alice", "wonderland"); + const alice = await Parse.User.signUp('alice', 'wonderland'); // Create an object shared by Bob and Alice. const object = new TestObject(); const acl = new Parse.ACL(alice); @@ -580,21 +596,21 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); // Sign in as Bob again. - await Parse.User.logIn("bob", "pass"); + await Parse.User.logIn('bob', 'pass'); const query = new Parse.Query(TestObject); - query.get(object.id).then((result) => { + query.get(object.id).then(result => { ok(result); equal(result.id, object.id); done(); }); }); - it("acl sharing with another user and find", async (done) => { + it('acl sharing with another user and find', async done => { // Sign in as Bob. - const bob = await Parse.User.signUp("bob", "pass"); - await Parse.User.logOut() + const bob = await Parse.User.signUp('bob', 'pass'); + await Parse.User.logOut(); // Sign in as Alice. - const alice = await Parse.User.signUp("alice", "wonderland"); + const alice = await Parse.User.signUp('alice', 'wonderland'); // Create an object shared by Bob and Alice. const object = new TestObject(); const acl = new Parse.ACL(alice); @@ -610,14 +626,14 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); // Sign in as Bob again. - await Parse.User.logIn("bob", "pass"); + await Parse.User.logIn('bob', 'pass'); const query = new Parse.Query(TestObject); - query.find().then((results) => { + query.find().then(results => { equal(results.length, 1); const result = results[0]; ok(result); if (!result) { - fail("should have result"); + fail('should have result'); } else { equal(result.id, object.id); } @@ -625,12 +641,12 @@ describe('Parse.ACL', () => { }); }); - it("acl sharing with another user and update", async (done) => { + it('acl sharing with another user and update', async done => { // Sign in as Bob. - const bob = await Parse.User.signUp("bob", "pass"); - await Parse.User.logOut() + const bob = await Parse.User.signUp('bob', 'pass'); + await Parse.User.logOut(); // Sign in as Alice. - const alice = await Parse.User.signUp("alice", "wonderland"); + const alice = await Parse.User.signUp('alice', 'wonderland'); // Create an object shared by Bob and Alice. const object = new TestObject(); const acl = new Parse.ACL(alice); @@ -646,17 +662,17 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); // Sign in as Bob again. - await Parse.User.logIn("bob", "pass"); - object.set("foo", "bar"); + await Parse.User.logIn('bob', 'pass'); + object.set('foo', 'bar'); object.save().then(done); }); - it("acl sharing with another user and delete", async (done) => { + it('acl sharing with another user and delete', async done => { // Sign in as Bob. - const bob = await Parse.User.signUp("bob", "pass"); + const bob = await Parse.User.signUp('bob', 'pass'); await Parse.User.logOut(); // Sign in as Alice. - const alice = await Parse.User.signUp("alice", "wonderland"); + const alice = await Parse.User.signUp('alice', 'wonderland'); // Create an object shared by Bob and Alice. const object = new TestObject(); const acl = new Parse.ACL(alice); @@ -672,16 +688,16 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); // Sign in as Bob again. - await Parse.User.logIn("bob", "pass"); - object.set("foo", "bar"); + await Parse.User.logIn('bob', 'pass'); + object.set('foo', 'bar'); object.destroy().then(done); }); - it("acl sharing with another user and public get", async (done) => { - const bob = await Parse.User.signUp("bob", "pass"); + it('acl sharing with another user and public get', async done => { + const bob = await Parse.User.signUp('bob', 'pass'); await Parse.User.logOut(); // Sign in as Alice. - const alice = await Parse.User.signUp("alice", "wonderland"); + const alice = await Parse.User.signUp('alice', 'wonderland'); // Create an object shared by Bob and Alice. const object = new TestObject(); const acl = new Parse.ACL(alice); @@ -696,21 +712,24 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicReadAccess(), false); equal(object.getACL().getPublicWriteAccess(), false); // Start making requests by the public. - await Parse.User.logOut() + await Parse.User.logOut(); const query = new Parse.Query(TestObject); - query.get(object.id).then((result) => { - fail(result); - }, (error) => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); + query.get(object.id).then( + result => { + fail(result); + }, + error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + } + ); }); - it("acl sharing with another user and public find", async (done) => { - const bob = await Parse.User.signUp("bob", "pass"); + it('acl sharing with another user and public find', async done => { + const bob = await Parse.User.signUp('bob', 'pass'); await Parse.User.logOut(); // Sign in as Alice. - const alice = await Parse.User.signUp("alice", "wonderland"); + const alice = await Parse.User.signUp('alice', 'wonderland'); // Create an object shared by Bob and Alice. const object = new TestObject(); const acl = new Parse.ACL(alice); @@ -726,22 +745,21 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); // Start making requests by the public. - Parse.User.logOut() - .then(() => { - const query = new Parse.Query(TestObject); - query.find().then(function(results) { - equal(results.length, 0); - done(); - }); + Parse.User.logOut().then(() => { + const query = new Parse.Query(TestObject); + query.find().then(function(results) { + equal(results.length, 0); + done(); }); + }); }); - it("acl sharing with another user and public update", async (done) => { + it('acl sharing with another user and public update', async done => { // Sign in as Bob. - const bob = await Parse.User.signUp("bob", "pass"); + const bob = await Parse.User.signUp('bob', 'pass'); await Parse.User.logOut(); // Sign in as Alice. - const alice = await Parse.User.signUp("alice", "wonderland"); + const alice = await Parse.User.signUp('alice', 'wonderland'); // Create an object shared by Bob and Alice. const object = new TestObject(); const acl = new Parse.ACL(alice); @@ -757,24 +775,26 @@ describe('Parse.ACL', () => { equal(object.getACL().getPublicWriteAccess(), false); // Start making requests by the public. - Parse.User.logOut() - .then(() => { - object.set("foo", "bar"); - object.save().then(() => { + Parse.User.logOut().then(() => { + object.set('foo', 'bar'); + object.save().then( + () => { fail('expected failure'); - }, (error) => { + }, + error => { expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); done(); - }); - }); + } + ); + }); }); - it("acl sharing with another user and public delete", async (done) => { + it('acl sharing with another user and public delete', async done => { // Sign in as Bob. - const bob = await Parse.User.signUp("bob", "pass"); + const bob = await Parse.User.signUp('bob', 'pass'); await Parse.User.logOut(); // Sign in as Alice. - const alice = await Parse.User.signUp("alice", "wonderland"); + const alice = await Parse.User.signUp('alice', 'wonderland'); // Create an object shared by Bob and Alice. const object = new TestObject(); const acl = new Parse.ACL(alice); @@ -792,16 +812,19 @@ describe('Parse.ACL', () => { // Start making requests by the public. Parse.User.logOut() .then(() => object.destroy()) - .then(() => { - fail('expected failure'); - }, (error) => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); + .then( + () => { + fail('expected failure'); + }, + error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + } + ); }); - it("acl saveAll with permissions", async (done) => { - const alice = await Parse.User.signUp("alice", "wonderland"); + it('acl saveAll with permissions', async done => { + const alice = await Parse.User.signUp('alice', 'wonderland'); const acl = new Parse.ACL(alice); const object1 = new TestObject(); const object2 = new TestObject(); @@ -818,59 +841,62 @@ describe('Parse.ACL', () => { equal(object2.getACL().getPublicWriteAccess(), false); // Save all the objects after updating them. - object1.set("foo", "bar"); - object2.set("foo", "bar"); + object1.set('foo', 'bar'); + object2.set('foo', 'bar'); await Parse.Object.saveAll([object1, object2]); const query = new Parse.Query(TestObject); - query.equalTo("foo", "bar"); + query.equalTo('foo', 'bar'); query.find().then(function(results) { equal(results.length, 2); done(); }); }); - it("empty acl works", async (done) => { - await Parse.User.signUp("tdurden", "mayhem", { + it('empty acl works', async done => { + await Parse.User.signUp('tdurden', 'mayhem', { ACL: new Parse.ACL(), - foo: "bar" + foo: 'bar', }); - await Parse.User.logOut() - const user = await Parse.User.logIn("tdurden", "mayhem"); - equal(user.get("foo"), "bar"); + await Parse.User.logOut(); + const user = await Parse.User.logIn('tdurden', 'mayhem'); + equal(user.get('foo'), 'bar'); done(); }); - it("query for included object with ACL works", async (done) => { - const obj1 = new Parse.Object("TestClass1"); - const obj2 = new Parse.Object("TestClass2"); + it('query for included object with ACL works', async done => { + const obj1 = new Parse.Object('TestClass1'); + const obj2 = new Parse.Object('TestClass2'); const acl = new Parse.ACL(); acl.setPublicReadAccess(true); - obj2.set("ACL", acl); - obj1.set("other", obj2); + obj2.set('ACL', acl); + obj1.set('other', obj2); await obj1.save(); obj2._clearServerData(); - const query = new Parse.Query("TestClass1"); + const query = new Parse.Query('TestClass1'); const obj1Again = await query.first(); - ok(!obj1Again.get("other").get("ACL")); + ok(!obj1Again.get('other').get('ACL')); - query.include("other"); + query.include('other'); const obj1AgainWithInclude = await query.first(); - ok(obj1AgainWithInclude.get("other").get("ACL")); + ok(obj1AgainWithInclude.get('other').get('ACL')); done(); }); - it('restricted ACL does not have public access', (done) => { - const obj = new Parse.Object("TestClassMasterACL"); + it('restricted ACL does not have public access', done => { + const obj = new Parse.Object('TestClassMasterACL'); const acl = new Parse.ACL(); obj.set('ACL', acl); - obj.save().then(() => { - const query = new Parse.Query("TestClassMasterACL"); - return query.find(); - }).then((results) => { - ok(!results.length, 'Should not have returned object with secure ACL.'); - done(); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('TestClassMasterACL'); + return query.find(); + }) + .then(results => { + ok(!results.length, 'Should not have returned object with secure ACL.'); + done(); + }); }); it('regression test #701', done => { @@ -878,9 +904,9 @@ describe('Parse.ACL', () => { const anonUser = { authData: { anonymous: { - id: '00000000-0000-0000-0000-000000000001' - } - } + id: '00000000-0000-0000-0000-000000000001', + }, + }, }; Parse.Cloud.afterSave(Parse.User, req => { @@ -888,18 +914,21 @@ describe('Parse.ACL', () => { const user = req.object; const acl = new Parse.ACL(user); user.setACL(acl); - user.save(null, {useMasterKey: true}).then(user => { - new Parse.Query('_User').get(user.objectId).then(() => { - fail('should not have fetched user without public read enabled'); - done(); - }, error => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); + user.save(null, { useMasterKey: true }).then(user => { + new Parse.Query('_User').get(user.objectId).then( + () => { + fail('should not have fetched user without public read enabled'); + done(); + }, + error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + } + ); }, done.fail); } }); - rest.create(config, auth.nobody(config), '_User', anonUser) - }) + rest.create(config, auth.nobody(config), '_User', anonUser); + }); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 064a43f705..2d1d7a5068 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -4,43 +4,59 @@ const request = require('request'); const rp = require('request-promise'); -const Parse = require("parse/node"); +const Parse = require('parse/node'); const Config = require('../lib/Config'); const SchemaController = require('../lib/Controllers/SchemaController'); const TestUtils = require('../lib/TestUtils'); -const userSchema = SchemaController.convertSchemaToAdapterSchema({ className: '_User', fields: Object.assign({}, SchemaController.defaultColumns._Default, SchemaController.defaultColumns._User) }); +const userSchema = SchemaController.convertSchemaToAdapterSchema({ + className: '_User', + fields: Object.assign( + {}, + SchemaController.defaultColumns._Default, + SchemaController.defaultColumns._User + ), +}); const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Installation-Id': 'yolo' -} + 'X-Parse-Installation-Id': 'yolo', +}; describe_only_db('mongo')('miscellaneous', () => { it('test rest_create_app', function(done) { let appId; - Parse._request('POST', 'rest_create_app').then((res) => { - expect(typeof res.application_id).toEqual('string'); - expect(res.master_key).toEqual('master'); - appId = res.application_id; - Parse.initialize(appId, 'unused'); - const obj = new Parse.Object('TestObject'); - obj.set('foo', 'bar'); - return obj.save(); - }).then(() => { - const config = Config.get(appId); - return config.database.adapter.find('TestObject', { fields: {} }, {}, {}); - }).then((results) => { - expect(results.length).toEqual(1); - expect(results[0]['foo']).toEqual('bar'); - done(); - }).catch(error => { - fail(JSON.stringify(error)); - done(); - }) + Parse._request('POST', 'rest_create_app') + .then(res => { + expect(typeof res.application_id).toEqual('string'); + expect(res.master_key).toEqual('master'); + appId = res.application_id; + Parse.initialize(appId, 'unused'); + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + return obj.save(); + }) + .then(() => { + const config = Config.get(appId); + return config.database.adapter.find( + 'TestObject', + { fields: {} }, + {}, + {} + ); + }) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0]['foo']).toEqual('bar'); + done(); + }) + .catch(error => { + fail(JSON.stringify(error)); + done(); + }); }); -}) +}); describe('miscellaneous', function() { it('create a GameScore object', function(done) { @@ -54,7 +70,7 @@ describe('miscellaneous', function() { }); it('get a TestObject', function(done) { - create({ 'bloop' : 'blarg' }, async function(obj) { + create({ bloop: 'blarg' }, async function(obj) { const t2 = new TestObject({ objectId: obj.id }); const obj2 = await t2.fetch(); expect(obj2.get('bloop')).toEqual('blarg'); @@ -76,36 +92,46 @@ describe('miscellaneous', function() { it('fail to create a duplicate username', async () => { let numFailed = 0; let numCreated = 0; - const p1 = rp.post(Parse.serverURL + '/users', { - json: { - password: 'asdf', - username: 'u1', - email: 'dupe@dupe.dupe' - }, - headers - }).then(() => { - numCreated++; - expect(numCreated).toEqual(1); - }, ({ error }) => { - numFailed++; - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); - - const p2 = rp.post(Parse.serverURL + '/users', { - json: { - password: 'otherpassword', - username: 'u1', - email: 'email@other.email' - }, - headers - }).then(() => { - numCreated++; - }, ({ error }) => { - numFailed++; - expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); - }); + const p1 = rp + .post(Parse.serverURL + '/users', { + json: { + password: 'asdf', + username: 'u1', + email: 'dupe@dupe.dupe', + }, + headers, + }) + .then( + () => { + numCreated++; + expect(numCreated).toEqual(1); + }, + ({ error }) => { + numFailed++; + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + } + ); + + const p2 = rp + .post(Parse.serverURL + '/users', { + json: { + password: 'otherpassword', + username: 'u1', + email: 'email@other.email', + }, + headers, + }) + .then( + () => { + numCreated++; + }, + ({ error }) => { + numFailed++; + expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); + } + ); - await Promise.all([p1, p2]) + await Promise.all([p1, p2]); expect(numFailed).toEqual(1); expect(numCreated).toBe(1); }); @@ -113,37 +139,47 @@ describe('miscellaneous', function() { it('ensure that email is uniquely indexed', async () => { let numFailed = 0; let numCreated = 0; - const p1 = rp.post(Parse.serverURL + '/users', { - json: { - password: 'asdf', - username: 'u1', - email: 'dupe@dupe.dupe' - }, - headers - }).then(() => { - numCreated++; - expect(numCreated).toEqual(1); - }, ({ error }) => { - numFailed++; - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); - - const p2 = rp.post(Parse.serverURL + '/users', { - json: { - password: 'asdf', - username: 'u2', - email: 'dupe@dupe.dupe' - }, - headers - }).then(() => { - numCreated++; - expect(numCreated).toEqual(1); - }, ({ error }) => { - numFailed++; - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); + const p1 = rp + .post(Parse.serverURL + '/users', { + json: { + password: 'asdf', + username: 'u1', + email: 'dupe@dupe.dupe', + }, + headers, + }) + .then( + () => { + numCreated++; + expect(numCreated).toEqual(1); + }, + ({ error }) => { + numFailed++; + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + } + ); + + const p2 = rp + .post(Parse.serverURL + '/users', { + json: { + password: 'asdf', + username: 'u2', + email: 'dupe@dupe.dupe', + }, + headers, + }) + .then( + () => { + numCreated++; + expect(numCreated).toEqual(1); + }, + ({ error }) => { + numFailed++; + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + } + ); - await Promise.all([p1, p2]) + await Promise.all([p1, p2]); expect(numFailed).toEqual(1); expect(numCreated).toBe(1); }); @@ -151,14 +187,24 @@ describe('miscellaneous', function() { it('ensure that if people already have duplicate users, they can still sign up new users', async done => { try { await Parse.User.logOut(); - } catch(e) { /* ignore */ } + } catch (e) { + /* ignore */ + } const config = Config.get('test'); // Remove existing data to clear out unique index TestUtils.destroyAllDataPermanently() .then(() => config.database.adapter.createClass('_User', userSchema)) - .then(() => config.database.adapter.createObject('_User', userSchema, { objectId: 'x', username: 'u' }).catch(fail)) - .then(() => config.database.adapter.createObject('_User', userSchema, { objectId: 'y', username: 'u' }).catch(fail)) - // Create a new server to try to recreate the unique indexes + .then(() => + config.database.adapter + .createObject('_User', userSchema, { objectId: 'x', username: 'u' }) + .catch(fail) + ) + .then(() => + config.database.adapter + .createObject('_User', userSchema, { objectId: 'y', username: 'u' }) + .catch(fail) + ) + // Create a new server to try to recreate the unique indexes .then(reconfigureServer) .catch(error => { expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); @@ -171,7 +217,7 @@ describe('miscellaneous', function() { const user = new Parse.User(); user.setPassword('asdf'); user.setUsername('u'); - return user.signUp() + return user.signUp(); }) .then(() => { fail('should not have been able to sign up'); @@ -180,7 +226,7 @@ describe('miscellaneous', function() { .catch(error => { expect(error.code).toEqual(Parse.Error.USERNAME_TAKEN); done(); - }) + }); }); it('ensure that if people already have duplicate emails, they can still sign up new users', done => { @@ -188,8 +234,18 @@ describe('miscellaneous', function() { // Remove existing data to clear out unique index TestUtils.destroyAllDataPermanently() .then(() => config.database.adapter.createClass('_User', userSchema)) - .then(() => config.database.adapter.createObject('_User', userSchema, { objectId: 'x', email: 'a@b.c' })) - .then(() => config.database.adapter.createObject('_User', userSchema, { objectId: 'y', email: 'a@b.c' })) + .then(() => + config.database.adapter.createObject('_User', userSchema, { + objectId: 'x', + email: 'a@b.c', + }) + ) + .then(() => + config.database.adapter.createObject('_User', userSchema, { + objectId: 'y', + email: 'a@b.c', + }) + ) .then(reconfigureServer) .catch(() => { const user = new Parse.User(); @@ -203,7 +259,7 @@ describe('miscellaneous', function() { user.setPassword('asdf'); user.setUsername('www'); user.setEmail('a@b.c'); - return user.signUp() + return user.signUp(); }) .catch(error => { expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); @@ -213,15 +269,20 @@ describe('miscellaneous', function() { it('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', done => { const config = Config.get('test'); - config.database.adapter.addFieldIfNotExists('_User', 'randomField', { type: 'String' }) - .then(() => config.database.adapter.ensureUniqueness('_User', userSchema, ['randomField'])) + config.database.adapter + .addFieldIfNotExists('_User', 'randomField', { type: 'String' }) + .then(() => + config.database.adapter.ensureUniqueness('_User', userSchema, [ + 'randomField', + ]) + ) .then(() => { const user = new Parse.User(); user.setPassword('asdf'); user.setUsername('1'); user.setEmail('1@b.c'); user.set('randomField', 'a'); - return user.signUp() + return user.signUp(); }) .then(() => { const user = new Parse.User(); @@ -229,7 +290,7 @@ describe('miscellaneous', function() { user.setUsername('2'); user.setEmail('2@b.c'); user.set('randomField', 'a'); - return user.signUp() + return user.signUp(); }) .catch(error => { expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); @@ -251,97 +312,123 @@ describe('miscellaneous', function() { }); it('increment with a user object', function(done) { - createTestUser().then((user) => { - user.increment('foo'); - return user.save(); - }).then(() => { - return Parse.User.logIn('test', 'moon-y'); - }).then((user) => { - expect(user.get('foo')).toEqual(1); - user.increment('foo'); - return user.save(); - }).then(() => Parse.User.logOut()) + createTestUser() + .then(user => { + user.increment('foo'); + return user.save(); + }) + .then(() => { + return Parse.User.logIn('test', 'moon-y'); + }) + .then(user => { + expect(user.get('foo')).toEqual(1); + user.increment('foo'); + return user.save(); + }) + .then(() => Parse.User.logOut()) .then(() => Parse.User.logIn('test', 'moon-y')) - .then((user) => { - expect(user.get('foo')).toEqual(2); - Parse.User.logOut() - .then(done); - }, (error) => { - fail(JSON.stringify(error)); - done(); - }); + .then( + user => { + expect(user.get('foo')).toEqual(2); + Parse.User.logOut().then(done); + }, + error => { + fail(JSON.stringify(error)); + done(); + } + ); }); it('save various data types', function(done) { const obj = new TestObject(); obj.set('date', new Date()); obj.set('array', [1, 2, 3]); - obj.set('object', {one: 1, two: 2}); - obj.save().then(() => { - const obj2 = new TestObject({objectId: obj.id}); - return obj2.fetch(); - }).then((obj2) => { - expect(obj2.get('date') instanceof Date).toBe(true); - expect(obj2.get('array') instanceof Array).toBe(true); - expect(obj2.get('object') instanceof Array).toBe(false); - expect(obj2.get('object') instanceof Object).toBe(true); - done(); - }); + obj.set('object', { one: 1, two: 2 }); + obj + .save() + .then(() => { + const obj2 = new TestObject({ objectId: obj.id }); + return obj2.fetch(); + }) + .then(obj2 => { + expect(obj2.get('date') instanceof Date).toBe(true); + expect(obj2.get('array') instanceof Array).toBe(true); + expect(obj2.get('object') instanceof Array).toBe(false); + expect(obj2.get('object') instanceof Object).toBe(true); + done(); + }); }); it('query with limit', function(done) { const baz = new TestObject({ foo: 'baz' }); const qux = new TestObject({ foo: 'qux' }); - baz.save().then(() => { - return qux.save(); - }).then(() => { - const query = new Parse.Query(TestObject); - query.limit(1); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(1); - done(); - }, (error) => { - fail(JSON.stringify(error)); - done(); - }); + baz + .save() + .then(() => { + return qux.save(); + }) + .then(() => { + const query = new Parse.Query(TestObject); + query.limit(1); + return query.find(); + }) + .then( + results => { + expect(results.length).toEqual(1); + done(); + }, + error => { + fail(JSON.stringify(error)); + done(); + } + ); }); it('query without limit get default 100 records', function(done) { const objects = []; - for (let i = 0; i < 150; i++) { - objects.push(new TestObject({name: 'name' + i})); + for (let i = 0; i < 150; i++) { + objects.push(new TestObject({ name: 'name' + i })); } - Parse.Object.saveAll(objects).then(() => { - return new Parse.Query(TestObject).find(); - }).then((results) => { - expect(results.length).toEqual(100); - done(); - }, error => { - fail(JSON.stringify(error)); - done(); - }); + Parse.Object.saveAll(objects) + .then(() => { + return new Parse.Query(TestObject).find(); + }) + .then( + results => { + expect(results.length).toEqual(100); + done(); + }, + error => { + fail(JSON.stringify(error)); + done(); + } + ); }); it('basic saveAll', function(done) { const alpha = new TestObject({ letter: 'alpha' }); const beta = new TestObject({ letter: 'beta' }); - Parse.Object.saveAll([alpha, beta]).then(() => { - expect(alpha.id).toBeTruthy(); - expect(beta.id).toBeTruthy(); - return new Parse.Query(TestObject).find(); - }).then((results) => { - expect(results.length).toEqual(2); - done(); - }, (error) => { - fail(error); - done(); - }); + Parse.Object.saveAll([alpha, beta]) + .then(() => { + expect(alpha.id).toBeTruthy(); + expect(beta.id).toBeTruthy(); + return new Parse.Query(TestObject).find(); + }) + .then( + results => { + expect(results.length).toEqual(2); + done(); + }, + error => { + fail(error); + done(); + } + ); }); it('test beforeSave set object acl success', function(done) { const acl = new Parse.ACL({ - '*': { read: true, write: false } + '*': { read: true, write: false }, }); Parse.Cloud.beforeSave('BeforeSaveAddACL', function(req) { req.object.setACL(acl); @@ -349,26 +436,32 @@ describe('miscellaneous', function() { const obj = new Parse.Object('BeforeSaveAddACL'); obj.set('lol', true); - obj.save().then(function() { - const query = new Parse.Query('BeforeSaveAddACL'); - query.get(obj.id).then(function(objAgain) { - expect(objAgain.get('lol')).toBeTruthy(); - expect(objAgain.getACL().equals(acl)); - done(); - }, function(error) { - fail(error); + obj.save().then( + function() { + const query = new Parse.Query('BeforeSaveAddACL'); + query.get(obj.id).then( + function(objAgain) { + expect(objAgain.get('lol')).toBeTruthy(); + expect(objAgain.getACL().equals(acl)); + done(); + }, + function(error) { + fail(error); + done(); + } + ); + }, + error => { + fail(JSON.stringify(error)); done(); - }); - }, error => { - fail(JSON.stringify(error)); - done(); - }); + } + ); }); it('object is set on create and update', done => { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.beforeSave('GameScore', (req) => { + Parse.Cloud.beforeSave('GameScore', req => { const object = req.object; expect(object instanceof Parse.Object).toBeTruthy(); expect(object.get('fooAgain')).toEqual('barAgain'); @@ -394,23 +487,29 @@ describe('miscellaneous', function() { const 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(); - }, error => { - fail(error); - done(); - }); + 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(); + }, + error => { + fail(error); + done(); + } + ); }); it('works when object is passed to success', done => { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.beforeSave('GameScore', (req) => { + Parse.Cloud.beforeSave('GameScore', req => { const object = req.object; object.set('foo', 'bar'); triggerTime++; @@ -419,20 +518,23 @@ describe('miscellaneous', function() { const obj = new Parse.Object('GameScore'); obj.set('foo', 'baz'); - obj.save().then(() => { - expect(triggerTime).toBe(1); - expect(obj.get('foo')).toEqual('bar'); - done(); - }, error => { - fail(error); - done(); - }); + obj.save().then( + () => { + expect(triggerTime).toBe(1); + expect(obj.get('foo')).toEqual('bar'); + done(); + }, + error => { + fail(error); + done(); + } + ); }); it('original object is set on update', done => { let triggerTime = 0; // Register a mock beforeSave hook - Parse.Cloud.beforeSave('GameScore', (req) => { + Parse.Cloud.beforeSave('GameScore', req => { const object = req.object; expect(object instanceof Parse.Object).toBeTruthy(); expect(object.get('fooAgain')).toEqual('barAgain'); @@ -468,24 +570,30 @@ describe('miscellaneous', function() { const 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(); - }, error => { - fail(error); - done(); - }); + 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(); + }, + error => { + fail(error); + done(); + } + ); }); it('pointer mutation properly saves object', done => { const className = 'GameScore'; - Parse.Cloud.beforeSave(className, (req) => { + Parse.Cloud.beforeSave(className, req => { const object = req.object; expect(object instanceof Parse.Object).toBeTruthy(); @@ -499,56 +607,65 @@ describe('miscellaneous', function() { obj.set('foo', 'bar'); const child = new Parse.Object('Child'); - child.save().then(() => { - obj.set('child', child); - return obj.save(); - }).then(() => { - const query = new Parse.Query(className); - query.include('child'); - return query.get(obj.id).then(objAgain => { - expect(objAgain.get('foo')).toEqual('bar'); - - const childAgain = objAgain.get('child'); - expect(childAgain instanceof Parse.Object).toBeTruthy(); - expect(childAgain.get('a')).toEqual('b'); - - return Promise.resolve(); - }); - }).then(() => { - done(); - }, error => { - fail(error); - done(); - }); - }); + child + .save() + .then(() => { + obj.set('child', child); + return obj.save(); + }) + .then(() => { + const query = new Parse.Query(className); + query.include('child'); + return query.get(obj.id).then(objAgain => { + expect(objAgain.get('foo')).toEqual('bar'); - it('pointer reassign is working properly (#1288)', (done) => { - Parse.Cloud.beforeSave('GameScore', (req) => { + const childAgain = objAgain.get('child'); + expect(childAgain instanceof Parse.Object).toBeTruthy(); + expect(childAgain.get('a')).toEqual('b'); + + return Promise.resolve(); + }); + }) + .then( + () => { + done(); + }, + error => { + fail(error); + done(); + } + ); + }); + it('pointer reassign is working properly (#1288)', done => { + Parse.Cloud.beforeSave('GameScore', req => { const obj = req.object; if (obj.get('point')) { return; } const TestObject1 = Parse.Object.extend('TestObject1'); - const newObj = new TestObject1({'key1': 1}); + const newObj = new TestObject1({ key1: 1 }); - return newObj.save().then((newObj) => { - obj.set('point' , newObj); + return newObj.save().then(newObj => { + obj.set('point', newObj); }); }); let pointId; const obj = new Parse.Object('GameScore'); obj.set('foo', 'bar'); - obj.save().then(() => { - expect(obj.get('point')).not.toBeUndefined(); - pointId = obj.get('point').id; - expect(pointId).not.toBeUndefined(); - obj.set('foo', 'baz'); - return obj.save(); - }).then((obj) => { - expect(obj.get('point').id).toEqual(pointId); - done(); - }) + obj + .save() + .then(() => { + expect(obj.get('point')).not.toBeUndefined(); + pointId = obj.get('point').id; + expect(pointId).not.toBeUndefined(); + obj.set('foo', 'baz'); + return obj.save(); + }) + .then(obj => { + expect(obj.get('point').id).toEqual(pointId); + done(); + }); }); it('test afterSave get full object on create and update', function(done) { @@ -576,18 +693,24 @@ describe('miscellaneous', function() { const obj = new Parse.Object('GameScore'); obj.set('foo', 'bar'); obj.set('fooAgain', 'barAgain'); - obj.save().then(function() { - // We only update foo - obj.set('foo', 'baz'); - return obj.save(); - }).then(function() { - // Make sure the checking has been triggered - expect(triggerTime).toBe(2); - done(); - }, function(error) { - fail(error); - done(); - }); + obj + .save() + .then(function() { + // We only update foo + obj.set('foo', 'baz'); + return obj.save(); + }) + .then( + function() { + // Make sure the checking has been triggered + expect(triggerTime).toBe(2); + done(); + }, + function(error) { + fail(error); + done(); + } + ); }); it('test afterSave get original object on update', function(done) { @@ -626,21 +749,27 @@ describe('miscellaneous', function() { const obj = new Parse.Object('GameScore'); obj.set('foo', 'bar'); obj.set('fooAgain', 'barAgain'); - obj.save().then(function() { - // We only update foo - obj.set('foo', 'baz'); - return obj.save(); - }).then(function() { - // Make sure the checking has been triggered - expect(triggerTime).toBe(2); - done(); - }, function(error) { - jfail(error); - done(); - }); + obj + .save() + .then(function() { + // We only update foo + obj.set('foo', 'baz'); + return obj.save(); + }) + .then( + function() { + // Make sure the checking has been triggered + expect(triggerTime).toBe(2); + done(); + }, + function(error) { + jfail(error); + done(); + } + ); }); - it('test afterSave get full original object even req auth can not query it', (done) => { + it('test afterSave get full original object even req auth can not query it', done => { let triggerTime = 0; // Register a mock beforeSave hook Parse.Cloud.afterSave('GameScore', function(req) { @@ -672,18 +801,24 @@ describe('miscellaneous', function() { acl.setPublicReadAccess(false); acl.setPublicWriteAccess(true); obj.setACL(acl); - obj.save().then(function() { - // We only update foo - obj.set('foo', 'baz'); - return obj.save(); - }).then(function() { - // Make sure the checking has been triggered - expect(triggerTime).toBe(2); - done(); - }, function(error) { - jfail(error); - done(); - }); + obj + .save() + .then(function() { + // We only update foo + obj.set('foo', 'baz'); + return obj.save(); + }) + .then( + function() { + // Make sure the checking has been triggered + expect(triggerTime).toBe(2); + done(); + }, + function(error) { + jfail(error); + done(); + } + ); }); it('afterSave flattens custom operations', done => { @@ -709,17 +844,23 @@ describe('miscellaneous', function() { const obj = new Parse.Object('GameScore'); obj.increment('yolo', 1); - obj.save().then(() => { - obj.increment('yolo', 1); - return obj.save(); - }).then(() => { - // Make sure the checking has been triggered - expect(triggerTime).toBe(2); - done(); - }, error => { - jfail(error); - done(); - }); + obj + .save() + .then(() => { + obj.increment('yolo', 1); + return obj.save(); + }) + .then( + () => { + // Make sure the checking has been triggered + expect(triggerTime).toBe(2); + done(); + }, + error => { + jfail(error); + done(); + } + ); }); it('beforeSave receives ACL', done => { @@ -746,18 +887,24 @@ describe('miscellaneous', function() { acl.setPublicReadAccess(true); acl.setPublicWriteAccess(true); obj.setACL(acl); - obj.save().then(() => { - acl.setPublicReadAccess(false); - obj.setACL(acl); - return obj.save(); - }).then(() => { - // Make sure the checking has been triggered - expect(triggerTime).toBe(2); - done(); - }, error => { - jfail(error); - done(); - }); + obj + .save() + .then(() => { + acl.setPublicReadAccess(false); + obj.setACL(acl); + return obj.save(); + }) + .then( + () => { + // Make sure the checking has been triggered + expect(triggerTime).toBe(2); + done(); + }, + error => { + jfail(error); + done(); + } + ); }); it('afterSave receives ACL', done => { @@ -784,111 +931,136 @@ describe('miscellaneous', function() { acl.setPublicReadAccess(true); acl.setPublicWriteAccess(true); obj.setACL(acl); - obj.save().then(() => { - acl.setPublicReadAccess(false); - obj.setACL(acl); - return obj.save(); - }).then(() => { - // Make sure the checking has been triggered - expect(triggerTime).toBe(2); - done(); - }, error => { - jfail(error); - done(); - }); + obj + .save() + .then(() => { + acl.setPublicReadAccess(false); + obj.setACL(acl); + return obj.save(); + }) + .then( + () => { + // Make sure the checking has been triggered + expect(triggerTime).toBe(2); + done(); + }, + error => { + jfail(error); + done(); + } + ); }); it('should return the updated fields on PUT', done => { const obj = new Parse.Object('GameScore'); - obj.save({a:'hello', c: 1, d: ['1'], e:['1'], f:['1','2']}).then(() => { - const headers = { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Installation-Id': 'yolo' - }; - request.put({ - headers: headers, - url: 'http://localhost:8378/1/classes/GameScore/' + obj.id, - body: JSON.stringify({ - a: 'b', - c: {"__op":"Increment","amount":2}, - d: {"__op":"Add", objects: ['2']}, - e: {"__op":"AddUnique", objects: ['1', '2']}, - f: {"__op":"Remove", objects: ['2']}, - selfThing: {"__type":"Pointer","className":"GameScore","objectId":obj.id}, - }) - }, (error, response, body) => { - try { - body = JSON.parse(body); - expect(body.a).toBeUndefined(); - expect(body.c).toEqual(3); // 2+1 - expect(body.d.length).toBe(2); - expect(body.d.indexOf('1') > -1).toBe(true); - expect(body.d.indexOf('2') > -1).toBe(true); - expect(body.e.length).toBe(2); - expect(body.e.indexOf('1') > -1).toBe(true); - expect(body.e.indexOf('2') > -1).toBe(true); - expect(body.f.length).toBe(1); - expect(body.f.indexOf('1') > -1).toBe(true); - // return nothing on other self - expect(body.selfThing).toBeUndefined(); - // updatedAt is always set - expect(body.updatedAt).not.toBeUndefined(); - }catch(e) { - jfail(e); - } + obj + .save({ a: 'hello', c: 1, d: ['1'], e: ['1'], f: ['1', '2'] }) + .then(() => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Installation-Id': 'yolo', + }; + request.put( + { + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore/' + obj.id, + body: JSON.stringify({ + a: 'b', + c: { __op: 'Increment', amount: 2 }, + d: { __op: 'Add', objects: ['2'] }, + e: { __op: 'AddUnique', objects: ['1', '2'] }, + f: { __op: 'Remove', objects: ['2'] }, + selfThing: { + __type: 'Pointer', + className: 'GameScore', + objectId: obj.id, + }, + }), + }, + (error, response, body) => { + try { + body = JSON.parse(body); + expect(body.a).toBeUndefined(); + expect(body.c).toEqual(3); // 2+1 + expect(body.d.length).toBe(2); + expect(body.d.indexOf('1') > -1).toBe(true); + expect(body.d.indexOf('2') > -1).toBe(true); + expect(body.e.length).toBe(2); + expect(body.e.indexOf('1') > -1).toBe(true); + expect(body.e.indexOf('2') > -1).toBe(true); + expect(body.f.length).toBe(1); + expect(body.f.indexOf('1') > -1).toBe(true); + // return nothing on other self + expect(body.selfThing).toBeUndefined(); + // updatedAt is always set + expect(body.updatedAt).not.toBeUndefined(); + } catch (e) { + jfail(e); + } + done(); + } + ); + }) + .catch(() => { + fail('Should not fail'); done(); }); - }).catch(() => { - fail('Should not fail'); - done(); - }) - }) + }); - it('test cloud function error handling', (done) => { + it('test cloud function error handling', done => { // Register a function which will fail Parse.Cloud.define('willFail', () => { throw new Error('noway'); }); - Parse.Cloud.run('willFail').then(() => { - fail('Should not have succeeded.'); - done(); - }, (e) => { - expect(e.code).toEqual(141); - expect(e.message).toEqual('noway'); - done(); - }); + Parse.Cloud.run('willFail').then( + () => { + fail('Should not have succeeded.'); + done(); + }, + e => { + expect(e.code).toEqual(141); + expect(e.message).toEqual('noway'); + done(); + } + ); }); - it('test cloud function error handling with custom error code', (done) => { + it('test cloud function error handling with custom error code', done => { // Register a function which will fail Parse.Cloud.define('willFail', () => { throw new Parse.Error(999, 'noway'); }); - Parse.Cloud.run('willFail').then(() => { - fail('Should not have succeeded.'); - done(); - }, (e) => { - expect(e.code).toEqual(999); - expect(e.message).toEqual('noway'); - done(); - }); + Parse.Cloud.run('willFail').then( + () => { + fail('Should not have succeeded.'); + done(); + }, + e => { + expect(e.code).toEqual(999); + expect(e.message).toEqual('noway'); + done(); + } + ); }); - it('test cloud function error handling with standard error code', (done) => { + it('test cloud function error handling with standard error code', done => { // Register a function which will fail Parse.Cloud.define('willFail', () => { throw new Error('noway'); }); - Parse.Cloud.run('willFail').then(() => { - fail('Should not have succeeded.'); - done(); - }, (e) => { - expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(e.message).toEqual('noway'); - done(); - }); + Parse.Cloud.run('willFail').then( + () => { + fail('Should not have succeeded.'); + done(); + }, + e => { + expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED); + expect(e.message).toEqual('noway'); + done(); + } + ); }); it('test beforeSave/afterSave get installationId', function(done) { @@ -908,17 +1080,20 @@ describe('miscellaneous', function() { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Installation-Id': 'yolo' + 'X-Parse-Installation-Id': 'yolo', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/classes/GameScore', - body: JSON.stringify({ a: 'b' }) - }, (error) => { - expect(error).toBe(null); - expect(triggerTime).toEqual(2); - done(); - }); + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/classes/GameScore', + body: JSON.stringify({ a: 'b' }), + }, + error => { + expect(error).toBe(null); + expect(triggerTime).toEqual(2); + done(); + } + ); }); it('test beforeDelete/afterDelete get installationId', function(done) { @@ -938,23 +1113,31 @@ describe('miscellaneous', function() { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Installation-Id': 'yolo' + 'X-Parse-Installation-Id': 'yolo', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/classes/GameScore', - body: JSON.stringify({ a: 'b' }) - }, (error, response, body) => { - expect(error).toBe(null); - request.del({ + request.post( + { headers: headers, - url: 'http://localhost:8378/1/classes/GameScore/' + JSON.parse(body).objectId - }, (error) => { + url: 'http://localhost:8378/1/classes/GameScore', + body: JSON.stringify({ a: 'b' }), + }, + (error, response, body) => { expect(error).toBe(null); - expect(triggerTime).toEqual(2); - done(); - }); - }); + request.del( + { + headers: headers, + url: + 'http://localhost:8378/1/classes/GameScore/' + + JSON.parse(body).objectId, + }, + error => { + expect(error).toBe(null); + expect(triggerTime).toEqual(2); + done(); + } + ); + } + ); }); it('test beforeDelete with locked down ACL', async () => { @@ -969,183 +1152,216 @@ describe('miscellaneous', function() { expect(objects.length).toBe(0); try { await object.destroy(); - } catch(e) { + } catch (e) { expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); } expect(called).toBe(false); }); - it('test cloud function query parameters', (done) => { - Parse.Cloud.define('echoParams', (req) => { + it('test cloud function query parameters', done => { + Parse.Cloud.define('echoParams', req => { return req.params; }); const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test' + 'X-Parse-Javascript-Key': 'test', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/functions/echoParams', //?option=1&other=2 - qs: { - option: 1, - other: 2 + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/functions/echoParams', //?option=1&other=2 + qs: { + option: 1, + other: 2, + }, + body: '{"foo":"bar", "other": 1}', }, - body: '{"foo":"bar", "other": 1}' - }, (error, response, body) => { - expect(error).toBe(null); - const res = JSON.parse(body).result; - expect(res.option).toEqual('1'); - // Make sure query string params override body params - expect(res.other).toEqual('2'); - expect(res.foo).toEqual("bar"); - done(); - }); + (error, response, body) => { + expect(error).toBe(null); + const res = JSON.parse(body).result; + expect(res.option).toEqual('1'); + // Make sure query string params override body params + expect(res.other).toEqual('2'); + expect(res.foo).toEqual('bar'); + done(); + } + ); }); - it('test cloud function parameter validation', (done) => { + it('test cloud function parameter validation', done => { // Register a function with validation - Parse.Cloud.define('functionWithParameterValidationFailure', () => { - return 'noway'; - }, (request) => { - return request.params.success === 100; - }); + Parse.Cloud.define( + 'functionWithParameterValidationFailure', + () => { + return 'noway'; + }, + request => { + return request.params.success === 100; + } + ); - Parse.Cloud.run('functionWithParameterValidationFailure', {"success":500}).then(() => { - fail('Validation should not have succeeded'); - done(); - }, (e) => { - expect(e.code).toEqual(142); - expect(e.message).toEqual('Validation failed.'); - done(); - }); + Parse.Cloud.run('functionWithParameterValidationFailure', { + success: 500, + }).then( + () => { + fail('Validation should not have succeeded'); + done(); + }, + e => { + expect(e.code).toEqual(142); + expect(e.message).toEqual('Validation failed.'); + done(); + } + ); }); it('can handle null params in cloud functions (regression test for #1742)', done => { - Parse.Cloud.define('func', (request) => { + Parse.Cloud.define('func', request => { expect(request.params.nullParam).toEqual(null); return 'yay'; }); - Parse.Cloud.run('func', {nullParam: null}) - .then(() => { - done() - }, () => { + Parse.Cloud.run('func', { nullParam: null }).then( + () => { + done(); + }, + () => { fail('cloud code call failed'); done(); - }); + } + ); }); it('can handle date params in cloud functions (#2214)', done => { const date = new Date(); - Parse.Cloud.define('dateFunc', (request) => { + Parse.Cloud.define('dateFunc', request => { expect(request.params.date.__type).toEqual('Date'); expect(request.params.date.iso).toEqual(date.toISOString()); return 'yay'; }); - Parse.Cloud.run('dateFunc', {date: date}) - .then(() => { - done() - }, () => { + Parse.Cloud.run('dateFunc', { date: date }).then( + () => { + done(); + }, + () => { fail('cloud code call failed'); done(); - }); + } + ); }); it('fails on invalid client key', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-Client-Key': 'notclient' + 'X-Parse-Client-Key': 'notclient', }; - request.get({ - headers: headers, - url: 'http://localhost:8378/1/classes/TestObject' - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.error).toEqual('unauthorized'); - done(); - }); + request.get( + { + headers: headers, + url: 'http://localhost:8378/1/classes/TestObject', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(b.error).toEqual('unauthorized'); + done(); + } + ); }); it('fails on invalid windows key', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-Windows-Key': 'notwindows' + 'X-Parse-Windows-Key': 'notwindows', }; - request.get({ - headers: headers, - url: 'http://localhost:8378/1/classes/TestObject' - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.error).toEqual('unauthorized'); - done(); - }); + request.get( + { + headers: headers, + url: 'http://localhost:8378/1/classes/TestObject', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(b.error).toEqual('unauthorized'); + done(); + } + ); }); it('fails on invalid javascript key', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'notjavascript' + 'X-Parse-Javascript-Key': 'notjavascript', }; - request.get({ - headers: headers, - url: 'http://localhost:8378/1/classes/TestObject' - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.error).toEqual('unauthorized'); - done(); - }); + request.get( + { + headers: headers, + url: 'http://localhost:8378/1/classes/TestObject', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(b.error).toEqual('unauthorized'); + done(); + } + ); }); it('fails on invalid rest api key', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'notrest' + 'X-Parse-REST-API-Key': 'notrest', }; - request.get({ - headers: headers, - url: 'http://localhost:8378/1/classes/TestObject' - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.error).toEqual('unauthorized'); - done(); - }); + request.get( + { + headers: headers, + url: 'http://localhost:8378/1/classes/TestObject', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(b.error).toEqual('unauthorized'); + done(); + } + ); }); it('fails on invalid function', done => { - Parse.Cloud.run('somethingThatDoesDefinitelyNotExist').then(() => { - fail('This should have never suceeded'); - done(); - }, (e) => { - expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED); - expect(e.message).toEqual('Invalid function: "somethingThatDoesDefinitelyNotExist"'); - done(); - }); + Parse.Cloud.run('somethingThatDoesDefinitelyNotExist').then( + () => { + fail('This should have never suceeded'); + done(); + }, + e => { + expect(e.code).toEqual(Parse.Error.SCRIPT_FAILED); + expect(e.message).toEqual( + 'Invalid function: "somethingThatDoesDefinitelyNotExist"' + ); + done(); + } + ); }); - it('dedupes an installation properly and returns updatedAt', (done) => { + it('dedupes an installation properly and returns updatedAt', done => { const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; const data = { - 'installationId': 'lkjsahdfkjhsdfkjhsdfkjhsdf', - 'deviceType': 'embedded' + installationId: 'lkjsahdfkjhsdfkjhsdfkjhsdf', + deviceType: 'embedded', }; const requestOptions = { headers: headers, url: 'http://localhost:8378/1/installations', - body: JSON.stringify(data) + body: JSON.stringify(data), }; request.post(requestOptions, (error, response, body) => { expect(error).toBe(null); @@ -1160,23 +1376,23 @@ describe('miscellaneous', function() { }); }); - it('android login providing empty authData block works', (done) => { + it('android login providing empty authData block works', done => { const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; const data = { username: 'pulse1989', password: 'password1234', - authData: {} + authData: {}, }; const requestOptions = { headers: headers, url: 'http://localhost:8378/1/users', - body: JSON.stringify(data) + body: JSON.stringify(data), }; - request.post(requestOptions, (error) => { + request.post(requestOptions, error => { expect(error).toBe(null); requestOptions.url = 'http://localhost:8378/1/login'; request.get(requestOptions, (error, response, body) => { @@ -1188,114 +1404,125 @@ describe('miscellaneous', function() { }); }); - it('gets relation fields', (done) => { + it('gets relation fields', done => { const object = new Parse.Object('AnObject'); const relatedObject = new Parse.Object('RelatedObject'); - Parse.Object.saveAll([object, relatedObject]).then(() => { - object.relation('related').add(relatedObject); - return object.save(); - }).then(() => { - const headers = { - 'Content-Type': 'application/json', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' - }; - const requestOptions = { - headers: headers, - url: 'http://localhost:8378/1/classes/AnObject', - json: true - }; - request.get(requestOptions, (err, res, body) => { - expect(body.results.length).toBe(1); - const result = body.results[0]; - expect(result.related).toEqual({ - __type: "Relation", - className: 'RelatedObject' - }) + Parse.Object.saveAll([object, relatedObject]) + .then(() => { + object.relation('related').add(relatedObject); + return object.save(); + }) + .then(() => { + const headers = { + 'Content-Type': 'application/json', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + const requestOptions = { + headers: headers, + url: 'http://localhost:8378/1/classes/AnObject', + json: true, + }; + request.get(requestOptions, (err, res, body) => { + expect(body.results.length).toBe(1); + const result = body.results[0]; + expect(result.related).toEqual({ + __type: 'Relation', + className: 'RelatedObject', + }); + done(); + }); + }) + .catch(err => { + jfail(err); done(); }); - }).catch((err) => { - jfail(err); - done(); - }) }); - it('properly returns incremented values (#1554)', (done) => { + it('properly returns incremented values (#1554)', done => { const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; const requestOptions = { headers: headers, url: 'http://localhost:8378/1/classes/AnObject', - json: true + json: true, }; const object = new Parse.Object('AnObject'); function runIncrement(amount) { const options = Object.assign({}, requestOptions, { body: { - "key": { + key: { __op: 'Increment', - amount: amount - } + amount: amount, + }, }, - url: 'http://localhost:8378/1/classes/AnObject/' + object.id - }) + url: 'http://localhost:8378/1/classes/AnObject/' + object.id, + }); return new Promise((resolve, reject) => { - request.put(options, (err, res, body) => { + request.put(options, (err, res, body) => { if (err) { reject(err); } else { resolve(body); } }); - }) + }); } - object.save().then(() => { - return runIncrement(1); - }).then((res) => { - expect(res.key).toBe(1); - return runIncrement(-1); - }).then((res) => { - expect(res.key).toBe(0); - done(); - }) - }) + object + .save() + .then(() => { + return runIncrement(1); + }) + .then(res => { + expect(res.key).toBe(1); + return runIncrement(-1); + }) + .then(res => { + expect(res.key).toBe(0); + done(); + }); + }); - it('ignores _RevocableSession "header" send by JS SDK', (done) => { + it('ignores _RevocableSession "header" send by JS SDK', done => { const object = new Parse.Object('AnObject'); object.set('a', 'b'); object.save().then(() => { - request.post({ - headers: {'Content-Type': 'application/json'}, - url: 'http://localhost:8378/1/classes/AnObject', - body: { - _method: 'GET', - _ApplicationId: 'test', - _JavaScriptKey: 'test', - _ClientVersion: 'js1.8.3', - _InstallationId: 'iid', - _RevocableSession: "1", + request.post( + { + headers: { 'Content-Type': 'application/json' }, + url: 'http://localhost:8378/1/classes/AnObject', + body: { + _method: 'GET', + _ApplicationId: 'test', + _JavaScriptKey: 'test', + _ClientVersion: 'js1.8.3', + _InstallationId: 'iid', + _RevocableSession: '1', + }, + json: true, }, - json: true - }, (err, res, body) => { - expect(body.error).toBeUndefined(); - expect(body.results).not.toBeUndefined(); - expect(body.results.length).toBe(1); - const result = body.results[0]; - expect(result.a).toBe('b'); - done(); - }) + (err, res, body) => { + expect(body.error).toBeUndefined(); + expect(body.results).not.toBeUndefined(); + expect(body.results.length).toBe(1); + const result = body.results[0]; + expect(result.a).toBe('b'); + done(); + } + ); }); }); it('doesnt convert interior keys of objects that use special names', done => { const obj = new Parse.Object('Obj'); obj.set('val', { createdAt: 'a', updatedAt: 1 }); - obj.save() + obj + .save() .then(obj => new Parse.Query('Obj').get(obj.id)) .then(obj => { expect(obj.get('val').createdAt).toEqual('a'); @@ -1305,88 +1532,112 @@ describe('miscellaneous', function() { }); it('bans interior keys containing . or $', done => { - new Parse.Object('Obj').save({innerObj: {'key with a $': 'fails'}}) - .then(() => { - fail('should not succeed') - }, error => { - expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); - return new Parse.Object('Obj').save({innerObj: {'key with a .': 'fails'}}); - }) - .then(() => { - fail('should not succeed') - }, error => { - expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); - return new Parse.Object('Obj').save({innerObj: {innerInnerObj: {'key with $': 'fails'}}}); - }) - .then(() => { - fail('should not succeed') - }, error => { - expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); - return new Parse.Object('Obj').save({innerObj: {innerInnerObj: {'key with .': 'fails'}}}); - }) - .then(() => { - fail('should not succeed') - done(); - }, error => { - expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); - done(); - }); + new Parse.Object('Obj') + .save({ innerObj: { 'key with a $': 'fails' } }) + .then( + () => { + fail('should not succeed'); + }, + error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + return new Parse.Object('Obj').save({ + innerObj: { 'key with a .': 'fails' }, + }); + } + ) + .then( + () => { + fail('should not succeed'); + }, + error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + return new Parse.Object('Obj').save({ + innerObj: { innerInnerObj: { 'key with $': 'fails' } }, + }); + } + ) + .then( + () => { + fail('should not succeed'); + }, + error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + return new Parse.Object('Obj').save({ + innerObj: { innerInnerObj: { 'key with .': 'fails' } }, + }); + } + ) + .then( + () => { + fail('should not succeed'); + done(); + }, + error => { + expect(error.code).toEqual(Parse.Error.INVALID_NESTED_KEY); + done(); + } + ); }); it('does not change inner object keys named _auth_data_something', done => { - new Parse.Object('O').save({ innerObj: {_auth_data_facebook: 7}}) + new Parse.Object('O') + .save({ innerObj: { _auth_data_facebook: 7 } }) .then(object => new Parse.Query('O').get(object.id)) .then(object => { - expect(object.get('innerObj')).toEqual({_auth_data_facebook: 7}); + expect(object.get('innerObj')).toEqual({ _auth_data_facebook: 7 }); done(); }); }); it('does not change inner object key names _p_somethign', done => { - new Parse.Object('O').save({ innerObj: {_p_data: 7}}) + new Parse.Object('O') + .save({ innerObj: { _p_data: 7 } }) .then(object => new Parse.Query('O').get(object.id)) .then(object => { - expect(object.get('innerObj')).toEqual({_p_data: 7}); + expect(object.get('innerObj')).toEqual({ _p_data: 7 }); done(); }); }); it('does not change inner object key names _rperm, _wperm', done => { - new Parse.Object('O').save({ innerObj: {_rperm: 7, _wperm: 8}}) + new Parse.Object('O') + .save({ innerObj: { _rperm: 7, _wperm: 8 } }) .then(object => new Parse.Query('O').get(object.id)) .then(object => { - expect(object.get('innerObj')).toEqual({_rperm: 7, _wperm: 8}); + expect(object.get('innerObj')).toEqual({ _rperm: 7, _wperm: 8 }); done(); }); }); it('does not change inner objects if the key has the same name as a geopoint field on the class, and the value is an array of length 2, or if the key has the same name as a file field on the class, and the value is a string', done => { const file = new Parse.File('myfile.txt', { base64: 'eAo=' }); - file.save() + file + .save() .then(f => { const obj = new Parse.Object('O'); obj.set('fileField', f); obj.set('geoField', new Parse.GeoPoint(0, 0)); obj.set('innerObj', { - fileField: "data", - geoField: [1,2], + fileField: 'data', + geoField: [1, 2], }); return obj.save(); }) .then(object => object.fetch()) .then(object => { expect(object.get('innerObj')).toEqual({ - fileField: "data", - geoField: [1,2], + fileField: 'data', + geoField: [1, 2], }); done(); - }).catch((e) => { + }) + .catch(e => { jfail(e); done(); }); }); - it('purge all objects in class', (done) => { + it('purge all objects in class', done => { const object = new Parse.Object('TestObject'); object.set('foo', 'bar'); const object2 = new Parse.Object('TestObject'); @@ -1394,112 +1645,132 @@ describe('miscellaneous', function() { Parse.Object.saveAll([object, object2]) .then(() => { const query = new Parse.Query(TestObject); - return query.count() - }).then((count) => { + return query.count(); + }) + .then(count => { expect(count).toBe(2); const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test' + 'X-Parse-Master-Key': 'test', }; - request.del({ - headers: headers, - url: 'http://localhost:8378/1/purge/TestObject', - json: true - }, (err) => { - expect(err).toBe(null); - const query = new Parse.Query(TestObject); - return query.count().then((count) => { - expect(count).toBe(0); - done(); - }); - }); + request.del( + { + headers: headers, + url: 'http://localhost:8378/1/purge/TestObject', + json: true, + }, + err => { + expect(err).toBe(null); + const query = new Parse.Query(TestObject); + return query.count().then(count => { + expect(count).toBe(0); + done(); + }); + } + ); }); }); - it('fail on purge all objects in class without master key', (done) => { + it('fail on purge all objects in class without master key', done => { const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; rp({ method: 'DELETE', headers: headers, uri: 'http://localhost:8378/1/purge/TestObject', - json: true - }).then(() => { - fail('Should not succeed'); - }).catch(err => { - expect(err.error.error).toEqual('unauthorized: master key is required'); - done(); - }); + json: true, + }) + .then(() => { + fail('Should not succeed'); + }) + .catch(err => { + expect(err.error.error).toEqual('unauthorized: master key is required'); + done(); + }); }); - it('purge all objects in _Role also purge cache', (done) => { + it('purge all objects in _Role also purge cache', done => { const headers = { 'Content-Type': 'application/json', 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test' + 'X-Parse-Master-Key': 'test', }; let user, object; - createTestUser().then((x) => { - user = x; - const acl = new Parse.ACL(); - acl.setPublicReadAccess(true); - acl.setPublicWriteAccess(false); - const role = new Parse.Object('_Role'); - role.set('name', 'TestRole'); - role.setACL(acl); - const users = role.relation('users'); - users.add(user); - return role.save({}, { useMasterKey: true }); - }).then(() => { - const query = new Parse.Query('_Role'); - return query.find({ useMasterKey: true }); - }).then((x) => { - expect(x.length).toEqual(1); - const relation = x[0].relation('users').query(); - return relation.first({ useMasterKey: true }); - }).then((x) => { - expect(x.id).toEqual(user.id); - object = new Parse.Object('TestObject'); - const acl = new Parse.ACL(); - acl.setPublicReadAccess(false); - acl.setPublicWriteAccess(false); - acl.setRoleReadAccess('TestRole', true); - acl.setRoleWriteAccess('TestRole', true); - object.setACL(acl); - return object.save(); - }).then(() => { - const query = new Parse.Query('TestObject'); - return query.find({ sessionToken: user.getSessionToken() }); - }).then((x) => { - expect(x.length).toEqual(1); - return rp({ - method: 'DELETE', - headers: headers, - uri: 'http://localhost:8378/1/purge/_Role', - json: true - }); - }).then(() => { - const query = new Parse.Query('TestObject'); - return query.get(object.id, { sessionToken: user.getSessionToken() }); - }).then(() => { - fail('Should not succeed'); - }, (e) => { - expect(e.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); + createTestUser() + .then(x => { + user = x; + const acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + acl.setPublicWriteAccess(false); + const role = new Parse.Object('_Role'); + role.set('name', 'TestRole'); + role.setACL(acl); + const users = role.relation('users'); + users.add(user); + return role.save({}, { useMasterKey: true }); + }) + .then(() => { + const query = new Parse.Query('_Role'); + return query.find({ useMasterKey: true }); + }) + .then(x => { + expect(x.length).toEqual(1); + const relation = x[0].relation('users').query(); + return relation.first({ useMasterKey: true }); + }) + .then(x => { + expect(x.id).toEqual(user.id); + object = new Parse.Object('TestObject'); + const acl = new Parse.ACL(); + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(false); + acl.setRoleReadAccess('TestRole', true); + acl.setRoleWriteAccess('TestRole', true); + object.setACL(acl); + return object.save(); + }) + .then(() => { + const query = new Parse.Query('TestObject'); + return query.find({ sessionToken: user.getSessionToken() }); + }) + .then(x => { + expect(x.length).toEqual(1); + return rp({ + method: 'DELETE', + headers: headers, + uri: 'http://localhost:8378/1/purge/_Role', + json: true, + }); + }) + .then(() => { + const query = new Parse.Query('TestObject'); + return query.get(object.id, { sessionToken: user.getSessionToken() }); + }) + .then( + () => { + fail('Should not succeed'); + }, + e => { + expect(e.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + } + ); }); - it('purge empty class', (done) => { + it('purge empty class', done => { const testSchema = new Parse.Schema('UnknownClass'); - testSchema.purge().then(done).catch(done.fail); + testSchema + .purge() + .then(done) + .catch(done.fail); }); - it('should not update schema beforeSave #2672', (done) => { - Parse.Cloud.beforeSave('MyObject', (request) => { + it('should not update schema beforeSave #2672', done => { + Parse.Cloud.beforeSave('MyObject', request => { if (request.object.get('secret')) { throw 'cannot set secret here'; } @@ -1507,63 +1778,77 @@ describe('miscellaneous', function() { const object = new Parse.Object('MyObject'); object.set('key', 'value'); - object.save().then(() => { - return object.save({'secret': 'should not update schema'}); - }).then(() => { - fail(); - done(); - }, () => { - return rp({ - method: 'GET', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test' + object + .save() + .then(() => { + return object.save({ secret: 'should not update schema' }); + }) + .then( + () => { + fail(); + done(); }, - uri: 'http://localhost:8378/1/schemas/MyObject', - json: true - }); - }).then((res) => { - const fields = res.fields; - expect(fields.secret).toBeUndefined(); - done(); - }, (err) => { - jfail(err); - done(); - }); + () => { + return rp({ + method: 'GET', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + uri: 'http://localhost:8378/1/schemas/MyObject', + json: true, + }); + } + ) + .then( + res => { + const fields = res.fields; + expect(fields.secret).toBeUndefined(); + done(); + }, + err => { + jfail(err); + done(); + } + ); }); }); describe_only_db('mongo')('legacy _acl', () => { - it('should have _acl when locking down (regression for #2465)', (done) => { + it('should have _acl when locking down (regression for #2465)', done => { const headers = { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' - } + 'X-Parse-REST-API-Key': 'rest', + }; rp({ method: 'POST', headers: headers, uri: 'http://localhost:8378/1/classes/Report', body: { ACL: {}, - name: 'My Report' + name: 'My Report', }, - json: true - }).then(() => { - const config = Config.get('test'); - const adapter = config.database.adapter; - return adapter._adaptiveCollection("Report") - .then(collection => collection.find({})) - }).then((results) => { - expect(results.length).toBe(1); - const result = results[0]; - expect(result.name).toEqual('My Report'); - expect(result._wperm).toEqual([]); - expect(result._rperm).toEqual([]); - expect(result._acl).toEqual({}); - done(); - }).catch((err) => { - fail(JSON.stringify(err)); - done(); - }); + json: true, + }) + .then(() => { + const config = Config.get('test'); + const adapter = config.database.adapter; + return adapter + ._adaptiveCollection('Report') + .then(collection => collection.find({})); + }) + .then(results => { + expect(results.length).toBe(1); + const result = results[0]; + expect(result.name).toEqual('My Report'); + expect(result._wperm).toEqual([]); + expect(result._rperm).toEqual([]); + expect(result._acl).toEqual({}); + done(); + }) + .catch(err => { + fail(JSON.stringify(err)); + done(); + }); }); }); diff --git a/spec/ParseCloudCodePublisher.spec.js b/spec/ParseCloudCodePublisher.spec.js index 1c242d9d7d..905b0cbbeb 100644 --- a/spec/ParseCloudCodePublisher.spec.js +++ b/spec/ParseCloudCodePublisher.spec.js @@ -1,4 +1,5 @@ -const ParseCloudCodePublisher = require('../lib/LiveQuery/ParseCloudCodePublisher').ParseCloudCodePublisher; +const ParseCloudCodePublisher = require('../lib/LiveQuery/ParseCloudCodePublisher') + .ParseCloudCodePublisher; const Parse = require('parse/node'); describe('ParseCloudCodePublisher', function() { @@ -7,19 +8,23 @@ describe('ParseCloudCodePublisher', function() { const mockParsePubSub = { createPublisher: jasmine.createSpy('publish').and.returnValue({ publish: jasmine.createSpy('publish'), - on: jasmine.createSpy('on') + on: jasmine.createSpy('on'), }), createSubscriber: jasmine.createSpy('publish').and.returnValue({ subscribe: jasmine.createSpy('subscribe'), - on: jasmine.createSpy('on') - }) + on: jasmine.createSpy('on'), + }), }; - jasmine.mockLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub); + jasmine.mockLibrary( + '../lib/LiveQuery/ParsePubSub', + 'ParsePubSub', + mockParsePubSub + ); done(); }); it('can initialize', function() { - const config = {} + const config = {}; new ParseCloudCodePublisher(config); const ParsePubSub = require('../lib/LiveQuery/ParsePubSub').ParsePubSub; @@ -32,7 +37,10 @@ describe('ParseCloudCodePublisher', function() { const request = {}; publisher.onCloudCodeAfterSave(request); - expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith(Parse.applicationId + 'afterSave', request); + expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith( + Parse.applicationId + 'afterSave', + request + ); }); it('can handle cloud code afterDelete request', function() { @@ -41,7 +49,10 @@ describe('ParseCloudCodePublisher', function() { const request = {}; publisher.onCloudCodeAfterDelete(request); - expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith(Parse.applicationId + 'afterDelete', request); + expect(publisher._onCloudCodeMessage).toHaveBeenCalledWith( + Parse.applicationId + 'afterDelete', + request + ); }); it('can handle cloud code request', function() { @@ -52,7 +63,7 @@ describe('ParseCloudCodePublisher', function() { originalParseObject.set('key', 'originalValue'); const request = { object: currentParseObject, - original: originalParseObject + original: originalParseObject, }; publisher._onCloudCodeMessage('afterSave', request); @@ -63,7 +74,7 @@ describe('ParseCloudCodePublisher', function() { expect(message.originalParseObject).toEqual(request.original._toFullJSON()); }); - afterEach(function(){ + afterEach(function() { jasmine.restoreLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub'); }); }); diff --git a/spec/ParseFile.spec.js b/spec/ParseFile.spec.js index 4c03417bcc..a5ebd62d84 100644 --- a/spec/ParseFile.spec.js +++ b/spec/ParseFile.spec.js @@ -1,13 +1,13 @@ // This is a port of the test suite: // hungry/js/test/parse_file_test.js -"use strict"; +'use strict'; const request = require('request'); -const str = "Hello World!"; +const str = 'Hello World!'; const data = []; -for (let i = 0; i < str.length; i++) { +for (let i = 0; i < str.length; i++) { data.push(str.charCodeAt(i)); } @@ -17,74 +17,87 @@ describe('Parse.File testing', () => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: 'argle bargle', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.name).toMatch(/_file.txt$/); - expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/); - request.get(b.url, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body).toEqual('argle bargle'); - done(); - }); - }); + const b = JSON.parse(body); + expect(b.name).toMatch(/_file.txt$/); + expect(b.url).toMatch( + /^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/ + ); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + }); + } + ); }); - 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); - const 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) => { - try { - expect(response.headers['content-type']).toMatch('^text/html'); - expect(error).toBe(null); - expect(body).toEqual('\n'); - } catch(e) { - jfail(e); - } - 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); + const 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) => { + try { + expect(response.headers['content-type']).toMatch('^text/html'); + expect(error).toBe(null); + expect(body).toEqual('\n'); + } catch (e) { + jfail(e); + } + done(); + }); + } + ); }); it('works without Content-Type', done => { const headers = { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: 'argle bargle', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.name).toMatch(/_file.txt$/); - expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/); - request.get(b.url, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body).toEqual('argle bargle'); - done(); - }); - }); + const b = JSON.parse(body); + expect(b.name).toMatch(/_file.txt$/); + expect(b.url).toMatch( + /^http:\/\/localhost:8378\/1\/files\/test\/.*file.txt$/ + ); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + }); + } + ); }); }); @@ -92,491 +105,567 @@ describe('Parse.File testing', () => { const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/testfile.txt', - body: 'check one two', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.name).toMatch(/_testfile.txt$/); - expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*testfile.txt$/); - request.get(b.url, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/testfile.txt', + body: 'check one two', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body).toEqual('check one two'); - request.del({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Master-Key': 'test' - }, - url: 'http://localhost:8378/1/files/' + b.name - }, (error, response) => { + const b = JSON.parse(body); + expect(b.name).toMatch(/_testfile.txt$/); + expect(b.url).toMatch( + /^http:\/\/localhost:8378\/1\/files\/test\/.*testfile.txt$/ + ); + request.get(b.url, (error, response, body) => { expect(error).toBe(null); - expect(response.statusCode).toEqual(200); - request.get({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + expect(body).toEqual('check one two'); + request.del( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'test', + }, + url: 'http://localhost:8378/1/files/' + b.name, }, - url: b.url - }, (error, response) => { - expect(error).toBe(null); - try { - expect(response.statusCode).toEqual(404); - } catch(e) { - jfail(e); + (error, response) => { + expect(error).toBe(null); + expect(response.statusCode).toEqual(200); + request.get( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + url: b.url, + }, + (error, response) => { + expect(error).toBe(null); + try { + expect(response.statusCode).toEqual(404); + } catch (e) { + jfail(e); + } + done(); + } + ); } - done(); - }); + ); }); - }); - }); + } + ); }); it('blocks file deletions with missing or incorrect master-key header', done => { const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/thefile.jpg', - body: 'the file body' - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/); - // missing X-Parse-Master-Key header - request.del({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/files/' + b.name - }, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/thefile.jpg', + body: 'the file body', + }, + (error, response, body) => { expect(error).toBe(null); - const del_b = JSON.parse(body); - expect(response.statusCode).toEqual(403); - expect(del_b.error).toMatch(/unauthorized/); - // incorrect X-Parse-Master-Key header - request.del({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Master-Key': 'tryagain' + const b = JSON.parse(body); + expect(b.url).toMatch( + /^http:\/\/localhost:8378\/1\/files\/test\/.*thefile.jpg$/ + ); + // missing X-Parse-Master-Key header + request.del( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/files/' + b.name, }, - url: 'http://localhost:8378/1/files/' + b.name - }, (error, response, body) => { - expect(error).toBe(null); - const del_b2 = JSON.parse(body); - expect(response.statusCode).toEqual(403); - expect(del_b2.error).toMatch(/unauthorized/); - done(); - }); - }); - }); + (error, response, body) => { + expect(error).toBe(null); + const del_b = JSON.parse(body); + expect(response.statusCode).toEqual(403); + expect(del_b.error).toMatch(/unauthorized/); + // incorrect X-Parse-Master-Key header + request.del( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Master-Key': 'tryagain', + }, + url: 'http://localhost:8378/1/files/' + b.name, + }, + (error, response, body) => { + expect(error).toBe(null); + const del_b2 = JSON.parse(body); + expect(response.statusCode).toEqual(403); + expect(del_b2.error).toMatch(/unauthorized/); + done(); + } + ); + } + ); + } + ); }); it('handles other filetypes', done => { const headers = { 'Content-Type': 'image/jpeg', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.jpg', - body: 'argle bargle', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.name).toMatch(/_file.jpg$/); - expect(b.url).toMatch(/^http:\/\/localhost:8378\/1\/files\/.*file.jpg$/); - request.get(b.url, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.jpg', + body: 'argle bargle', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body).toEqual('argle bargle'); - done(); - }); - }); + const b = JSON.parse(body); + expect(b.name).toMatch(/_file.jpg$/); + expect(b.url).toMatch( + /^http:\/\/localhost:8378\/1\/files\/.*file.jpg$/ + ); + request.get(b.url, (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + }); + } + ); }); - it("save file", async () => { - const file = new Parse.File("hello.txt", data, "text/plain"); + it('save file', async () => { + const file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const result = await file.save(); strictEqual(result, file); ok(file.name()); ok(file.url()); - notEqual(file.name(), "hello.txt"); + notEqual(file.name(), 'hello.txt'); }); - it("save file in object", async done => { - const file = new Parse.File("hello.txt", data, "text/plain"); + it('save file in object', async done => { + const file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const result = await file.save(); strictEqual(result, file); ok(file.name()); ok(file.url()); - notEqual(file.name(), "hello.txt"); + notEqual(file.name(), 'hello.txt'); - const object = new Parse.Object("TestObject"); + const object = new Parse.Object('TestObject'); await object.save({ file: file }); - const objectAgain = await (new Parse.Query("TestObject")).get(object.id); - ok(objectAgain.get("file") instanceof Parse.File); + const objectAgain = await new Parse.Query('TestObject').get(object.id); + ok(objectAgain.get('file') instanceof Parse.File); done(); }); - it("save file in object with escaped characters in filename", async () => { - const file = new Parse.File("hello . txt", data, "text/plain"); + it('save file in object with escaped characters in filename', async () => { + const file = new Parse.File('hello . txt', data, 'text/plain'); ok(!file.url()); const result = await file.save(); strictEqual(result, file); ok(file.name()); ok(file.url()); - notEqual(file.name(), "hello . txt"); + notEqual(file.name(), 'hello . txt'); - const object = new Parse.Object("TestObject"); + const object = new Parse.Object('TestObject'); await object.save({ file }); - const objectAgain = await (new Parse.Query("TestObject")).get(object.id); - ok(objectAgain.get("file") instanceof Parse.File); + const objectAgain = await new Parse.Query('TestObject').get(object.id); + ok(objectAgain.get('file') instanceof Parse.File); }); - it("autosave file in object", async done => { - let file = new Parse.File("hello.txt", data, "text/plain"); + it('autosave file in object', async done => { + let file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); - const object = new Parse.Object("TestObject"); + const object = new Parse.Object('TestObject'); await object.save({ file }); - const objectAgain = await (new Parse.Query("TestObject")).get(object.id); - file = objectAgain.get("file"); + const objectAgain = await new Parse.Query('TestObject').get(object.id); + file = objectAgain.get('file'); ok(file instanceof Parse.File); ok(file.name()); ok(file.url()); - notEqual(file.name(), "hello.txt"); + notEqual(file.name(), 'hello.txt'); done(); }); - it("autosave file in object in object", async done => { - let file = new Parse.File("hello.txt", data, "text/plain"); + it('autosave file in object in object', async done => { + let file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); - const child = new Parse.Object("Child"); - child.set("file", file); + const child = new Parse.Object('Child'); + child.set('file', file); - const parent = new Parse.Object("Parent"); - parent.set("child", child); + const parent = new Parse.Object('Parent'); + parent.set('child', child); await parent.save(); - const query = new Parse.Query("Parent"); - query.include("child"); + const query = new Parse.Query('Parent'); + query.include('child'); const parentAgain = await query.get(parent.id); - const childAgain = parentAgain.get("child"); - file = childAgain.get("file"); + const childAgain = parentAgain.get('child'); + file = childAgain.get('file'); ok(file instanceof Parse.File); ok(file.name()); ok(file.url()); - notEqual(file.name(), "hello.txt"); + notEqual(file.name(), 'hello.txt'); done(); }); - it("saving an already saved file", async () => { - const file = new Parse.File("hello.txt", data, "text/plain"); + it('saving an already saved file', async () => { + const file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); const result = await file.save(); strictEqual(result, file); ok(file.name()); ok(file.url()); - notEqual(file.name(), "hello.txt"); + notEqual(file.name(), 'hello.txt'); const previousName = file.name(); await file.save(); equal(file.name(), previousName); }); - it("two saves at the same time", done => { - const file = new Parse.File("hello.txt", data, "text/plain"); + it('two saves at the same time', done => { + const file = new Parse.File('hello.txt', data, 'text/plain'); let firstName; let secondName; - const firstSave = file.save().then(function() { firstName = file.name(); }); - const secondSave = file.save().then(function() { secondName = file.name(); }); - - Promise.all([firstSave, secondSave]).then(function() { - equal(firstName, secondName); - done(); - }, function(error) { - ok(false, error); - done(); + const firstSave = file.save().then(function() { + firstName = file.name(); + }); + const secondSave = file.save().then(function() { + secondName = file.name(); }); + + Promise.all([firstSave, secondSave]).then( + function() { + equal(firstName, secondName); + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it("file toJSON testing", async () => { - const file = new Parse.File("hello.txt", data, "text/plain"); + it('file toJSON testing', async () => { + const file = new Parse.File('hello.txt', data, 'text/plain'); ok(!file.url()); - const object = new Parse.Object("TestObject"); + const object = new Parse.Object('TestObject'); await object.save({ - file: file + file: file, }); ok(object.toJSON().file.url); }); - it("content-type used with no extension", done => { + it('content-type used with no extension', done => { const headers = { 'Content-Type': 'text/html', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file', - body: 'fee fi fo', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.name).toMatch(/\.html$/); - request.get(b.url, (error, response) => { - if (!response) { - fail('response should be set'); - return done(); - } - expect(response.headers['content-type']).toMatch(/^text\/html/); - done(); - }); - }); + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file', + body: 'fee fi fo', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(b.name).toMatch(/\.html$/); + request.get(b.url, (error, response) => { + if (!response) { + fail('response should be set'); + return done(); + } + expect(response.headers['content-type']).toMatch(/^text\/html/); + done(); + }); + } + ); }); - it("filename is url encoded", done => { + it('filename is url encoded', done => { const headers = { 'Content-Type': 'text/html', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/hello world.txt', - body: 'oh emm gee', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.url).toMatch(/hello%20world/); - done(); - }) + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/hello world.txt', + body: 'oh emm gee', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(b.url).toMatch(/hello%20world/); + done(); + } + ); }); it('supports array of files', done => { const file = { __type: 'File', url: 'http://meep.meep', - name: 'meep' + name: 'meep', }; const files = [file, file]; const obj = new Parse.Object('FilesArrayTest'); obj.set('files', files); - obj.save().then(() => { - const query = new Parse.Query('FilesArrayTest'); - return query.first(); - }).then((result) => { - const filesAgain = result.get('files'); - expect(filesAgain.length).toEqual(2); - expect(filesAgain[0].name()).toEqual('meep'); - expect(filesAgain[0].url()).toEqual('http://meep.meep'); - done(); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('FilesArrayTest'); + return query.first(); + }) + .then(result => { + const filesAgain = result.get('files'); + expect(filesAgain.length).toEqual(2); + expect(filesAgain[0].name()).toEqual('meep'); + expect(filesAgain[0].url()).toEqual('http://meep.meep'); + done(); + }); }); it('validates filename characters', done => { const headers = { 'Content-Type': 'text/plain', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/di$avowed.txt', - body: 'will fail', - }, (error, response, body) => { - const b = JSON.parse(body); - expect(b.code).toEqual(122); - done(); - }); + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/di$avowed.txt', + body: 'will fail', + }, + (error, response, body) => { + const b = JSON.parse(body); + expect(b.code).toEqual(122); + done(); + } + ); }); it('validates filename length', done => { const headers = { 'Content-Type': 'text/plain', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - const fileName = 'Onceuponamidnightdrearywhileiponderedweak' + - 'andwearyOveramanyquaintandcuriousvolumeof' + - 'forgottenloreWhileinoddednearlynappingsud' + - 'denlytherecameatapping'; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/' + fileName, - body: 'will fail', - }, (error, response, body) => { - const b = JSON.parse(body); - expect(b.code).toEqual(122); - done(); - }); + const fileName = + 'Onceuponamidnightdrearywhileiponderedweak' + + 'andwearyOveramanyquaintandcuriousvolumeof' + + 'forgottenloreWhileinoddednearlynappingsud' + + 'denlytherecameatapping'; + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/' + fileName, + body: 'will fail', + }, + (error, response, body) => { + const b = JSON.parse(body); + expect(b.code).toEqual(122); + done(); + } + ); }); it('supports a dictionary with file', done => { const file = { __type: 'File', url: 'http://meep.meep', - name: 'meep' + name: 'meep', }; const dict = { - file: file + file: file, }; const obj = new Parse.Object('FileObjTest'); obj.set('obj', dict); - obj.save().then(() => { - const query = new Parse.Query('FileObjTest'); - return query.first(); - }).then((result) => { - const dictAgain = result.get('obj'); - expect(typeof dictAgain).toEqual('object'); - const fileAgain = dictAgain['file']; - expect(fileAgain.name()).toEqual('meep'); - expect(fileAgain.url()).toEqual('http://meep.meep'); - done(); - }).catch((e) => { - jfail(e); - done(); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('FileObjTest'); + return query.first(); + }) + .then(result => { + const dictAgain = result.get('obj'); + expect(typeof dictAgain).toEqual('object'); + const fileAgain = dictAgain['file']; + expect(fileAgain.name()).toEqual('meep'); + expect(fileAgain.url()).toEqual('http://meep.meep'); + done(); + }) + .catch(e => { + jfail(e); + done(); + }); }); it('creates correct url for old files hosted on files.parsetfss.com', done => { const file = { __type: 'File', url: 'http://irrelevant.elephant/', - name: 'tfss-123.txt' + name: 'tfss-123.txt', }; const obj = new Parse.Object('OldFileTest'); obj.set('oldfile', file); - obj.save().then(() => { - const query = new Parse.Query('OldFileTest'); - return query.first(); - }).then((result) => { - const fileAgain = result.get('oldfile'); - expect(fileAgain.url()).toEqual( - 'http://files.parsetfss.com/test/tfss-123.txt' - ); - done(); - }).catch((e) => { - jfail(e); - done(); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('OldFileTest'); + return query.first(); + }) + .then(result => { + const fileAgain = result.get('oldfile'); + expect(fileAgain.url()).toEqual( + 'http://files.parsetfss.com/test/tfss-123.txt' + ); + done(); + }) + .catch(e => { + jfail(e); + done(); + }); }); it('creates correct url for old files hosted on files.parse.com', done => { const file = { __type: 'File', url: 'http://irrelevant.elephant/', - name: 'd6e80979-a128-4c57-a167-302f874700dc-123.txt' + name: 'd6e80979-a128-4c57-a167-302f874700dc-123.txt', }; const obj = new Parse.Object('OldFileTest'); obj.set('oldfile', file); - obj.save().then(() => { - const query = new Parse.Query('OldFileTest'); - return query.first(); - }).then((result) => { - const fileAgain = result.get('oldfile'); - expect(fileAgain.url()).toEqual( - 'http://files.parse.com/test/d6e80979-a128-4c57-a167-302f874700dc-123.txt' - ); - done(); - }).catch((e) => { - jfail(e); - done(); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('OldFileTest'); + return query.first(); + }) + .then(result => { + const fileAgain = result.get('oldfile'); + expect(fileAgain.url()).toEqual( + 'http://files.parse.com/test/d6e80979-a128-4c57-a167-302f874700dc-123.txt' + ); + done(); + }) + .catch(e => { + jfail(e); + done(); + }); }); it('supports files in objects without urls', done => { const file = { __type: 'File', - name: '123.txt' + name: '123.txt', }; const obj = new Parse.Object('FileTest'); obj.set('file', file); - obj.save().then(() => { - const query = new Parse.Query('FileTest'); - return query.first(); - }).then(result => { - const fileAgain = result.get('file'); - expect(fileAgain.url()).toMatch(/123.txt$/); - done(); - }).catch((e) => { - jfail(e); - done(); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('FileTest'); + return query.first(); + }) + .then(result => { + const fileAgain = result.get('file'); + expect(fileAgain.url()).toMatch(/123.txt$/); + done(); + }) + .catch(e => { + jfail(e); + done(); + }); }); it('return with publicServerURL when provided', done => { reconfigureServer({ - publicServerURL: 'https://mydomain/parse' - }).then(() => { - const file = { - __type: 'File', - name: '123.txt' - }; - const obj = new Parse.Object('FileTest'); - obj.set('file', file); - return obj.save() - }).then(() => { - const query = new Parse.Query('FileTest'); - return query.first(); - }).then(result => { - const fileAgain = result.get('file'); - expect(fileAgain.url().indexOf('https://mydomain/parse')).toBe(0); - done(); - }).catch((e) => { - jfail(e); - done(); - }); + publicServerURL: 'https://mydomain/parse', + }) + .then(() => { + const file = { + __type: 'File', + name: '123.txt', + }; + const obj = new Parse.Object('FileTest'); + obj.set('file', file); + return obj.save(); + }) + .then(() => { + const query = new Parse.Query('FileTest'); + return query.first(); + }) + .then(result => { + const fileAgain = result.get('file'); + expect(fileAgain.url().indexOf('https://mydomain/parse')).toBe(0); + done(); + }) + .catch(e => { + jfail(e); + done(); + }); }); it('fails to upload an empty file', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: '', - }, (error, response, body) => { - expect(error).toBe(null); - expect(response.statusCode).toBe(400); - expect(body).toEqual('{"code":130,"error":"Invalid file upload."}'); - done(); - }); + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: '', + }, + (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toBe(400); + expect(body).toEqual('{"code":130,"error":"Invalid file upload."}'); + done(); + } + ); }); it('fails to upload without a file name', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/', - body: 'yolo', - }, (error, response, body) => { - expect(error).toBe(null); - expect(response.statusCode).toBe(400); - expect(body).toEqual('{"code":122,"error":"Filename not provided."}'); - done(); - }); + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/', + body: 'yolo', + }, + (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toBe(400); + expect(body).toEqual('{"code":122,"error":"Filename not provided."}'); + done(); + } + ); }); it('fails to delete an unkown file', done => { @@ -584,17 +673,20 @@ describe('Parse.File testing', () => { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Master-Key': 'test' + 'X-Parse-Master-Key': 'test', }; - request.delete({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - }, (error, response, body) => { - expect(error).toBe(null); - expect(response.statusCode).toBe(400); - expect(body).toEqual('{"code":153,"error":"Could not delete file."}'); - done(); - }); + request.delete( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + }, + (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toBe(400); + expect(body).toEqual('{"code":153,"error":"Could not delete file."}'); + done(); + } + ); }); describe_only_db('mongo')('Gridstore Range tests', () => { @@ -602,52 +694,70 @@ describe('Parse.File testing', () => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: 'argle bargle', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - request.get({ url: b.url, headers: { - 'Content-Type': 'application/octet-stream', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'Range': 'bytes=0-5' - } }, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body).toEqual('argle '); - done(); - }); - }); + const b = JSON.parse(body); + request.get( + { + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + Range: 'bytes=0-5', + }, + }, + (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle '); + done(); + } + ); + } + ); }); it('supports small range requests', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: 'argle bargle', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - request.get({ url: b.url, headers: { - 'Content-Type': 'application/octet-stream', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'Range': 'bytes=0-2' - } }, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body).toEqual('arg'); - done(); - }); - }); + const b = JSON.parse(body); + request.get( + { + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + Range: 'bytes=0-2', + }, + }, + (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('arg'); + done(); + } + ); + } + ); }); // See specs https://www.greenbytes.de/tech/webdav/draft-ietf-httpbis-p5-range-latest.html#byte.ranges @@ -655,79 +765,106 @@ describe('Parse.File testing', () => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: 'argle bargle', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - request.get({ url: b.url, headers: { - 'Content-Type': 'application/octet-stream', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'Range': 'bytes=2-2' - } }, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body).toEqual('g'); - done(); - }); - }); + const b = JSON.parse(body); + request.get( + { + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + Range: 'bytes=2-2', + }, + }, + (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('g'); + done(); + } + ); + } + ); }); it('supports getting last n bytes', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: 'something different', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - request.get({ url: b.url, headers: { - 'Content-Type': 'application/octet-stream', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'Range': 'bytes=-4' - } }, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'something different', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body.length).toBe(4); - expect(body).toEqual('rent'); - done(); - }); - }); + const b = JSON.parse(body); + request.get( + { + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + Range: 'bytes=-4', + }, + }, + (error, response, body) => { + expect(error).toBe(null); + expect(body.length).toBe(4); + expect(body).toEqual('rent'); + done(); + } + ); + } + ); }); it('supports getting first n bytes', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: 'something different', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - request.get({ url: b.url, headers: { - 'Content-Type': 'application/octet-stream', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'Range': 'bytes=10-' - } }, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'something different', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body).toEqual('different'); - done(); - }); - }); + const b = JSON.parse(body); + request.get( + { + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + Range: 'bytes=10-', + }, + }, + (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('different'); + done(); + } + ); + } + ); }); function repeat(string, count) { @@ -743,41 +880,56 @@ describe('Parse.File testing', () => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: repeat('argle bargle', 100) - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - request.get({ url: b.url, headers: { - 'Content-Type': 'application/octet-stream', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'Range': 'bytes=13-240' - } }, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: repeat('argle bargle', 100), + }, + (error, response, body) => { expect(error).toBe(null); - expect(body.length).toEqual(228); - expect(body.indexOf('rgle barglea')).toBe(0); - done(); - }); - }); + const b = JSON.parse(body); + request.get( + { + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + Range: 'bytes=13-240', + }, + }, + (error, response, body) => { + expect(error).toBe(null); + expect(body.length).toEqual(228); + expect(body.indexOf('rgle barglea')).toBe(0); + done(); + } + ); + } + ); }); it('fails to stream unknown file', done => { - request.get({ url: 'http://localhost:8378/1/files/test/file.txt', headers: { - 'Content-Type': 'application/octet-stream', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'Range': 'bytes=13-240' - } }, (error, response, body) => { - expect(error).toBe(null); - expect(response.statusCode).toBe(404); - expect(body).toEqual('File not found.'); - done(); - }); + request.get( + { + url: 'http://localhost:8378/1/files/test/file.txt', + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + Range: 'bytes=13-240', + }, + }, + (error, response, body) => { + expect(error).toBe(null); + expect(response.statusCode).toBe(404); + expect(body).toEqual('File not found.'); + done(); + } + ); }); }); @@ -788,26 +940,35 @@ describe('Parse.File testing', () => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/files/file.txt', - body: 'argle bargle', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - request.get({ url: b.url, headers: { - 'Content-Type': 'application/octet-stream', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'Range': 'bytes=0-5' - } }, (error, response, body) => { + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/files/file.txt', + body: 'argle bargle', + }, + (error, response, body) => { expect(error).toBe(null); - expect(body).toEqual('argle bargle'); - done(); - }); - }); + const b = JSON.parse(body); + request.get( + { + url: b.url, + headers: { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + Range: 'bytes=0-5', + }, + }, + (error, response, body) => { + expect(error).toBe(null); + expect(body).toEqual('argle bargle'); + done(); + } + ); + } + ); }); }); }); diff --git a/spec/ParseGeoPoint.spec.js b/spec/ParseGeoPoint.spec.js index 20b49dfaaa..ea90e5c798 100644 --- a/spec/ParseGeoPoint.spec.js +++ b/spec/ParseGeoPoint.spec.js @@ -5,7 +5,6 @@ const rp = require('request-promise'); const TestObject = Parse.Object.extend('TestObject'); describe('Parse.GeoPoint testing', () => { - it('geo point roundtrip', async () => { const point = new Parse.GeoPoint(44.0, -11.0); const obj = new TestObject(); @@ -20,74 +19,87 @@ describe('Parse.GeoPoint testing', () => { equal(pointAgain.longitude, -11.0); }); - it('update geopoint', (done) => { + it('update geopoint', done => { const oldPoint = new Parse.GeoPoint(44.0, -11.0); const newPoint = new Parse.GeoPoint(24.0, 19.0); const obj = new TestObject(); obj.set('location', oldPoint); - obj.save().then(() => { - obj.set('location', newPoint); - return obj.save(); - }).then(() => { - const query = new Parse.Query(TestObject); - return query.get(obj.id); - }).then((result) => { - const point = result.get('location'); - equal(point.latitude, newPoint.latitude); - equal(point.longitude, newPoint.longitude); - done(); - }); + obj + .save() + .then(() => { + obj.set('location', newPoint); + return obj.save(); + }) + .then(() => { + const query = new Parse.Query(TestObject); + return query.get(obj.id); + }) + .then(result => { + const point = result.get('location'); + equal(point.latitude, newPoint.latitude); + equal(point.longitude, newPoint.longitude); + done(); + }); }); - it('has the correct __type field in the json response', async (done) => { + it('has the correct __type field in the json response', async done => { const point = new Parse.GeoPoint(44.0, -11.0); const obj = new TestObject(); obj.set('location', point); - obj.set('name', 'Zhoul') + obj.set('name', 'Zhoul'); await obj.save(); Parse.Cloud.httpRequest({ url: 'http://localhost:8378/1/classes/TestObject/' + obj.id, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test' - } + 'X-Parse-Master-Key': 'test', + }, }).then(response => { equal(response.data.location.__type, 'GeoPoint'); done(); }); }); - it('creating geo point exception two fields', (done) => { + it('creating geo point exception two fields', done => { const point = new Parse.GeoPoint(20, 20); const obj = new TestObject(); obj.set('locationOne', point); obj.set('locationTwo', point); - obj.save().then(() => { - fail('expected error'); - }, (err) => { - equal(err.code, Parse.Error.INCORRECT_TYPE); - done(); - }); + obj.save().then( + () => { + fail('expected error'); + }, + err => { + equal(err.code, Parse.Error.INCORRECT_TYPE); + done(); + } + ); }); // TODO: This should also have support in postgres, or higher level database agnostic support. - it_exclude_dbs(['postgres'])('updating geo point exception two fields', async (done) => { - const point = new Parse.GeoPoint(20, 20); - const obj = new TestObject(); - obj.set('locationOne', point); - await obj.save(); - obj.set('locationTwo', point); - obj.save().then(() => { - fail('expected error'); - }, (err) => { - equal(err.code, Parse.Error.INCORRECT_TYPE); - done(); - }); - }); + it_exclude_dbs(['postgres'])( + 'updating geo point exception two fields', + async done => { + const point = new Parse.GeoPoint(20, 20); + const obj = new TestObject(); + obj.set('locationOne', point); + await obj.save(); + obj.set('locationTwo', point); + obj.save().then( + () => { + fail('expected error'); + }, + err => { + equal(err.code, Parse.Error.INCORRECT_TYPE); + done(); + } + ); + } + ); - it('geo line', async (done) => { + it('geo line', async done => { const line = []; - for (let i = 0; i < 10; ++i) { + for (let i = 0; i < 10; ++i) { const obj = new TestObject(); const point = new Parse.GeoPoint(i * 4.0 - 12.0, i * 3.2 - 11.0); obj.set('location', point); @@ -100,14 +112,14 @@ describe('Parse.GeoPoint testing', () => { const point = new Parse.GeoPoint(24, 19); query.equalTo('construct', 'line'); query.withinMiles('location', point, 10000); - const results = await query.find() + const results = await query.find(); equal(results.length, 10); equal(results[0].get('seq'), 9); equal(results[3].get('seq'), 6); done(); }); - it('geo max distance large', (done) => { + it('geo max distance large', done => { const objects = []; [0, 1, 2].map(function(i) { const obj = new TestObject(); @@ -116,18 +128,23 @@ describe('Parse.GeoPoint testing', () => { obj.set('index', i); objects.push(obj); }); - Parse.Object.saveAll(objects).then(() => { - const query = new Parse.Query(TestObject); - const point = new Parse.GeoPoint(1.0, -1.0); - query.withinRadians('location', point, 3.14); - return query.find(); - }).then((results) => { - equal(results.length, 3); - done(); - }, (err) => { - fail("Couldn't query GeoPoint"); - jfail(err) - }); + Parse.Object.saveAll(objects) + .then(() => { + const query = new Parse.Query(TestObject); + const point = new Parse.GeoPoint(1.0, -1.0); + query.withinRadians('location', point, 3.14); + return query.find(); + }) + .then( + results => { + equal(results.length, 3); + done(); + }, + err => { + fail("Couldn't query GeoPoint"); + jfail(err); + } + ); }); it('geo max distance medium', async () => { @@ -139,7 +156,7 @@ describe('Parse.GeoPoint testing', () => { obj.set('index', i); objects.push(obj); }); - await Parse.Object.saveAll(objects) + await Parse.Object.saveAll(objects); const query = new Parse.Query(TestObject); const point = new Parse.GeoPoint(1.0, -1.0); query.withinRadians('location', point, 3.14 * 0.5); @@ -169,7 +186,7 @@ describe('Parse.GeoPoint testing', () => { const makeSomeGeoPoints = function() { const sacramento = new TestObject(); - sacramento.set('location', new Parse.GeoPoint(38.52, -121.50)); + sacramento.set('location', new Parse.GeoPoint(38.52, -121.5)); sacramento.set('name', 'Sacramento'); const honolulu = new TestObject(); @@ -183,7 +200,7 @@ describe('Parse.GeoPoint testing', () => { return Parse.Object.saveAll([sacramento, sf, honolulu]); }; - it('geo max distance in km everywhere', async (done) => { + it('geo max distance in km everywhere', async done => { await makeSomeGeoPoints(); const sfo = new Parse.GeoPoint(37.6189722, -122.3748889); const query = new Parse.Query(TestObject); @@ -273,23 +290,25 @@ describe('Parse.GeoPoint testing', () => { equal(results[1].get('name'), 'Sacramento'); }); - it('works with geobox queries', (done) => { + it('works with geobox queries', done => { const inbound = new Parse.GeoPoint(1.5, 1.5); const onbound = new Parse.GeoPoint(10, 10); const outbound = new Parse.GeoPoint(20, 20); - const obj1 = new Parse.Object('TestObject', {location: inbound}); - const obj2 = new Parse.Object('TestObject', {location: onbound}); - const obj3 = new Parse.Object('TestObject', {location: outbound}); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const sw = new Parse.GeoPoint(0, 0); - const ne = new Parse.GeoPoint(10, 10); - const query = new Parse.Query(TestObject); - query.withinGeoBox('location', sw, ne); - return query.find(); - }).then((results) => { - equal(results.length, 2); - done(); - }); + const obj1 = new Parse.Object('TestObject', { location: inbound }); + const obj2 = new Parse.Object('TestObject', { location: onbound }); + const obj3 = new Parse.Object('TestObject', { location: outbound }); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const sw = new Parse.GeoPoint(0, 0); + const ne = new Parse.GeoPoint(10, 10); + const query = new Parse.Query(TestObject); + query.withinGeoBox('location', sw, ne); + return query.find(); + }) + .then(results => { + equal(results.length, 2); + done(); + }); }); it('supports a sub-object with a geo point', async () => { @@ -310,7 +329,7 @@ describe('Parse.GeoPoint testing', () => { const point1 = new Parse.GeoPoint(44.0, -11.0); const point2 = new Parse.GeoPoint(22.0, -55.0); const obj = new TestObject(); - obj.set('locations', [ point1, point2 ]); + obj.set('locations', [point1, point2]); await obj.save(); const query = new Parse.Query(TestObject); const results = await query.find(); @@ -321,349 +340,371 @@ describe('Parse.GeoPoint testing', () => { expect(locations[1]).toEqual(point2); }); - it('equalTo geopoint', (done) => { + it('equalTo geopoint', done => { const point = new Parse.GeoPoint(44.0, -11.0); const obj = new TestObject(); obj.set('location', point); - obj.save().then(() => { - const query = new Parse.Query(TestObject); - query.equalTo('location', point); - return query.find(); - }).then((results) => { - equal(results.length, 1); - const loc = results[0].get('location'); - equal(loc.latitude, point.latitude); - equal(loc.longitude, point.longitude); - done(); - }); + obj + .save() + .then(() => { + const query = new Parse.Query(TestObject); + query.equalTo('location', point); + return query.find(); + }) + .then(results => { + equal(results.length, 1); + const loc = results[0].get('location'); + equal(loc.latitude, point.latitude); + equal(loc.longitude, point.longitude); + done(); + }); }); - it('supports withinPolygon open path', (done) => { + it('supports withinPolygon open path', done => { const inbound = new Parse.GeoPoint(1.5, 1.5); const onbound = new Parse.GeoPoint(10, 10); const outbound = new Parse.GeoPoint(20, 20); - const obj1 = new Parse.Object('Polygon', {location: inbound}); - const obj2 = new Parse.Object('Polygon', {location: onbound}); - const obj3 = new Parse.Object('Polygon', {location: outbound}); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const where = { - location: { - $geoWithin: { - $polygon: [ - { __type: 'GeoPoint', latitude: 0, longitude: 0 }, - { __type: 'GeoPoint', latitude: 0, longitude: 10 }, - { __type: 'GeoPoint', latitude: 10, longitude: 10 }, - { __type: 'GeoPoint', latitude: 10, longitude: 0 } - ] - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(2); - done(); - }, done.fail); - }); - - it('supports withinPolygon closed path', (done) => { + const obj1 = new Parse.Object('Polygon', { location: inbound }); + const obj2 = new Parse.Object('Polygon', { location: onbound }); + const obj3 = new Parse.Object('Polygon', { location: outbound }); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: [ + { __type: 'GeoPoint', latitude: 0, longitude: 0 }, + { __type: 'GeoPoint', latitude: 0, longitude: 10 }, + { __type: 'GeoPoint', latitude: 10, longitude: 10 }, + { __type: 'GeoPoint', latitude: 10, longitude: 0 }, + ], + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(2); + done(); + }, done.fail); + }); + + it('supports withinPolygon closed path', done => { const inbound = new Parse.GeoPoint(1.5, 1.5); const onbound = new Parse.GeoPoint(10, 10); const outbound = new Parse.GeoPoint(20, 20); - const obj1 = new Parse.Object('Polygon', {location: inbound}); - const obj2 = new Parse.Object('Polygon', {location: onbound}); - const obj3 = new Parse.Object('Polygon', {location: outbound}); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const where = { - location: { - $geoWithin: { - $polygon: [ - { __type: 'GeoPoint', latitude: 0, longitude: 0 }, - { __type: 'GeoPoint', latitude: 0, longitude: 10 }, - { __type: 'GeoPoint', latitude: 10, longitude: 10 }, - { __type: 'GeoPoint', latitude: 10, longitude: 0 }, - { __type: 'GeoPoint', latitude: 0, longitude: 0 } - ] - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(2); - done(); - }, done.fail); - }); - - it('supports withinPolygon Polygon object', (done) => { + const obj1 = new Parse.Object('Polygon', { location: inbound }); + const obj2 = new Parse.Object('Polygon', { location: onbound }); + const obj3 = new Parse.Object('Polygon', { location: outbound }); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: [ + { __type: 'GeoPoint', latitude: 0, longitude: 0 }, + { __type: 'GeoPoint', latitude: 0, longitude: 10 }, + { __type: 'GeoPoint', latitude: 10, longitude: 10 }, + { __type: 'GeoPoint', latitude: 10, longitude: 0 }, + { __type: 'GeoPoint', latitude: 0, longitude: 0 }, + ], + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(2); + done(); + }, done.fail); + }); + + it('supports withinPolygon Polygon object', done => { const inbound = new Parse.GeoPoint(1.5, 1.5); const onbound = new Parse.GeoPoint(10, 10); const outbound = new Parse.GeoPoint(20, 20); - const obj1 = new Parse.Object('Polygon', {location: inbound}); - const obj2 = new Parse.Object('Polygon', {location: onbound}); - const obj3 = new Parse.Object('Polygon', {location: outbound}); + const obj1 = new Parse.Object('Polygon', { location: inbound }); + const obj2 = new Parse.Object('Polygon', { location: onbound }); + const obj3 = new Parse.Object('Polygon', { location: outbound }); const polygon = { __type: 'Polygon', - coordinates: [ - [0, 0], - [10, 0], - [10, 10], - [0, 10], - [0, 0] - ] - } - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const where = { - location: { - $geoWithin: { - $polygon: polygon - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(2); - done(); - }, done.fail); - }); - - it('invalid Polygon object withinPolygon', (done) => { + coordinates: [[0, 0], [10, 0], [10, 10], [0, 10], [0, 0]], + }; + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: polygon, + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(2); + done(); + }, done.fail); + }); + + it('invalid Polygon object withinPolygon', done => { const point = new Parse.GeoPoint(1.5, 1.5); - const obj = new Parse.Object('Polygon', {location: point}); + const obj = new Parse.Object('Polygon', { location: point }); const polygon = { __type: 'Polygon', - coordinates: [ - [0, 0], - [10, 0], - ] - } - obj.save().then(() => { - const where = { - location: { - $geoWithin: { - $polygon: polygon - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + coordinates: [[0, 0], [10, 0]], + }; + obj + .save() + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: polygon, + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); }); - it('out of bounds Polygon object withinPolygon', (done) => { + it('out of bounds Polygon object withinPolygon', done => { const point = new Parse.GeoPoint(1.5, 1.5); - const obj = new Parse.Object('Polygon', {location: point}); + const obj = new Parse.Object('Polygon', { location: point }); const polygon = { __type: 'Polygon', - coordinates: [ - [0, 0], - [181, 0], - [0, 10] - ] - } - obj.save().then(() => { - const where = { - location: { - $geoWithin: { - $polygon: polygon - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + coordinates: [[0, 0], [181, 0], [0, 10]], + }; + obj + .save() + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: polygon, + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(1); + done(); }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(1); - done(); - }); }); - it('invalid input withinPolygon', (done) => { + it('invalid input withinPolygon', done => { const point = new Parse.GeoPoint(1.5, 1.5); - const obj = new Parse.Object('Polygon', {location: point}); - obj.save().then(() => { - const where = { - location: { - $geoWithin: { - $polygon: 1234 - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + const obj = new Parse.Object('Polygon', { location: point }); + obj + .save() + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: 1234, + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); }); - it('invalid geoPoint withinPolygon', (done) => { + it('invalid geoPoint withinPolygon', done => { const point = new Parse.GeoPoint(1.5, 1.5); - const obj = new Parse.Object('Polygon', {location: point}); - obj.save().then(() => { - const where = { - location: { - $geoWithin: { - $polygon: [ - {} - ] - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + const obj = new Parse.Object('Polygon', { location: point }); + obj + .save() + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: [{}], + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); }); - it('invalid latitude withinPolygon', (done) => { + it('invalid latitude withinPolygon', done => { const point = new Parse.GeoPoint(1.5, 1.5); - const obj = new Parse.Object('Polygon', {location: point}); - obj.save().then(() => { - const where = { - location: { - $geoWithin: { - $polygon: [ - { __type: 'GeoPoint', latitude: 0, longitude: 0 }, - { __type: 'GeoPoint', latitude: 181, longitude: 0 }, - { __type: 'GeoPoint', latitude: 0, longitude: 0 } - ] - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + const obj = new Parse.Object('Polygon', { location: point }); + obj + .save() + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: [ + { __type: 'GeoPoint', latitude: 0, longitude: 0 }, + { __type: 'GeoPoint', latitude: 181, longitude: 0 }, + { __type: 'GeoPoint', latitude: 0, longitude: 0 }, + ], + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(1); + done(); }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(1); - done(); - }); }); - it('invalid longitude withinPolygon', (done) => { + it('invalid longitude withinPolygon', done => { const point = new Parse.GeoPoint(1.5, 1.5); - const obj = new Parse.Object('Polygon', {location: point}); - obj.save().then(() => { - const where = { - location: { - $geoWithin: { - $polygon: [ - { __type: 'GeoPoint', latitude: 0, longitude: 0 }, - { __type: 'GeoPoint', latitude: 0, longitude: 181 }, - { __type: 'GeoPoint', latitude: 0, longitude: 0 } - ] - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + const obj = new Parse.Object('Polygon', { location: point }); + obj + .save() + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: [ + { __type: 'GeoPoint', latitude: 0, longitude: 0 }, + { __type: 'GeoPoint', latitude: 0, longitude: 181 }, + { __type: 'GeoPoint', latitude: 0, longitude: 0 }, + ], + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(1); + done(); }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(1); - done(); - }); }); - it('minimum 3 points withinPolygon', (done) => { + it('minimum 3 points withinPolygon', done => { const point = new Parse.GeoPoint(1.5, 1.5); - const obj = new Parse.Object('Polygon', {location: point}); - obj.save().then(() => { - const where = { - location: { - $geoWithin: { - $polygon: [] - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/Polygon', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + const obj = new Parse.Object('Polygon', { location: point }); + obj + .save() + .then(() => { + const where = { + location: { + $geoWithin: { + $polygon: [], + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/Polygon', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(107); + done(); }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(107); - done(); - }); }); }); diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js index 2e1032a670..c3e9ff771d 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -6,168 +6,209 @@ const Config = require('../lib/Config'); describe('a GlobalConfig', () => { beforeEach(done => { const config = Config.get('test'); - const query = on_db('mongo', () => { - // Legacy is with an int... - return { objectId: 1 }; - }, () => { - return { objectId: "1" } - }) - config.database.adapter.upsertOneObject( - '_GlobalConfig', - { fields: { objectId: { type: 'Number' }, params: {type: 'Object'}} }, - query, - { params: { companies: ['US', 'DK'] } } - ).then(done, (err) => { - jfail(err); - done(); - }); + const query = on_db( + 'mongo', + () => { + // Legacy is with an int... + return { objectId: 1 }; + }, + () => { + return { objectId: '1' }; + } + ); + config.database.adapter + .upsertOneObject( + '_GlobalConfig', + { + fields: { objectId: { type: 'Number' }, params: { type: 'Object' } }, + }, + query, + { params: { companies: ['US', 'DK'] } } + ) + .then(done, err => { + jfail(err); + done(); + }); }); - it('can be retrieved', (done) => { - request.get({ - url : 'http://localhost:8378/1/config', - json : true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key' : 'test' + it('can be retrieved', done => { + request.get( + { + url: 'http://localhost:8378/1/config', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }, + (error, response, body) => { + try { + expect(response.statusCode).toEqual(200); + expect(body.params.companies).toEqual(['US', 'DK']); + } catch (e) { + jfail(e); + } + done(); } - }, (error, response, body) => { - try { - expect(response.statusCode).toEqual(200); - expect(body.params.companies).toEqual(['US', 'DK']); - } catch(e) { jfail(e); } - done(); - }); + ); }); - it('can be updated when a master key exists', (done) => { - request.put({ - url : 'http://localhost:8378/1/config', - json : true, - body : { params: { companies: ['US', 'DK', 'SE'] } }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key' : 'test' + it('can be updated when a master key exists', done => { + request.put( + { + url: 'http://localhost:8378/1/config', + json: true, + body: { params: { companies: ['US', 'DK', 'SE'] } }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(200); + expect(body.result).toEqual(true); + done(); } - }, (error, response, body) => { - expect(response.statusCode).toEqual(200); - expect(body.result).toEqual(true); - done(); - }); + ); }); - it('can add and retrive files', (done) => { - request.put({ - url : 'http://localhost:8378/1/config', - json : true, - body : { params: { file: { __type: 'File', name: 'name', url: 'http://url' } } }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key' : 'test' + it('can add and retrive files', done => { + request.put( + { + url: 'http://localhost:8378/1/config', + json: true, + body: { + params: { file: { __type: 'File', name: 'name', url: 'http://url' } }, + }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(200); + expect(body.result).toEqual(true); + Parse.Config.get().then(res => { + const file = res.get('file'); + expect(file.name()).toBe('name'); + expect(file.url()).toBe('http://url'); + done(); + }); } - }, (error, response, body) => { - expect(response.statusCode).toEqual(200); - expect(body.result).toEqual(true); - Parse.Config.get().then((res) => { - const file = res.get('file'); - expect(file.name()).toBe('name'); - expect(file.url()).toBe('http://url'); - done(); - }); - }); + ); }); - it('can add and retrive Geopoints', (done) => { - const geopoint = new Parse.GeoPoint(10,-20); - request.put({ - url : 'http://localhost:8378/1/config', - json : true, - body : { params: { point: geopoint.toJSON() } }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key' : 'test' + it('can add and retrive Geopoints', done => { + const geopoint = new Parse.GeoPoint(10, -20); + request.put( + { + url: 'http://localhost:8378/1/config', + json: true, + body: { params: { point: geopoint.toJSON() } }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(200); + expect(body.result).toEqual(true); + Parse.Config.get().then(res => { + const point = res.get('point'); + expect(point.latitude).toBe(10); + expect(point.longitude).toBe(-20); + done(); + }); } - }, (error, response, body) => { - expect(response.statusCode).toEqual(200); - expect(body.result).toEqual(true); - Parse.Config.get().then((res) => { - const point = res.get('point'); - expect(point.latitude).toBe(10); - expect(point.longitude).toBe(-20); - done(); - }); - }); + ); }); - it('properly handles delete op', (done) => { - request.put({ - url : 'http://localhost:8378/1/config', - 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, + it('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) => { - try { - expect(response.statusCode).toEqual(200); - expect(body.params.companies).toBeUndefined(); - expect(body.params.foo).toBe('bar'); - expect(Object.keys(body.params).length).toBe(1); - } catch(e) { jfail(e); } - done(); - }); - }); + '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) => { + try { + expect(response.statusCode).toEqual(200); + expect(body.params.companies).toBeUndefined(); + expect(body.params.foo).toBe('bar'); + expect(Object.keys(body.params).length).toBe(1); + } catch (e) { + jfail(e); + } + done(); + } + ); + } + ); }); - it('fail to update if master key is missing', (done) => { - request.put({ - url : 'http://localhost:8378/1/config', - json : true, - body : { params: { companies: [] } }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key' : 'rest' + it('fail to update if master key is missing', done => { + request.put( + { + url: 'http://localhost:8378/1/config', + json: true, + body: { params: { companies: [] } }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized: master key is required'); + done(); } - }, (error, response, body) => { - expect(response.statusCode).toEqual(403); - expect(body.error).toEqual('unauthorized: master key is required'); - done(); - }); + ); }); - it('failed getting config when it is missing', (done) => { + it('failed getting config when it is missing', done => { const config = Config.get('test'); - 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({}); + 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(); + } + ); + }) + .catch(e => { + jfail(e); done(); }); - }).catch((e) => { - jfail(e); - done(); - }); }); }); diff --git a/spec/ParseHooks.spec.js b/spec/ParseHooks.spec.js index 5c3eae8b68..524b7deb84 100644 --- a/spec/ParseHooks.spec.js +++ b/spec/ParseHooks.spec.js @@ -1,509 +1,717 @@ -"use strict"; +'use strict'; /* global describe, it, expect, fail, Parse */ const request = require('request'); const triggers = require('../lib/triggers'); const HooksController = require('../lib/Controllers/HooksController').default; -const express = require("express"); +const express = require('express'); const bodyParser = require('body-parser'); const auth = require('../lib/Auth'); const Config = require('../lib/Config'); - const port = 12345; -const hookServerURL = "http://localhost:" + port; +const hookServerURL = 'http://localhost:' + port; const AppCache = require('../lib/cache').AppCache; describe('Hooks', () => { let server; let app; - beforeAll((done) => { + beforeAll(done => { app = express(); - app.use(bodyParser.json({ 'type': '*/*' })) + app.use(bodyParser.json({ type: '*/*' })); server = app.listen(12345, undefined, done); }); - afterAll((done) => { + afterAll(done => { server.close(done); }); - it("should have no hooks registered", (done) => { - Parse.Hooks.getFunctions().then((res) => { - expect(res.constructor).toBe(Array.prototype.constructor); - done(); - }, (err) => { - jfail(err); - done(); - }); + it('should have no hooks registered', done => { + Parse.Hooks.getFunctions().then( + res => { + expect(res.constructor).toBe(Array.prototype.constructor); + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - it("should have no triggers registered", (done) => { - Parse.Hooks.getTriggers().then((res) => { - expect(res.constructor).toBe(Array.prototype.constructor); - done(); - }, (err) => { - jfail(err); - done(); - }); + it('should have no triggers registered', done => { + Parse.Hooks.getTriggers().then( + res => { + expect(res.constructor).toBe(Array.prototype.constructor); + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - it("should CRUD a function registration", (done) => { + it('should CRUD a function registration', done => { // Create - Parse.Hooks.createFunction("My-Test-Function", "http://someurl") + Parse.Hooks.createFunction('My-Test-Function', 'http://someurl') .then(response => { - expect(response.functionName).toBe("My-Test-Function"); - expect(response.url).toBe("http://someurl") + expect(response.functionName).toBe('My-Test-Function'); + expect(response.url).toBe('http://someurl'); // Find - return Parse.Hooks.getFunction("My-Test-Function") - }).then(response => { + 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"); + expect(response.url).toBe('http://someurl'); + return Parse.Hooks.updateFunction( + 'My-Test-Function', + 'http://anotherurl' + ); }) - .then((res) => { + .then(res => { expect(res.objectId).toBeUndefined(); - expect(res.functionName).toBe("My-Test-Function"); - expect(res.url).toBe("http://anotherurl") + expect(res.functionName).toBe('My-Test-Function'); + expect(res.url).toBe('http://anotherurl'); // delete - return Parse.Hooks.removeFunction("My-Test-Function") + return Parse.Hooks.removeFunction('My-Test-Function'); }) .then(() => { - // Find again! but should be deleted - return Parse.Hooks.getFunction("My-Test-Function") - .then(res => { - fail("Failed to delete hook") - fail(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) => { + }, + err => { expect(err.code).toBe(143); - expect(err.message).toBe("no function named: My-Test-Function is defined") + expect(err.message).toBe( + 'no function named: My-Test-Function is defined' + ); done(); return Promise.resolve(); - }) + } + ); }) .catch(error => { jfail(error); done(); - }) + }); }); - it("should CRUD a trigger registration", (done) => { + it('should CRUD a trigger registration', done => { // Create - Parse.Hooks.createTrigger("MyClass","beforeDelete", "http://someurl").then((res) => { - expect(res.className).toBe("MyClass"); - expect(res.triggerName).toBe("beforeDelete"); - expect(res.url).toBe("http://someurl") - // Find - return Parse.Hooks.getTrigger("MyClass","beforeDelete"); - }, (err) => { - fail(err); - done(); - }).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"); - }, (err) => { - jfail(err); - done(); - }).then((res) => { - expect(res.className).toBe("MyClass"); - expect(res.url).toBe("http://anotherurl") - expect(res.objectId).toBeUndefined(); - - return Parse.Hooks.removeTrigger("MyClass","beforeDelete"); - }, (err) => { - jfail(err); - done(); - }).then(() => { - // Find again! but should be deleted - return Parse.Hooks.getTrigger("MyClass","beforeDelete"); - }, (err) => { - jfail(err); - done(); - }).then(function(){ - fail("should not succeed"); - done(); - }, (err) => { - if (err) { - expect(err).not.toBe(null); - expect(err).not.toBe(undefined); - expect(err.code).toBe(143); - expect(err.message).toBe("class MyClass does not exist") - } else { - fail('should have errored'); - } - done(); - }); + Parse.Hooks.createTrigger('MyClass', 'beforeDelete', 'http://someurl') + .then( + res => { + expect(res.className).toBe('MyClass'); + expect(res.triggerName).toBe('beforeDelete'); + expect(res.url).toBe('http://someurl'); + // Find + return Parse.Hooks.getTrigger('MyClass', 'beforeDelete'); + }, + err => { + fail(err); + done(); + } + ) + .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' + ); + }, + err => { + jfail(err); + done(); + } + ) + .then( + res => { + expect(res.className).toBe('MyClass'); + expect(res.url).toBe('http://anotherurl'); + expect(res.objectId).toBeUndefined(); + + return Parse.Hooks.removeTrigger('MyClass', 'beforeDelete'); + }, + err => { + jfail(err); + done(); + } + ) + .then( + () => { + // Find again! but should be deleted + return Parse.Hooks.getTrigger('MyClass', 'beforeDelete'); + }, + err => { + jfail(err); + done(); + } + ) + .then( + function() { + fail('should not succeed'); + done(); + }, + err => { + if (err) { + expect(err).not.toBe(null); + expect(err).not.toBe(undefined); + expect(err.code).toBe(143); + expect(err.message).toBe('class MyClass does not exist'); + } else { + fail('should have errored'); + } + done(); + } + ); }); - it("should fail to register hooks without Master Key", (done) => { - request.post(Parse.serverURL + "/hooks/functions", { - headers: { - "X-Parse-Application-Id": Parse.applicationId, - "X-Parse-REST-API-Key": Parse.restKey, + it('should fail to register hooks without Master Key', done => { + request.post( + Parse.serverURL + '/hooks/functions', + { + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': Parse.restKey, + }, + body: JSON.stringify({ + url: 'http://hello.word', + functionName: 'SomeFunction', + }), }, - body: JSON.stringify({ url: "http://hello.word", functionName: "SomeFunction"}) - }, (err, res, body) => { - body = JSON.parse(body); - expect(body.error).toBe("unauthorized"); - done(); - }) + (err, res, body) => { + body = JSON.parse(body); + expect(body.error).toBe('unauthorized'); + done(); + } + ); }); - it("should fail trying to create two times the same function", (done) => { - Parse.Hooks.createFunction("my_new_function", "http://url.com") + it('should fail trying to create two times the same function', done => { + Parse.Hooks.createFunction('my_new_function', 'http://url.com') .then(() => new Promise(resolve => setTimeout(resolve, 100))) - .then(() => { - return Parse.Hooks.createFunction("my_new_function", "http://url.com") - }, () => { - fail("should create a new function"); - }).then(() => { - fail("should not be able to create the same function"); - }, (err) => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(143); - expect(err.message).toBe('function name: my_new_function already exits') + .then( + () => { + return Parse.Hooks.createFunction( + 'my_new_function', + 'http://url.com' + ); + }, + () => { + fail('should create a new function'); } - return Parse.Hooks.removeFunction("my_new_function"); - }).then(() => { - done(); - }, (err) => { - jfail(err); - done(); - }) + ) + .then( + () => { + fail('should not be able to create the same function'); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(143); + expect(err.message).toBe( + 'function name: my_new_function already exits' + ); + } + return Parse.Hooks.removeFunction('my_new_function'); + } + ) + .then( + () => { + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - it("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") - }, () => { - fail("should create a new trigger"); - }).then(() => { - fail("should not be able to create the same trigger"); - }, (err) => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(143); - expect(err.message).toBe('class MyClass already has trigger beforeSave') - } - return Parse.Hooks.removeTrigger("MyClass", "beforeSave"); - }).then(() => { - done(); - }, (err) => { - jfail(err); - done(); - }) + it('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' + ); + }, + () => { + fail('should create a new trigger'); + } + ) + .then( + () => { + fail('should not be able to create the same trigger'); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(143); + expect(err.message).toBe( + 'class MyClass already has trigger beforeSave' + ); + } + return Parse.Hooks.removeTrigger('MyClass', 'beforeSave'); + } + ) + .then( + () => { + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - it("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) => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(143); - expect(err.message).toBe('no function named: A_COOL_FUNCTION is defined'); - } - return Parse.Hooks.getFunction("A_COOL_FUNCTION") - }).then(() => { - fail("the function should not exist"); - done(); - }, (err) => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(143); - expect(err.message).toBe('no function named: A_COOL_FUNCTION is defined'); - } - done(); - }); + it("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 => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(143); + expect(err.message).toBe( + 'no function named: A_COOL_FUNCTION is defined' + ); + } + return Parse.Hooks.getFunction('A_COOL_FUNCTION'); + } + ) + .then( + () => { + fail('the function should not exist'); + done(); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(143); + expect(err.message).toBe( + 'no function named: A_COOL_FUNCTION is defined' + ); + } + done(); + } + ); }); - it("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) => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(143); - expect(err.message).toBe('class AClassName does not exist'); - } - return Parse.Hooks.getTrigger("AClassName","beforeSave") - }).then(() => { - fail("the function should not exist"); - done(); - }, (err) => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(143); - expect(err.message).toBe('class AClassName does not exist'); - } - done(); - }); + it("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 => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(143); + expect(err.message).toBe('class AClassName does not exist'); + } + return Parse.Hooks.getTrigger('AClassName', 'beforeSave'); + } + ) + .then( + () => { + fail('the function should not exist'); + done(); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(143); + expect(err.message).toBe('class AClassName does not exist'); + } + done(); + } + ); }); - - it("should fail trying to create a malformed function", (done) => { - Parse.Hooks.createFunction("MyFunction").then((res) => { - fail(res); - }, (err) => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(143); - expect(err.error).toBe("invalid hook declaration"); + it('should fail trying to create a malformed function', done => { + Parse.Hooks.createFunction('MyFunction').then( + res => { + fail(res); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(143); + expect(err.error).toBe('invalid hook declaration'); + } + done(); } - done(); - }); + ); }); - it("should fail trying to create a malformed function (REST)", (done) => { - request.post(Parse.serverURL + "/hooks/functions", { - headers: { - "X-Parse-Application-Id": Parse.applicationId, - "X-Parse-Master-Key": Parse.masterKey, + it('should fail trying to create a malformed function (REST)', done => { + request.post( + Parse.serverURL + '/hooks/functions', + { + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, + body: JSON.stringify({ functionName: 'SomeFunction' }), }, - body: JSON.stringify({ functionName: "SomeFunction"}) - }, (err, res, body) => { - body = JSON.parse(body); - expect(body.error).toBe("invalid hook declaration"); - expect(body.code).toBe(143); - done(); - }) + (err, res, body) => { + body = JSON.parse(body); + expect(body.error).toBe('invalid hook declaration'); + expect(body.code).toBe(143); + done(); + } + ); }); - - it("should create hooks and properly preload them", (done) => { - + it('should create hooks and properly preload them', done => { const promises = []; - for (let i = 0; i < 5; i++) { - promises.push(Parse.Hooks.createTrigger("MyClass" + i, "beforeSave", "http://url.com/beforeSave/" + i)); - promises.push(Parse.Hooks.createFunction("AFunction" + i, "http://url.com/function" + i)); + for (let 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 + ) + ); } - Promise.all(promises).then(function(){ - for (let i = 0; i < 5; i++) { - // Delete everything from memory, as the server just started - triggers.removeTrigger("beforeSave", "MyClass" + i, Parse.applicationId); - triggers.removeFunction("AFunction" + i, Parse.applicationId); - expect(triggers.getTrigger("MyClass" + i, "beforeSave", Parse.applicationId)).toBeUndefined(); - expect(triggers.getFunction("AFunction" + i, Parse.applicationId)).toBeUndefined(); - } - const hooksController = new HooksController(Parse.applicationId, AppCache.get('test').databaseController); - return hooksController.load() - }, (err) => { - jfail(err); - fail('Should properly create all hooks'); - done(); - }).then(function() { - for (let i = 0; i < 5; i++) { - expect(triggers.getTrigger("MyClass" + i, "beforeSave", Parse.applicationId)).not.toBeUndefined(); - expect(triggers.getFunction("AFunction" + i, Parse.applicationId)).not.toBeUndefined(); - } - done(); - }, (err) => { - jfail(err); - fail('should properly load all hooks'); - done(); - }) + Promise.all(promises) + .then( + function() { + for (let i = 0; i < 5; i++) { + // Delete everything from memory, as the server just started + triggers.removeTrigger( + 'beforeSave', + 'MyClass' + i, + Parse.applicationId + ); + triggers.removeFunction('AFunction' + i, Parse.applicationId); + expect( + triggers.getTrigger( + 'MyClass' + i, + 'beforeSave', + Parse.applicationId + ) + ).toBeUndefined(); + expect( + triggers.getFunction('AFunction' + i, Parse.applicationId) + ).toBeUndefined(); + } + const hooksController = new HooksController( + Parse.applicationId, + AppCache.get('test').databaseController + ); + return hooksController.load(); + }, + err => { + jfail(err); + fail('Should properly create all hooks'); + done(); + } + ) + .then( + function() { + for (let i = 0; i < 5; i++) { + expect( + triggers.getTrigger( + 'MyClass' + i, + 'beforeSave', + Parse.applicationId + ) + ).not.toBeUndefined(); + expect( + triggers.getFunction('AFunction' + i, Parse.applicationId) + ).not.toBeUndefined(); + } + done(); + }, + err => { + jfail(err); + fail('should properly load all hooks'); + done(); + } + ); }); - it("should run the function on the test server", (done) => { - - app.post("/SomeFunction", function(req, res) { - res.json({success:"OK!"}); + it('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) => { - jfail(err); - fail("Should not fail creating a function"); - done(); - }).then(function(res){ - expect(res).toBe("OK!"); - done(); - }, (err) => { - jfail(err); - fail("Should not fail calling a function"); - done(); - }); + Parse.Hooks.createFunction( + 'SOME_TEST_FUNCTION', + hookServerURL + '/SomeFunction' + ) + .then( + function() { + return Parse.Cloud.run('SOME_TEST_FUNCTION'); + }, + err => { + jfail(err); + fail('Should not fail creating a function'); + done(); + } + ) + .then( + function(res) { + expect(res).toBe('OK!'); + done(); + }, + err => { + jfail(err); + fail('Should not fail calling a function'); + done(); + } + ); }); - it("should run the function on the test server (error handling)", (done) => { - - app.post("/SomeFunctionError", function(req, res) { - res.json({error: {code: 1337, error: "hacking that one!"}}); + it('should run the function on the test server (error handling)', done => { + app.post('/SomeFunctionError', function(req, res) { + res.json({ error: { code: 1337, error: 'hacking that one!' } }); }); // The function is deleted as the DB is dropped between calls - Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/SomeFunctionError").then(function(){ - return Parse.Cloud.run("SOME_TEST_FUNCTION") - }, (err) => { - jfail(err); - fail("Should not fail creating a function"); - done(); - }).then(function() { - fail("Should not succeed calling that function"); - done(); - }, (err) => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(141); - expect(err.message.code).toEqual(1337) - expect(err.message.error).toEqual("hacking that one!"); - } - done(); - }); + Parse.Hooks.createFunction( + 'SOME_TEST_FUNCTION', + hookServerURL + '/SomeFunctionError' + ) + .then( + function() { + return Parse.Cloud.run('SOME_TEST_FUNCTION'); + }, + err => { + jfail(err); + fail('Should not fail creating a function'); + done(); + } + ) + .then( + function() { + fail('Should not succeed calling that function'); + done(); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(141); + expect(err.message.code).toEqual(1337); + expect(err.message.error).toEqual('hacking that one!'); + } + done(); + } + ); }); - it("should provide X-Parse-Webhook-Key when defined", (done) => { - app.post("/ExpectingKey", function(req, res) { + it('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"}); + res.json({ success: 'correct key provided' }); } else { - res.json({error: "incorrect key provided"}); + res.json({ error: 'incorrect key provided' }); } }); - Parse.Hooks.createFunction("SOME_TEST_FUNCTION", hookServerURL + "/ExpectingKey").then(function(){ - return Parse.Cloud.run("SOME_TEST_FUNCTION") - }, (err) => { - jfail(err); - fail("Should not fail creating a function"); - done(); - }).then(function(res){ - expect(res).toBe("correct key provided"); - done(); - }, (err) => { - jfail(err); - fail("Should not fail calling a function"); - done(); - }); - }); - - it("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) => { + Parse.Hooks.createFunction( + 'SOME_TEST_FUNCTION', + hookServerURL + '/ExpectingKey' + ) + .then( + function() { + return Parse.Cloud.run('SOME_TEST_FUNCTION'); + }, + err => { jfail(err); - fail("Should not fail creating a function"); + fail('Should not fail creating a function'); done(); - }).then(function(){ - fail("Should not succeed calling that function"); + } + ) + .then( + function(res) { + expect(res).toBe('correct key provided'); done(); - }, (err) => { - expect(err).not.toBe(undefined); - expect(err).not.toBe(null); - if (err) { - expect(err.code).toBe(141); - expect(err.message).toEqual("incorrect key provided"); - } + }, + err => { + jfail(err); + fail('Should not fail calling a function'); done(); - }); - }); + } + ); }); + it('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 => { + jfail(err); + fail('Should not fail creating a function'); + done(); + } + ) + .then( + function() { + fail('Should not succeed calling that function'); + done(); + }, + err => { + expect(err).not.toBe(undefined); + expect(err).not.toBe(null); + if (err) { + expect(err.code).toBe(141); + expect(err.message).toEqual('incorrect key provided'); + } + done(); + } + ); + }); + }); - it("should run the beforeSave hook on the test server", (done) => { + it('should run the beforeSave hook on the test server', done => { let triggerCount = 0; - app.post("/BeforeSaveSome", function(req, res) { + app.post('/BeforeSaveSome', function(req, res) { triggerCount++; const object = req.body.object; - object.hello = "world"; + object.hello = 'world'; // Would need parse cloud express to set much more // But this should override the key upon return - res.json({success: object}); + res.json({ success: object }); }); // The function is deleted as the DB is dropped between calls - Parse.Hooks.createTrigger("SomeRandomObject", "beforeSave", hookServerURL + "/BeforeSaveSome").then(function () { - const obj = new Parse.Object("SomeRandomObject"); - return obj.save(); - }).then(function(res) { - expect(triggerCount).toBe(1); - return res.fetch(); - }).then(function(res) { - expect(res.get("hello")).toEqual("world"); - done(); - }).catch((err) => { - jfail(err); - fail("Should not fail creating a function"); - done(); - }); + Parse.Hooks.createTrigger( + 'SomeRandomObject', + 'beforeSave', + hookServerURL + '/BeforeSaveSome' + ) + .then(function() { + const obj = new Parse.Object('SomeRandomObject'); + return obj.save(); + }) + .then(function(res) { + expect(triggerCount).toBe(1); + return res.fetch(); + }) + .then(function(res) { + expect(res.get('hello')).toEqual('world'); + done(); + }) + .catch(err => { + jfail(err); + fail('Should not fail creating a function'); + done(); + }); }); - it("beforeSave hooks should correctly handle responses containing entire object", (done) => { - app.post("/BeforeSaveSome2", function(req, res) { + it('beforeSave hooks should correctly handle responses containing entire object', done => { + app.post('/BeforeSaveSome2', function(req, res) { const 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(); - }).catch((err) => { - fail(`Should not fail: ${JSON.stringify(err)}`); - done(); + 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(); + }) + .catch(err => { + fail(`Should not fail: ${JSON.stringify(err)}`); + done(); + }); }); - it("should run the afterSave hook on the test server", (done) => { + it('should run the afterSave hook on the test server', done => { let triggerCount = 0; let newObjectId; - app.post("/AfterSaveSome", function(req, res) { + app.post('/AfterSaveSome', function(req, res) { triggerCount++; - const obj = new Parse.Object("AnotherObject"); - obj.set("foo", "bar"); - obj.save().then(function(obj){ + const obj = new Parse.Object('AnotherObject'); + obj.set('foo', 'bar'); + obj.save().then(function(obj) { newObjectId = obj.id; - res.json({success: {}}); - }) + res.json({ success: {} }); + }); }); // The function is deleted as the DB is dropped between calls - Parse.Hooks.createTrigger("SomeRandomObject", "afterSave", hookServerURL + "/AfterSaveSome").then(function(){ - const obj = new Parse.Object("SomeRandomObject"); - return obj.save(); - }).then(function() { - return new Promise((resolve) => { - setTimeout(() => { - expect(triggerCount).toBe(1); - new Parse.Query("AnotherObject") - .get(newObjectId) - .then((r) => resolve(r)); - }, 500); + Parse.Hooks.createTrigger( + 'SomeRandomObject', + 'afterSave', + hookServerURL + '/AfterSaveSome' + ) + .then(function() { + const obj = new Parse.Object('SomeRandomObject'); + return obj.save(); + }) + .then(function() { + return new Promise(resolve => { + setTimeout(() => { + expect(triggerCount).toBe(1); + new Parse.Query('AnotherObject') + .get(newObjectId) + .then(r => resolve(r)); + }, 500); + }); + }) + .then(function(res) { + expect(res.get('foo')).toEqual('bar'); + done(); + }) + .catch(err => { + jfail(err); + fail('Should not fail creating a function'); + done(); }); - }).then(function(res){ - expect(res.get("foo")).toEqual("bar"); - done(); - }).catch((err) => { - jfail(err); - fail("Should not fail creating a function"); - done(); - }); }); }); @@ -512,16 +720,23 @@ describe('triggers', () => { const config = Config.get('test'); const master = auth.master(config); const context = { - originalKey: 'original' + originalKey: 'original', }; - const req = triggers.getRequestObject(triggers.Types.beforeSave, master, {}, {}, config, context); + const req = triggers.getRequestObject( + triggers.Types.beforeSave, + master, + {}, + {}, + config, + context + ); expect(req.context.originalKey).toBe('original'); req.context = { - key: 'value' + key: 'value', }; expect(context.key).toBe(undefined); req.context = { - key: 'newValue' + key: 'newValue', }; expect(context.key).toBe(undefined); }); @@ -530,7 +745,14 @@ describe('triggers', () => { const config = Config.get('test'); const master = auth.master(config); const context = {}; - const req = triggers.getRequestObject(triggers.Types.afterSave, master, {}, {}, config, context); + const req = triggers.getRequestObject( + triggers.Types.afterSave, + master, + {}, + {}, + config, + context + ); expect(req.context).not.toBeUndefined(); }); @@ -538,7 +760,14 @@ describe('triggers', () => { const config = Config.get('test'); const master = auth.master(config); const context = {}; - const req = triggers.getRequestObject(triggers.Types.beforeFind, master, {}, {}, config, context); + const req = triggers.getRequestObject( + triggers.Types.beforeFind, + master, + {}, + {}, + config, + context + ); expect(req.context).toBeUndefined(); }); }); diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 853262087f..e85d980374 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -6,89 +6,123 @@ const auth = require('../lib/Auth'); const Config = require('../lib/Config'); const Parse = require('parse/node').Parse; const rest = require('../lib/rest'); -const request = require("request"); +const request = require('request'); let config; let database; -const defaultColumns = require('../lib/Controllers/SchemaController').defaultColumns; +const defaultColumns = require('../lib/Controllers/SchemaController') + .defaultColumns; const delay = function delay(delay) { return new Promise(resolve => setTimeout(resolve, delay)); -} +}; -const installationSchema = { fields: Object.assign({}, defaultColumns._Default, defaultColumns._Installation) }; +const installationSchema = { + fields: Object.assign( + {}, + defaultColumns._Default, + defaultColumns._Installation + ), +}; describe('Installations', () => { - beforeEach(() => { config = Config.get('test'); database = config.database; }); - it('creates an android installation with ids', (done) => { + it('creates an android installation with ids', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { - 'installationId': installId, - 'deviceType': device + installationId: installId, + deviceType: device, }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); const obj = results[0]; expect(obj.installationId).toEqual(installId); expect(obj.deviceType).toEqual(device); done(); - }).catch((error) => { console.log(error); jfail(error); done(); }); + }) + .catch(error => { + console.log(error); + jfail(error); + done(); + }); }); - it('creates an ios installation with ids', (done) => { - const t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + it('creates an ios installation with ids', done => { + const t = + '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; const device = 'ios'; const input = { - 'deviceToken': t, - 'deviceType': device + deviceToken: t, + deviceType: device, }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); const obj = results[0]; expect(obj.deviceToken).toEqual(t); expect(obj.deviceType).toEqual(device); done(); - }).catch((error) => { console.log(error); jfail(error); done(); }); + }) + .catch(error => { + console.log(error); + jfail(error); + done(); + }); }); - it('creates an embedded installation with ids', (done) => { + it('creates an embedded installation with ids', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'embedded'; const input = { - 'installationId': installId, - 'deviceType': device + installationId: installId, + deviceType: device, }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); const obj = results[0]; expect(obj.installationId).toEqual(installId); expect(obj.deviceType).toEqual(device); done(); - }).catch((error) => { console.log(error); jfail(error); done(); }); + }) + .catch(error => { + console.log(error); + jfail(error); + done(); + }); }); - it('creates an android installation with all fields', (done) => { + it('creates an android installation with all fields', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { - 'installationId': installId, - 'deviceType': device, - 'channels': ['foo', 'bar'] + installationId: installId, + deviceType: device, + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); const obj = results[0]; @@ -99,19 +133,28 @@ describe('Installations', () => { expect(obj.channels[0]).toEqual('foo'); expect(obj.channels[1]).toEqual('bar'); done(); - }).catch((error) => { console.log(error); jfail(error); done(); }); + }) + .catch(error => { + console.log(error); + jfail(error); + done(); + }); }); - it('creates an ios installation with all fields', (done) => { - const t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + it('creates an ios installation with all fields', done => { + const t = + '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; const device = 'ios'; const input = { - 'deviceToken': t, - 'deviceType': device, - 'channels': ['foo', 'bar'] + deviceToken: t, + deviceType: device, + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); const obj = results[0]; @@ -122,117 +165,145 @@ describe('Installations', () => { expect(obj.channels[0]).toEqual('foo'); expect(obj.channels[1]).toEqual('bar'); done(); - }).catch((error) => { console.log(error); jfail(error); done(); }); + }) + .catch(error => { + console.log(error); + jfail(error); + done(); + }); }); - it('should properly fail queying installations', (done) => { + it('should properly fail queying installations', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { - 'installationId': installId, - 'deviceType': device + installationId: installId, + deviceType: device, }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { const query = new Parse.Query(Parse.Installation); - return query.find() - }).then(() => { + return query.find(); + }) + .then(() => { fail('Should not succeed!'); done(); - }).catch((error) => { + }) + .catch(error => { expect(error.code).toBe(119); - expect(error.message).toBe('Clients aren\'t allowed to perform the find operation on the installation collection.') + expect(error.message).toBe( + "Clients aren't allowed to perform the find operation on the installation collection." + ); done(); }); }); - it('should properly queying installations with masterKey', (done) => { + it('should properly queying installations with masterKey', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { - 'installationId': installId, - 'deviceType': device + installationId: installId, + deviceType: device, }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { const query = new Parse.Query(Parse.Installation); - return query.find({useMasterKey: true}); - }).then((results) => { + return query.find({ useMasterKey: true }); + }) + .then(results => { expect(results.length).toEqual(1); const obj = results[0].toJSON(); expect(obj.installationId).toEqual(installId); expect(obj.deviceType).toEqual(device); done(); - }).catch(() => { + }) + .catch(() => { fail('Should not fail'); done(); }); }); - it('fails with missing ids', (done) => { + it('fails with missing ids', done => { const input = { - 'deviceType': 'android', - 'channels': ['foo', 'bar'] + deviceType: 'android', + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { fail('Should not have been able to create an Installation.'); done(); - }).catch((error) => { + }) + .catch(error => { expect(error.code).toEqual(135); done(); }); }); - it('fails for android with missing type', (done) => { + it('fails for android with missing type', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const input = { - 'installationId': installId, - 'channels': ['foo', 'bar'] + installationId: installId, + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { fail('Should not have been able to create an Installation.'); done(); - }).catch((error) => { + }) + .catch(error => { expect(error.code).toEqual(135); done(); }); }); - it('creates an object with custom fields', (done) => { - const t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + it('creates an object with custom fields', done => { + const t = + '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; const input = { - 'deviceToken': t, - 'deviceType': 'ios', - 'channels': ['foo', 'bar'], - 'custom': 'allowed' + deviceToken: t, + deviceType: 'ios', + channels: ['foo', 'bar'], + custom: 'allowed', }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); const obj = results[0]; expect(obj.custom).toEqual('allowed'); done(); - }).catch((error) => { console.log(error); }); + }) + .catch(error => { + console.log(error); + }); }); // Note: did not port test 'TestObjectIDForIdentifiers' - it('merging when installationId already exists', (done) => { + it('merging when installationId already exists', done => { const installId1 = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + const t = + '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; const input = { - 'deviceToken': t, - 'deviceType': 'ios', - 'installationId': installId1, - 'channels': ['foo', 'bar'] + deviceToken: t, + deviceType: 'ios', + installationId: installId1, + channels: ['foo', 'bar'], }; let firstObject; let secondObject; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); firstObject = results[0]; @@ -241,7 +312,9 @@ describe('Installations', () => { input['foo'] = 'bar'; return rest.create(config, auth.nobody(config), '_Installation', input); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); secondObject = results[0]; @@ -249,35 +322,49 @@ describe('Installations', () => { expect(secondObject.channels.length).toEqual(2); expect(secondObject.foo).toEqual('bar'); done(); - }).catch((error) => { console.log(error); }); + }) + .catch(error => { + console.log(error); + }); }); - it('merging when two objects both only have one id', (done) => { + it('merging when two objects both only have one id', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; const input1 = { - 'installationId': installId, - 'deviceType': 'ios' + installationId: installId, + deviceType: 'ios', }; const input2 = { - 'deviceToken': t, - 'deviceType': 'ios' + deviceToken: t, + deviceType: 'ios', }; const input3 = { - 'deviceToken': t, - 'installationId': installId, - 'deviceType': 'ios' + deviceToken: t, + installationId: installId, + deviceType: 'ios', }; let firstObject; let secondObject; - rest.create(config, auth.nobody(config), '_Installation', input1) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input1) + .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); + return rest.create( + config, + auth.nobody(config), + '_Installation', + input2 + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(2); if (results[0]['_id'] == firstObject._id) { @@ -285,196 +372,301 @@ describe('Installations', () => { } else { secondObject = results[0]; } - return rest.create(config, auth.nobody(config), '_Installation', input3); + return rest.create( + config, + auth.nobody(config), + '_Installation', + input3 + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); expect(results[0]['_id']).toEqual(secondObject._id); done(); - }).catch((error) => { + }) + .catch(error => { jfail(error); done(); }); }); - xit('creating multiple devices with same device token works', (done) => { + xit('creating multiple devices with same device token works', done => { const installId1 = '11111111-abcd-abcd-abcd-123456789abc'; const installId2 = '22222222-abcd-abcd-abcd-123456789abc'; const installId3 = '33333333-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; const input = { - 'installationId': installId1, - 'deviceType': 'ios', - 'deviceToken': t + installationId: installId1, + deviceType: 'ios', + deviceToken: t, }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { input.installationId = installId2; return rest.create(config, auth.nobody(config), '_Installation', input); - }).then(() => { + }) + .then(() => { input.installationId = installId3; return rest.create(config, auth.nobody(config), '_Installation', input); }) - .then(() => database.adapter.find('_Installation', {installationId: installId1}, installationSchema, {})) + .then(() => + database.adapter.find( + '_Installation', + { installationId: installId1 }, + installationSchema, + {} + ) + ) .then(results => { expect(results.length).toEqual(1); - return database.adapter.find('_Installation', {installationId: installId2}, installationSchema, {}); - }).then(results => { + return database.adapter.find( + '_Installation', + { installationId: installId2 }, + installationSchema, + {} + ); + }) + .then(results => { expect(results.length).toEqual(1); - return database.adapter.find('_Installation', {installationId: installId3}, installationSchema, {}); - }).then((results) => { + return database.adapter.find( + '_Installation', + { installationId: installId3 }, + installationSchema, + {} + ); + }) + .then(results => { expect(results.length).toEqual(1); done(); - }).catch((error) => { console.log(error); }); + }) + .catch(error => { + console.log(error); + }); }); - it('updating with new channels', (done) => { + it('updating with new channels', done => { const input = { installationId: '12345678-abcd-abcd-abcd-123456789abc', deviceType: 'android', - channels: ['foo', 'bar'] + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); const objectId = results[0].objectId; const update = { - 'channels': ['baz'] + channels: ['baz'], }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId }, update); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId }, + update + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .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 => { + }) + .catch(error => { jfail(error); done(); }); }); - it('update android fails with new installation id', (done) => { + it('update android fails with new installation id', done => { const installId1 = '12345678-abcd-abcd-abcd-123456789abc'; const installId2 = '87654321-abcd-abcd-abcd-123456789abc'; let input = { - 'installationId': installId1, - 'deviceType': 'android', - 'channels': ['foo', 'bar'] + installationId: installId1, + deviceType: 'android', + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); - input = { 'installationId': installId2 }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: results[0].objectId }, input); - }).then(() => { + input = { installationId: installId2 }; + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + input + ); + }) + .then(() => { fail('Updating the installation should have failed.'); done(); - }).catch((error) => { + }) + .catch(error => { expect(error.code).toEqual(136); done(); }); }); - it('update ios fails with new deviceToken and no installationId', (done) => { - const a = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; - const b = '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + it('update ios fails with new deviceToken and no installationId', done => { + const a = + '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + const b = + '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; let input = { - 'deviceToken': a, - 'deviceType': 'ios', - 'channels': ['foo', 'bar'] + deviceToken: a, + deviceType: 'ios', + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); - input = { 'deviceToken': b }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: results[0].objectId }, input); - }).then(() => { + input = { deviceToken: b }; + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + input + ); + }) + .then(() => { fail('Updating the installation should have failed.'); - }).catch((error) => { + }) + .catch(error => { expect(error.code).toEqual(136); done(); }); }); - it('update ios updates device token', (done) => { + it('update ios updates device token', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; - const u = '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + const t = + '11433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; + const u = + '91433856eed2f1285fb3aa11136718c1198ed5647875096952c66bf8cb976306'; let input = { - 'installationId': installId, - 'deviceType': 'ios', - 'deviceToken': t, - 'channels': ['foo', 'bar'] + installationId: installId, + deviceType: 'ios', + deviceToken: t, + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); input = { - 'installationId': installId, - 'deviceToken': u, - 'deviceType': 'ios' + installationId: installId, + deviceToken: u, + deviceType: 'ios', }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: results[0].objectId }, input); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + input + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); expect(results[0].deviceToken).toEqual(u); done(); - }).catch(err => { + }) + .catch(err => { jfail(err); done(); - }) + }); }); - it('update fails to change deviceType', (done) => { + it('update fails to change deviceType', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; let input = { - 'installationId': installId, - 'deviceType': 'android', - 'channels': ['foo', 'bar'] + installationId: installId, + deviceType: 'android', + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); input = { - 'deviceType': 'ios' + deviceType: 'ios', }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: results[0].objectId }, input); - }).then(() => { + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + input + ); + }) + .then(() => { fail('Should not have been able to update Installation.'); done(); - }).catch((error) => { + }) + .catch(error => { expect(error.code).toEqual(136); done(); }); }); - it('update android with custom field', (done) => { + it('update android with custom field', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; let input = { - 'installationId': installId, - 'deviceType': 'android', - 'channels': ['foo', 'bar'] + installationId: installId, + deviceType: 'android', + channels: ['foo', 'bar'], }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); input = { - 'custom': 'allowed' + custom: 'allowed', }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: results[0].objectId }, input); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + input + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); expect(results[0]['custom']).toEqual('allowed'); @@ -482,226 +674,338 @@ describe('Installations', () => { }); }); - it('update android device token with duplicate device token', (done) => { + it('update android device token with duplicate device token', done => { const installId1 = '11111111-abcd-abcd-abcd-123456789abc'; const installId2 = '22222222-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; let input = { - 'installationId': installId1, - 'deviceToken': t, - 'deviceType': 'android' + installationId: installId1, + deviceToken: t, + deviceType: 'android', }; let firstObject; let secondObject; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { input = { - 'installationId': installId2, - 'deviceType': 'android' + installationId: installId2, + deviceType: 'android', }; return rest.create(config, auth.nobody(config), '_Installation', input); }) - .then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {})) + .then(() => + database.adapter.find( + '_Installation', + installationSchema, + { installationId: installId1 }, + {} + ) + ) .then(results => { firstObject = results[0]; expect(results.length).toEqual(1); - return database.adapter.find('_Installation', installationSchema, {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 input = { - 'objectId': secondObject.objectId, - 'deviceToken': t + objectId: secondObject.objectId, + deviceToken: t, }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: secondObject.objectId }, input); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: secondObject.objectId }, + input + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {})) + .then(() => + database.adapter.find( + '_Installation', + installationSchema, + { objectId: firstObject.objectId }, + {} + ) + ) .then(results => { - // The first object should have been deleted + // The first object should have been deleted expect(results.length).toEqual(0); done(); - }).catch(error => { + }) + .catch(error => { jfail(error); done(); }); }); - it('update ios device token with duplicate device token', (done) => { + it('update ios device token with duplicate device token', done => { const installId1 = '11111111-abcd-abcd-abcd-123456789abc'; const installId2 = '22222222-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; let input = { - 'installationId': installId1, - 'deviceToken': t, - 'deviceType': 'ios' + installationId: installId1, + deviceToken: t, + deviceType: 'ios', }; let firstObject; let secondObject; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { input = { - 'installationId': installId2, - 'deviceType': 'ios' + installationId: installId2, + deviceType: 'ios', }; return rest.create(config, auth.nobody(config), '_Installation', input); }) .then(() => delay(100)) - .then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId1}, {})) - .then((results) => { + .then(() => + database.adapter.find( + '_Installation', + installationSchema, + { installationId: installId1 }, + {} + ) + ) + .then(results => { expect(results.length).toEqual(1); firstObject = results[0]; }) .then(() => delay(100)) - .then(() => database.adapter.find('_Installation', installationSchema, {installationId: installId2}, {})) + .then(() => + 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 input = { - 'installationId': installId2, - 'deviceToken': t + installationId: installId2, + deviceToken: t, }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: secondObject.objectId }, input); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: secondObject.objectId }, + input + ); }) .then(() => delay(100)) - .then(() => database.adapter.find('_Installation', installationSchema, {objectId: firstObject.objectId}, {})) + .then(() => + database.adapter.find( + '_Installation', + installationSchema, + { objectId: firstObject.objectId }, + {} + ) + ) .then(results => { - // The first object should have been deleted + // The first object should have been deleted expect(results.length).toEqual(0); done(); - }).catch(error => { + }) + .catch(error => { jfail(error); done(); }); }); - xit('update ios device token with duplicate token different app', (done) => { + xit('update ios device token with duplicate token different app', done => { const installId1 = '11111111-abcd-abcd-abcd-123456789abc'; const installId2 = '22222222-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; const input = { - 'installationId': installId1, - 'deviceToken': t, - 'deviceType': 'ios', - 'appIdentifier': 'foo' + installationId: installId1, + deviceToken: t, + deviceType: 'ios', + appIdentifier: 'foo', }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { input.installationId = installId2; input.appIdentifier = 'bar'; return rest.create(config, auth.nobody(config), '_Installation', input); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { - // The first object should have been deleted during merge + // The first object should have been deleted during merge expect(results.length).toEqual(1); expect(results[0].installationId).toEqual(installId2); done(); - }).catch(error => { + }) + .catch(error => { jfail(error); done(); }); }); - it('update ios token and channels', (done) => { + it('update ios token and channels', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; let input = { - 'installationId': installId, - 'deviceType': 'ios' + installationId: installId, + deviceType: 'ios', }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); input = { - 'deviceToken': t, - 'channels': [] + deviceToken: t, + channels: [], }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: results[0].objectId }, input); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + input + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .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); expect(results[0].channels.length).toEqual(0); done(); - }).catch(error => { + }) + .catch(error => { jfail(error); done(); }); }); - it('update ios linking two existing objects', (done) => { + it('update ios linking two existing objects', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; let input = { - 'installationId': installId, - 'deviceType': 'ios' + installationId: installId, + deviceType: 'ios', }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { input = { - 'deviceToken': t, - 'deviceType': 'ios' + deviceToken: t, + deviceType: 'ios', }; return rest.create(config, auth.nobody(config), '_Installation', input); }) - .then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {})) + .then(() => + database.adapter.find( + '_Installation', + installationSchema, + { deviceToken: t }, + {} + ) + ) .then(results => { expect(results.length).toEqual(1); input = { - 'deviceToken': t, - 'installationId': installId, - 'deviceType': 'ios' + deviceToken: t, + installationId: installId, + deviceType: 'ios', }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: results[0].objectId }, input); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + input + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .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); expect(results[0].deviceType).toEqual('ios'); done(); - }).catch(error => { + }) + .catch(error => { jfail(error); done(); }); }); - it('update is linking two existing objects w/ increment', (done) => { + it('update is linking two existing objects w/ increment', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; let input = { - 'installationId': installId, - 'deviceType': 'ios' + installationId: installId, + deviceType: 'ios', }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { input = { - 'deviceToken': t, - 'deviceType': 'ios' + deviceToken: t, + deviceType: 'ios', }; return rest.create(config, auth.nobody(config), '_Installation', input); }) - .then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {})) + .then(() => + database.adapter.find( + '_Installation', + installationSchema, + { deviceToken: t }, + {} + ) + ) .then(results => { expect(results.length).toEqual(1); input = { - 'deviceToken': t, - 'installationId': installId, - 'deviceType': 'ios', - 'score': { - '__op': 'Increment', - 'amount': 1 - } + deviceToken: t, + installationId: installId, + deviceType: 'ios', + score: { + __op: 'Increment', + amount: 1, + }, }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: results[0].objectId }, input); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: results[0].objectId }, + input + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); expect(results[0].installationId).toEqual(installId); @@ -709,104 +1013,155 @@ describe('Installations', () => { expect(results[0].deviceType).toEqual('ios'); expect(results[0].score).toEqual(1); done(); - }).catch(error => { + }) + .catch(error => { jfail(error); done(); }); }); - it('update is linking two existing with installation id', (done) => { + it('update is linking two existing with installation id', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; let input = { - 'installationId': installId, - 'deviceType': 'ios' + installationId: installId, + deviceType: 'ios', }; let installObj; let tokenObj; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); installObj = results[0]; input = { - 'deviceToken': t, - 'deviceType': 'ios' + deviceToken: t, + deviceType: 'ios', }; return rest.create(config, auth.nobody(config), '_Installation', input); }) - .then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {})) + .then(() => + database.adapter.find( + '_Installation', + installationSchema, + { deviceToken: t }, + {} + ) + ) .then(results => { expect(results.length).toEqual(1); tokenObj = results[0]; input = { - 'installationId': installId, - 'deviceToken': t, - 'deviceType': 'ios' + installationId: installId, + deviceToken: t, + deviceType: 'ios', }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: installObj.objectId }, input); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: installObj.objectId }, + input + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, { objectId: tokenObj.objectId }, {})) + .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); done(); - }).catch(error => { + }) + .catch(error => { jfail(error); done(); }); }); - it('update is linking two existing with installation id w/ op', (done) => { + it('update is linking two existing with installation id w/ op', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; let input = { - 'installationId': installId, - 'deviceType': 'ios' + installationId: installId, + deviceType: 'ios', }; let installObj; let tokenObj; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); installObj = results[0]; input = { - 'deviceToken': t, - 'deviceType': 'ios' + deviceToken: t, + deviceType: 'ios', }; return rest.create(config, auth.nobody(config), '_Installation', input); }) - .then(() => database.adapter.find('_Installation', installationSchema, { deviceToken: t }, {})) + .then(() => + database.adapter.find( + '_Installation', + installationSchema, + { deviceToken: t }, + {} + ) + ) .then(results => { expect(results.length).toEqual(1); tokenObj = results[0]; input = { - 'installationId': installId, - 'deviceToken': t, - 'deviceType': 'ios', - 'score': { - '__op': 'Increment', - 'amount': 1 - } + installationId: installId, + deviceToken: t, + deviceType: 'ios', + score: { + __op: 'Increment', + amount: 1, + }, }; - return rest.update(config, auth.nobody(config), '_Installation', { objectId: installObj.objectId }, input); + return rest.update( + config, + auth.nobody(config), + '_Installation', + { objectId: installObj.objectId }, + input + ); }) - .then(() => database.adapter.find('_Installation', installationSchema, { objectId: tokenObj.objectId }, {})) + .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); expect(results[0].score).toEqual(1); done(); - }).catch(error => { + }) + .catch(error => { jfail(error); done(); }); }); - it('ios merge existing same token no installation id', (done) => { + it('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 @@ -817,24 +1172,30 @@ describe('Installations', () => { // imported installation, then we should reuse the existing installation // object in case the developer already added additional fields via Data // Browser or REST API (e.g. channel targeting info). - const t = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; + const t = + '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; const installId = '12345678-abcd-abcd-abcd-123456789abc'; let input = { - 'deviceToken': t, - 'deviceType': 'ios' + deviceToken: t, + deviceType: 'ios', }; - rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); input = { - 'installationId': installId, - 'deviceToken': t, - 'deviceType': 'ios' + installationId: installId, + deviceToken: t, + deviceType: 'ios', }; return rest.create(config, auth.nobody(config), '_Installation', input); }) - .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => + database.adapter.find('_Installation', installationSchema, {}, {}) + ) .then(results => { expect(results.length).toEqual(1); expect(results[0].deviceToken).toEqual(t); @@ -852,23 +1213,29 @@ describe('Installations', () => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { - 'installationId': installId, - 'deviceType': device + installationId: installId, + deviceType: device, }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(createResult => { const headers = { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', + '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(); - }); + 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); @@ -881,28 +1248,32 @@ describe('Installations', () => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { - 'installationId': installId, - 'deviceType': device + installationId: installId, + deviceType: device, }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(() => { const headers = { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Installation-Id': installId + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Installation-Id': installId, }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/classes/_Installation', - json: true, - body: { - date: new Date() + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/classes/_Installation', + json: true, + body: { + date: new Date(), + }, + }, + (error, response, body) => { + expect(response.statusCode).toBe(200); + expect(body.updatedAt).not.toBeUndefined(); + done(); } - }, (error, response, body) => { - expect(response.statusCode).toBe(200); - expect(body.updatedAt).not.toBeUndefined(); - done(); - }); + ); }) .catch(error => { console.log(error); @@ -915,19 +1286,24 @@ describe('Installations', () => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { - 'installationId': installId, - 'deviceType': device + installationId: installId, + deviceType: device, }; - rest.create(config, auth.nobody(config), '_Installation', input) + rest + .create(config, auth.nobody(config), '_Installation', input) .then(createResult => { - const installationObj = Parse.Installation.createWithoutData(createResult.response.objectId); + const installationObj = Parse.Installation.createWithoutData( + createResult.response.objectId + ); installationObj.set('customField', 'custom value'); - return installationObj.save(null, {useMasterKey: true}); - }).then(updateResult => { + return installationObj.save(null, { useMasterKey: true }); + }) + .then(updateResult => { expect(updateResult).not.toBeUndefined(); expect(updateResult.get('customField')).toEqual('custom value'); done(); - }).catch(error => { + }) + .catch(error => { console.log(error); fail('failed'); done(); @@ -938,49 +1314,73 @@ describe('Installations', () => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { - 'installationId': installId, - 'deviceType': device + installationId: installId, + deviceType: device, }; - rest.create(config, auth.nobody(config), '_Installation', input).then(() => { - const query = new Parse.Query(Parse.Installation); - query.equalTo('installationId', installId); - query.first({useMasterKey: true}).then((installation) => { - return installation.save({ - key: 'value' - }, {useMasterKey: true}); - }).then(() => { - done(); - }, (err) => { - jfail(err) - done(); + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => { + const query = new Parse.Query(Parse.Installation); + query.equalTo('installationId', installId); + query + .first({ useMasterKey: true }) + .then(installation => { + return installation.save( + { + key: 'value', + }, + { useMasterKey: true } + ); + }) + .then( + () => { + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - }); }); it('should properly reject updating installationId', done => { const installId = '12345678-abcd-abcd-abcd-123456789abc'; const device = 'android'; const input = { - 'installationId': installId, - 'deviceType': device - }; - rest.create(config, auth.nobody(config), '_Installation', input).then(() => { - const query = new Parse.Query(Parse.Installation); - query.equalTo('installationId', installId); - query.first({useMasterKey: true}).then((installation) => { - return installation.save({ - key: 'value', - installationId: '22222222-abcd-abcd-abcd-123456789abc' - }, {useMasterKey: true}); - }).then(() => { - fail('should not succeed'); - done(); - }, (err) => { - expect(err.code).toBe(136); - expect(err.message).toBe('installationId may not be changed in this operation'); - done(); - }); - }); + installationId: installId, + deviceType: device, + }; + rest + .create(config, auth.nobody(config), '_Installation', input) + .then(() => { + const query = new Parse.Query(Parse.Installation); + query.equalTo('installationId', installId); + query + .first({ useMasterKey: true }) + .then(installation => { + return installation.save( + { + key: 'value', + installationId: '22222222-abcd-abcd-abcd-123456789abc', + }, + { useMasterKey: true } + ); + }) + .then( + () => { + fail('should not succeed'); + done(); + }, + err => { + expect(err.code).toBe(136); + expect(err.message).toBe( + 'installationId may not be changed in this operation' + ); + done(); + } + ); + }); }); // TODO: Look at additional tests from installation_collection_test.go:882 diff --git a/spec/ParseLiveQueryServer.spec.js b/spec/ParseLiveQueryServer.spec.js index de318ecfa3..38cfd9028a 100644 --- a/spec/ParseLiveQueryServer.spec.js +++ b/spec/ParseLiveQueryServer.spec.js @@ -1,5 +1,6 @@ const Parse = require('parse/node'); -const ParseLiveQueryServer = require('../lib/LiveQuery/ParseLiveQueryServer').ParseLiveQueryServer; +const ParseLiveQueryServer = require('../lib/LiveQuery/ParseLiveQueryServer') + .ParseLiveQueryServer; const ParseServer = require('../lib/ParseServer').default; // Global mock info @@ -11,7 +12,11 @@ describe('ParseLiveQueryServer', function() { beforeEach(function(done) { // Mock ParseWebSocketServer const mockParseWebSocketServer = jasmine.createSpy('ParseWebSocketServer'); - jasmine.mockLibrary('../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer', mockParseWebSocketServer); + jasmine.mockLibrary( + '../lib/LiveQuery/ParseWebSocketServer', + 'ParseWebSocketServer', + mockParseWebSocketServer + ); // Mock Client const mockClient = function(id, socket, hasMasterKey) { this.pushConnect = jasmine.createSpy('pushConnect'); @@ -26,40 +31,62 @@ describe('ParseLiveQueryServer', function() { this.getSubscriptionInfo = jasmine.createSpy('getSubscriptionInfo'); this.deleteSubscriptionInfo = jasmine.createSpy('deleteSubscriptionInfo'); this.hasMasterKey = hasMasterKey; - } + }; mockClient.pushError = jasmine.createSpy('pushError'); jasmine.mockLibrary('../lib/LiveQuery/Client', 'Client', mockClient); // Mock Subscription const mockSubscriotion = function() { this.addClientSubscription = jasmine.createSpy('addClientSubscription'); - this.deleteClientSubscription = jasmine.createSpy('deleteClientSubscription'); - } - jasmine.mockLibrary('../lib/LiveQuery/Subscription', 'Subscription', mockSubscriotion); + this.deleteClientSubscription = jasmine.createSpy( + 'deleteClientSubscription' + ); + }; + jasmine.mockLibrary( + '../lib/LiveQuery/Subscription', + 'Subscription', + mockSubscriotion + ); // Mock queryHash - const mockQueryHash = jasmine.createSpy('matchesQuery').and.returnValue(queryHashValue); - jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'queryHash', mockQueryHash); + const mockQueryHash = jasmine + .createSpy('matchesQuery') + .and.returnValue(queryHashValue); + jasmine.mockLibrary( + '../lib/LiveQuery/QueryTools', + 'queryHash', + mockQueryHash + ); // Mock matchesQuery - const mockMatchesQuery = jasmine.createSpy('matchesQuery').and.returnValue(true); - jasmine.mockLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery', mockMatchesQuery); + const mockMatchesQuery = jasmine + .createSpy('matchesQuery') + .and.returnValue(true); + jasmine.mockLibrary( + '../lib/LiveQuery/QueryTools', + 'matchesQuery', + mockMatchesQuery + ); // Mock ParsePubSub const mockParsePubSub = { createPublisher: function() { return { publish: jasmine.createSpy('publish'), - on: jasmine.createSpy('on') - } + on: jasmine.createSpy('on'), + }; }, createSubscriber: function() { return { subscribe: jasmine.createSpy('subscribe'), - on: jasmine.createSpy('on') - } - } + on: jasmine.createSpy('on'), + }; + }, }; - jasmine.mockLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub', mockParsePubSub); + jasmine.mockLibrary( + '../lib/LiveQuery/ParsePubSub', + 'ParsePubSub', + mockParsePubSub + ); // Make mock SessionTokenCache - const mockSessionTokenCache = function(){ - this.getUserId = function(sessionToken){ + const mockSessionTokenCache = function() { + this.getUserId = function(sessionToken) { if (typeof sessionToken === 'undefined') { return Promise.resolve(undefined); } @@ -69,7 +96,11 @@ describe('ParseLiveQueryServer', function() { return Promise.resolve(testUserId); }; }; - jasmine.mockLibrary('../lib/LiveQuery/SessionTokenCache', 'SessionTokenCache', mockSessionTokenCache); + jasmine.mockLibrary( + '../lib/LiveQuery/SessionTokenCache', + 'SessionTokenCache', + mockSessionTokenCache + ); done(); }); @@ -84,7 +115,10 @@ describe('ParseLiveQueryServer', function() { it('can be initialized from ParseServer', function() { const httpServer = {}; - const parseLiveQueryServer = ParseServer.createLiveQueryServer(httpServer, {}); + const parseLiveQueryServer = ParseServer.createLiveQueryServer( + httpServer, + {} + ); expect(parseLiveQueryServer.clientId).toBeUndefined(); expect(parseLiveQueryServer.clients.size).toBe(0); @@ -93,7 +127,7 @@ describe('ParseLiveQueryServer', function() { it('can be initialized from ParseServer without httpServer', function(done) { const parseLiveQueryServer = ParseServer.createLiveQueryServer(undefined, { - port: 22345 + port: 22345, }); expect(parseLiveQueryServer.clientId).toBeUndefined(); @@ -111,9 +145,9 @@ describe('ParseLiveQueryServer', function() { mountPath: '/1', serverURL: 'http://localhost:12345/1', liveQuery: { - classNames: ['Yolo'] + classNames: ['Yolo'], }, - startLiveQueryServer: true + startLiveQueryServer: true, }); expect(parseServer.liveQueryServer).not.toBeUndefined(); @@ -129,11 +163,11 @@ describe('ParseLiveQueryServer', function() { mountPath: '/1', serverURL: 'http://localhost:12345/1', liveQuery: { - classNames: ['Yolo'] + classNames: ['Yolo'], }, liveQueryServerOptions: { port: 22347, - } + }, }); expect(parseServer.liveQueryServer).not.toBeUndefined(); @@ -146,9 +180,11 @@ describe('ParseLiveQueryServer', function() { it('can handle connect command', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const parseWebSocket = { - clientId: -1 + clientId: -1, }; - parseLiveQueryServer._validateKeys = jasmine.createSpy('validateKeys').and.returnValue(true); + parseLiveQueryServer._validateKeys = jasmine + .createSpy('validateKeys') + .and.returnValue(true); parseLiveQueryServer._handleConnect(parseWebSocket); const clientKeys = parseLiveQueryServer.clients.keys(); @@ -163,8 +199,7 @@ describe('ParseLiveQueryServer', function() { it('can handle subscribe command without clientId', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); - const incompleteParseConn = { - }; + const incompleteParseConn = {}; parseLiveQueryServer._handleSubscribe(incompleteParseConn, {}); const Client = require('../lib/LiveQuery/Client').Client; @@ -178,21 +213,21 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Handle mock subscription const parseWebSocket = { - clientId: clientId + clientId: clientId, }; const query = { className: 'test', where: { - key: 'value' + key: 'value', }, - fields: [ 'test' ] - } + fields: ['test'], + }; const requestId = 2; const request = { query: query, requestId: requestId, - sessionToken: 'sessionToken' - } + sessionToken: 'sessionToken', + }; parseLiveQueryServer._handleSubscribe(parseWebSocket, request); // Make sure we add the subscription to the server @@ -205,7 +240,10 @@ describe('ParseLiveQueryServer', function() { // TODO(check subscription constructor to verify we pass the right argument) // Make sure we add clientInfo to the subscription const subscription = classSubscriptions.get('hash'); - expect(subscription.addClientSubscription).toHaveBeenCalledWith(clientId, requestId); + expect(subscription.addClientSubscription).toHaveBeenCalledWith( + clientId, + requestId + ); // Make sure we add subscriptionInfo to the client const args = client.addSubscriptionInfo.calls.first().args; expect(args[0]).toBe(requestId); @@ -224,30 +262,42 @@ describe('ParseLiveQueryServer', function() { const clientAgain = addMockClient(parseLiveQueryServer, clientIdAgain); // Add subscription for mock client 1 const parseWebSocket = { - clientId: clientId + clientId: clientId, }; const requestId = 2; const query = { className: 'test', where: { - key: 'value' + key: 'value', }, - fields: [ 'test' ] - } - addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query); + fields: ['test'], + }; + addMockSubscription( + parseLiveQueryServer, + clientId, + requestId, + parseWebSocket, + query + ); // Add subscription for mock client 2 const parseWebSocketAgain = { - clientId: clientIdAgain + clientId: clientIdAgain, }; const queryAgain = { className: 'test', where: { - key: 'value' + key: 'value', }, - fields: [ 'testAgain' ] - } + fields: ['testAgain'], + }; const requestIdAgain = 1; - addMockSubscription(parseLiveQueryServer, clientIdAgain, requestIdAgain, parseWebSocketAgain, queryAgain); + addMockSubscription( + parseLiveQueryServer, + clientIdAgain, + requestIdAgain, + parseWebSocketAgain, + queryAgain + ); // Make sure we only have one subscription const subscriptions = parseLiveQueryServer.subscriptions; @@ -269,8 +319,7 @@ describe('ParseLiveQueryServer', function() { it('can handle unsubscribe command without clientId', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); - const incompleteParseConn = { - }; + const incompleteParseConn = {}; parseLiveQueryServer._handleUnsubscribe(incompleteParseConn, {}); const Client = require('../lib/LiveQuery/Client').Client; @@ -280,7 +329,7 @@ describe('ParseLiveQueryServer', function() { it('can handle unsubscribe command without not existed client', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const parseWebSocket = { - clientId: 1 + clientId: 1, }; parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {}); @@ -295,7 +344,7 @@ describe('ParseLiveQueryServer', function() { addMockClient(parseLiveQueryServer, clientId); // Handle unsubscribe command const parseWebSocket = { - clientId: 1 + clientId: 1, }; parseLiveQueryServer._handleUnsubscribe(parseWebSocket, {}); @@ -310,25 +359,34 @@ describe('ParseLiveQueryServer', function() { const client = addMockClient(parseLiveQueryServer, clientId); // Add subscription for mock client const parseWebSocket = { - clientId: 1 + clientId: 1, }; const requestId = 2; - const subscription = addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket); + const subscription = addMockSubscription( + parseLiveQueryServer, + clientId, + requestId, + parseWebSocket + ); // Mock client.getSubscriptionInfo - const subscriptionInfo = client.addSubscriptionInfo.calls.mostRecent().args[1]; + const subscriptionInfo = client.addSubscriptionInfo.calls.mostRecent() + .args[1]; client.getSubscriptionInfo = function() { return subscriptionInfo; }; // Handle unsubscribe command const requestAgain = { - requestId: requestId + requestId: requestId, }; parseLiveQueryServer._handleUnsubscribe(parseWebSocket, requestAgain); // Make sure we delete subscription from client expect(client.deleteSubscriptionInfo).toHaveBeenCalledWith(requestId); // Make sure we delete client from subscription - expect(subscription.deleteClientSubscription).toHaveBeenCalledWith(clientId, requestId); + expect(subscription.deleteClientSubscription).toHaveBeenCalledWith( + clientId, + requestId + ); // Make sure we clear subscription in the server const subscriptions = parseLiveQueryServer.subscriptions; expect(subscriptions.size).toBe(0); @@ -347,7 +405,7 @@ describe('ParseLiveQueryServer', function() { // Check connect request const connectRequest = { op: 'connect', - applicationId: '1' + applicationId: '1', }; // Trigger message event parseWebSocket.emit('message', connectRequest); @@ -359,7 +417,9 @@ describe('ParseLiveQueryServer', function() { it('can set subscribe command message handler for a parseWebSocket', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); // Register mock connect/subscribe/unsubscribe handler for the server - parseLiveQueryServer._handleSubscribe = jasmine.createSpy('_handleSubscribe'); + parseLiveQueryServer._handleSubscribe = jasmine.createSpy( + '_handleSubscribe' + ); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); @@ -370,7 +430,7 @@ describe('ParseLiveQueryServer', function() { const subscribeRequest = JSON.stringify({ op: 'subscribe', requestId: 1, - query: {className: 'Test', where: {}} + query: { className: 'Test', where: {} }, }); // Trigger message event parseWebSocket.emit('message', subscribeRequest); @@ -383,7 +443,9 @@ describe('ParseLiveQueryServer', function() { it('can set unsubscribe command message handler for a parseWebSocket', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); // Register mock connect/subscribe/unsubscribe handler for the server - parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy('_handleSubscribe'); + parseLiveQueryServer._handleUnsubscribe = jasmine.createSpy( + '_handleSubscribe' + ); // Make mock parseWebsocket const EventEmitter = require('events'); const parseWebSocket = new EventEmitter(); @@ -391,11 +453,15 @@ describe('ParseLiveQueryServer', function() { parseLiveQueryServer._onConnect(parseWebSocket); // Check unsubscribe request - const unsubscribeRequest = JSON.stringify({op: 'unsubscribe', requestId: 1}); + const unsubscribeRequest = JSON.stringify({ + op: 'unsubscribe', + requestId: 1, + }); // Trigger message event parseWebSocket.emit('message', unsubscribeRequest); // Make sure _handleUnsubscribe is called - const args = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args; + const args = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent() + .args; expect(args[0]).toBe(parseWebSocket); expect(JSON.stringify(args[1])).toBe(unsubscribeRequest); }); @@ -418,16 +484,18 @@ describe('ParseLiveQueryServer', function() { const updateRequest = JSON.stringify({ op: 'update', requestId: 1, - query: {className: 'Test', where: {}} + query: { className: 'Test', where: {} }, }); // Trigger message event parseWebSocket.emit('message', updateRequest); // Make sure _handleUnsubscribe is called - const args = parseLiveQueryServer._handleUpdateSubscription.calls.mostRecent().args; + const args = parseLiveQueryServer._handleUpdateSubscription.calls.mostRecent() + .args; expect(args[0]).toBe(parseWebSocket); expect(JSON.stringify(args[1])).toBe(updateRequest); expect(parseLiveQueryServer._handleUnsubscribe).toHaveBeenCalled(); - const unsubArgs = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent().args; + const unsubArgs = parseLiveQueryServer._handleUnsubscribe.calls.mostRecent() + .args; expect(unsubArgs.length).toBe(3); expect(unsubArgs[2]).toBe(false); expect(parseLiveQueryServer._handleSubscribe).toHaveBeenCalled(); @@ -480,8 +548,8 @@ describe('ParseLiveQueryServer', function() { it('can forward event to cloud code', function() { const cloudCodeHandler = { - handler: () => {} - } + handler: () => {}, + }; const spy = spyOn(cloudCodeHandler, 'handler').and.callThrough(); Parse.Cloud.onLiveQueryEvent(cloudCodeHandler.handler); const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); @@ -499,7 +567,6 @@ describe('ParseLiveQueryServer', function() { expect(spy.calls.count()).toBe(2); }); - // TODO: Test server can set disconnect command message handler for a parseWebSocket it('has no subscription and can handle object delete command', function() { @@ -508,11 +575,11 @@ describe('ParseLiveQueryServer', function() { const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ key: 'value', - className: testClassName + className: testClassName, }); // Make mock message const message = { - currentParseObject: parseObject + currentParseObject: parseObject, }; // Make sure we do not crash in this case parseLiveQueryServer._onAfterDelete(message, {}); @@ -524,11 +591,11 @@ describe('ParseLiveQueryServer', function() { const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ key: 'value', - className: testClassName + className: testClassName, }); // Make mock message const message = { - currentParseObject: parseObject + currentParseObject: parseObject, }; // Add mock client @@ -557,11 +624,11 @@ describe('ParseLiveQueryServer', function() { const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ key: 'value', - className: testClassName + className: testClassName, }); // Make mock message const message = { - currentParseObject: parseObject + currentParseObject: parseObject, }; // Add mock client const clientId = 1; @@ -610,13 +677,13 @@ describe('ParseLiveQueryServer', function() { return false; }; parseLiveQueryServer._matchesACL = function() { - return Promise.resolve(true) + return Promise.resolve(true); }; // Trigger onAfterSave parseLiveQueryServer._onAfterSave(message); // Make sure we do not send command to client - setTimeout(function(){ + setTimeout(function() { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -640,7 +707,7 @@ describe('ParseLiveQueryServer', function() { // In order to mimic a enter, we need original match return false // and the current match return true let counter = 0; - parseLiveQueryServer._matchesSubscription = function(parseObject){ + parseLiveQueryServer._matchesSubscription = function(parseObject) { if (!parseObject) { return false; } @@ -648,12 +715,12 @@ describe('ParseLiveQueryServer', function() { return counter % 2 === 0; }; parseLiveQueryServer._matchesACL = function() { - return Promise.resolve(true) + return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send enter command to client - setTimeout(function(){ + setTimeout(function() { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -674,19 +741,19 @@ describe('ParseLiveQueryServer', function() { const requestId = 2; addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject){ + parseLiveQueryServer._matchesSubscription = function(parseObject) { if (!parseObject) { return false; } return true; }; parseLiveQueryServer._matchesACL = function() { - return Promise.resolve(true) + return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send update command to client - setTimeout(function(){ + setTimeout(function() { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).toHaveBeenCalled(); @@ -710,7 +777,7 @@ describe('ParseLiveQueryServer', function() { // In order to mimic a leave, we need original match return true // and the current match return false let counter = 0; - parseLiveQueryServer._matchesSubscription = function(parseObject){ + parseLiveQueryServer._matchesSubscription = function(parseObject) { if (!parseObject) { return false; } @@ -718,12 +785,12 @@ describe('ParseLiveQueryServer', function() { return counter % 2 !== 0; }; parseLiveQueryServer._matchesACL = function() { - return Promise.resolve(true) + return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send leave command to client - setTimeout(function(){ + setTimeout(function() { expect(client.pushCreate).not.toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -744,19 +811,19 @@ describe('ParseLiveQueryServer', function() { const requestId = 2; addMockSubscription(parseLiveQueryServer, clientId, requestId); // Mock _matchesSubscription to return matching - parseLiveQueryServer._matchesSubscription = function(parseObject){ + parseLiveQueryServer._matchesSubscription = function(parseObject) { if (!parseObject) { return false; } return true; }; parseLiveQueryServer._matchesACL = function() { - return Promise.resolve(true) + return Promise.resolve(true); }; parseLiveQueryServer._onAfterSave(message); // Make sure we send create command to client - setTimeout(function(){ + setTimeout(function() { expect(client.pushCreate).toHaveBeenCalled(); expect(client.pushEnter).not.toHaveBeenCalled(); expect(client.pushUpdate).not.toHaveBeenCalled(); @@ -770,11 +837,15 @@ describe('ParseLiveQueryServer', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); // Make mock subscription const subscription = { - match: jasmine.createSpy('match') - } + match: jasmine.createSpy('match'), + }; - expect(parseLiveQueryServer._matchesSubscription(null, subscription)).toBe(false); - expect(parseLiveQueryServer._matchesSubscription(undefined, subscription)).toBe(false); + expect(parseLiveQueryServer._matchesSubscription(null, subscription)).toBe( + false + ); + expect( + parseLiveQueryServer._matchesSubscription(undefined, subscription) + ).toBe(false); // Make sure subscription.match is not called expect(subscription.match).not.toHaveBeenCalled(); }); @@ -783,10 +854,12 @@ describe('ParseLiveQueryServer', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); // Make mock subscription const subscription = { - query: {} - } + query: {}, + }; const parseObject = {}; - expect(parseLiveQueryServer._matchesSubscription(parseObject, subscription)).toBe(true); + expect( + parseLiveQueryServer._matchesSubscription(parseObject, subscription) + ).toBe(true); // Make sure matchesQuery is called const matchesQuery = require('../lib/LiveQuery/QueryTools').matchesQuery; expect(matchesQuery).toHaveBeenCalledWith(parseObject, subscription.query); @@ -796,22 +869,22 @@ describe('ParseLiveQueryServer', function() { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); // Make mock request const objectJSON = { - "className":"testClassName", - "createdAt":"2015-12-22T01:51:12.955Z", - "key":"value", - "objectId":"BfwxBCz6yW", - "updatedAt":"2016-01-05T00:46:45.659Z" + className: 'testClassName', + createdAt: '2015-12-22T01:51:12.955Z', + key: 'value', + objectId: 'BfwxBCz6yW', + updatedAt: '2016-01-05T00:46:45.659Z', }; const originalObjectJSON = { - "className":"testClassName", - "createdAt":"2015-12-22T01:51:12.955Z", - "key":"originalValue", - "objectId":"BfwxBCz6yW", - "updatedAt":"2016-01-05T00:46:45.659Z" + className: 'testClassName', + createdAt: '2015-12-22T01:51:12.955Z', + key: 'originalValue', + objectId: 'BfwxBCz6yW', + updatedAt: '2016-01-05T00:46:45.659Z', }; const message = { currentParseObject: objectJSON, - originalParseObject: originalObjectJSON + originalParseObject: originalObjectJSON, }; // Inflate the object parseLiveQueryServer._inflateParseObject(message); @@ -839,24 +912,30 @@ describe('ParseLiveQueryServer', function() { const client = {}; const requestId = 0; - parseLiveQueryServer._matchesACL(undefined, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(true); - done(); - }); + parseLiveQueryServer + ._matchesACL(undefined, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(true); + done(); + }); }); it('can match ACL with none exist requestId', function(done) { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const acl = new Parse.ACL(); const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue(undefined) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue(undefined), }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(false); - done(); - }); + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(false); + done(); + }); }); it('can match ACL with public read access', function(done) { @@ -864,16 +943,20 @@ describe('ParseLiveQueryServer', function() { const acl = new Parse.ACL(); acl.setPublicReadAccess(true); const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - sessionToken: 'sessionToken' - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: 'sessionToken', + }), }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(true); - done(); - }); + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(true); + done(); + }); }); it('can match ACL with valid subscription sessionToken', function(done) { @@ -881,16 +964,20 @@ describe('ParseLiveQueryServer', function() { const acl = new Parse.ACL(); acl.setReadAccess(testUserId, true); const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - sessionToken: 'sessionToken' - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: 'sessionToken', + }), }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(true); - done(); - }); + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(true); + done(); + }); }); it('can match ACL with valid client sessionToken', function(done) { @@ -900,16 +987,20 @@ describe('ParseLiveQueryServer', function() { // Mock sessionTokenCache will return false when sessionToken is undefined const client = { sessionToken: 'sessionToken', - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - sessionToken: undefined - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: undefined, + }), }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(true); - done(); - }); + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(true); + done(); + }); }); it('can match ACL with invalid subscription and client sessionToken', function(done) { @@ -919,16 +1010,20 @@ describe('ParseLiveQueryServer', function() { // Mock sessionTokenCache will return false when sessionToken is undefined const client = { sessionToken: undefined, - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - sessionToken: undefined - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: undefined, + }), }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(false); - done(); - }); + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(false); + done(); + }); }); it('can match ACL with subscription sessionToken checking error', function(done) { @@ -938,16 +1033,20 @@ describe('ParseLiveQueryServer', function() { // Mock sessionTokenCache will return error when sessionToken is null, this is just // the behaviour of our mock sessionTokenCache, not real sessionTokenCache const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - sessionToken: null - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: null, + }), }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(false); - done(); - }); + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(false); + done(); + }); }); it('can match ACL with client sessionToken checking error', function(done) { @@ -957,70 +1056,78 @@ describe('ParseLiveQueryServer', function() { // Mock sessionTokenCache will return error when sessionToken is null const client = { sessionToken: null, - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - sessionToken: null - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: null, + }), }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(false); - done(); - }); + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(false); + done(); + }); }); - it('won\'t match ACL that doesn\'t have public read or any roles', function(done){ - + it("won't match ACL that doesn't have public read or any roles", function(done) { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - sessionToken: 'sessionToken' - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: 'sessionToken', + }), }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(false); - done(); - }); - + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(false); + done(); + }); }); - it('won\'t match non-public ACL with role when there is no user', function(done){ - + it("won't match non-public ACL with role when there is no user", function(done) { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); - acl.setRoleReadAccess("livequery", true); + acl.setRoleReadAccess('livequery', true); const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({}), }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(false); - done(); - }); - + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(false); + done(); + }); }); - it('won\'t match ACL with role based read access set to false', function(done){ - + it("won't match ACL with role based read access set to false", function(done) { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); - acl.setRoleReadAccess("liveQueryRead", false); + acl.setRoleReadAccess('liveQueryRead', false); const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - sessionToken: 'sessionToken' - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: 'sessionToken', + }), }; const requestId = 0; - spyOn(Parse, "Query").and.callFake(function(){ + spyOn(Parse, 'Query').and.callFake(function() { return { equalTo() { // Nothing to do here @@ -1029,34 +1136,34 @@ describe('ParseLiveQueryServer', function() { //Return a role with the name "liveQueryRead" as that is what was set on the ACL const liveQueryRole = new Parse.Role(); liveQueryRole.set('name', 'liveQueryRead'); - return [ - liveQueryRole - ]; - } - } - }); - - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(false); - done(); + return [liveQueryRole]; + }, + }; }); + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(false); + done(); + }); }); - it('will match ACL with role based read access set to true', function(done){ - + it('will match ACL with role based read access set to true', function(done) { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); - acl.setRoleReadAccess("liveQueryRead", true); + acl.setRoleReadAccess('liveQueryRead', true); const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - sessionToken: 'sessionToken' - }) + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({ + sessionToken: 'sessionToken', + }), }; const requestId = 0; - spyOn(Parse, "Query").and.callFake(function(){ + spyOn(Parse, 'Query').and.callFake(function() { return { equalTo() { // Nothing to do here @@ -1065,157 +1172,196 @@ describe('ParseLiveQueryServer', function() { //Return a role with the name "liveQueryRead" as that is what was set on the ACL const liveQueryRole = new Parse.Role(); liveQueryRole.set('name', 'liveQueryRead'); - return [ - liveQueryRole - ]; - } - } - }); - - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(true); - done(); + return [liveQueryRole]; + }, + }; }); + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(true); + done(); + }); }); it('can validate key when valid key is provided', function() { - const parseLiveQueryServer = new ParseLiveQueryServer({}, { - keyPairs: { - clientKey: 'test' + const parseLiveQueryServer = new ParseLiveQueryServer( + {}, + { + keyPairs: { + clientKey: 'test', + }, } - }); + ); const request = { - clientKey: 'test' - } + clientKey: 'test', + }; - expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy(); + expect( + parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs) + ).toBeTruthy(); }); it('can validate key when invalid key is provided', function() { - const parseLiveQueryServer = new ParseLiveQueryServer({}, { - keyPairs: { - clientKey: 'test' + const parseLiveQueryServer = new ParseLiveQueryServer( + {}, + { + keyPairs: { + clientKey: 'test', + }, } - }); + ); const request = { - clientKey: 'error' - } + clientKey: 'error', + }; - expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy(); + expect( + parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs) + ).not.toBeTruthy(); }); it('can validate key when key is not provided', function() { - const parseLiveQueryServer = new ParseLiveQueryServer({}, { - keyPairs: { - clientKey: 'test' + const parseLiveQueryServer = new ParseLiveQueryServer( + {}, + { + keyPairs: { + clientKey: 'test', + }, } - }); - const request = { - } + ); + const request = {}; - expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy(); + expect( + parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs) + ).not.toBeTruthy(); }); it('can validate key when validKerPairs is empty', function() { const parseLiveQueryServer = new ParseLiveQueryServer({}, {}); - const request = { - } + const request = {}; - expect(parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs)).toBeTruthy(); + expect( + parseLiveQueryServer._validateKeys(request, parseLiveQueryServer.keyPairs) + ).toBeTruthy(); }); it('can validate client has master key when valid', function() { - const parseLiveQueryServer = new ParseLiveQueryServer({}, { - keyPairs: { - masterKey: 'test' + const parseLiveQueryServer = new ParseLiveQueryServer( + {}, + { + keyPairs: { + masterKey: 'test', + }, } - }); + ); const request = { - masterKey: 'test' + masterKey: 'test', }; - expect(parseLiveQueryServer._hasMasterKey(request, parseLiveQueryServer.keyPairs)).toBeTruthy(); + expect( + parseLiveQueryServer._hasMasterKey(request, parseLiveQueryServer.keyPairs) + ).toBeTruthy(); }); - it('can validate client doesn\'t have master key when invalid', function() { - const parseLiveQueryServer = new ParseLiveQueryServer({}, { - keyPairs: { - masterKey: 'test' + it("can validate client doesn't have master key when invalid", function() { + const parseLiveQueryServer = new ParseLiveQueryServer( + {}, + { + keyPairs: { + masterKey: 'test', + }, } - }); + ); const request = { - masterKey: 'notValid' + masterKey: 'notValid', }; - expect(parseLiveQueryServer._hasMasterKey(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy(); + expect( + parseLiveQueryServer._hasMasterKey(request, parseLiveQueryServer.keyPairs) + ).not.toBeTruthy(); }); - it('can validate client doesn\'t have master key when not provided', function() { - const parseLiveQueryServer = new ParseLiveQueryServer({}, { - keyPairs: { - masterKey: 'test' + it("can validate client doesn't have master key when not provided", function() { + const parseLiveQueryServer = new ParseLiveQueryServer( + {}, + { + keyPairs: { + masterKey: 'test', + }, } - }); + ); - expect(parseLiveQueryServer._hasMasterKey({}, parseLiveQueryServer.keyPairs)).not.toBeTruthy(); + expect( + parseLiveQueryServer._hasMasterKey({}, parseLiveQueryServer.keyPairs) + ).not.toBeTruthy(); }); - it('can validate client doesn\'t have master key when validKeyPairs is empty', function() { + it("can validate client doesn't have master key when validKeyPairs is empty", function() { const parseLiveQueryServer = new ParseLiveQueryServer({}, {}); const request = { - masterKey: 'test' + masterKey: 'test', }; - expect(parseLiveQueryServer._hasMasterKey(request, parseLiveQueryServer.keyPairs)).not.toBeTruthy(); + expect( + parseLiveQueryServer._hasMasterKey(request, parseLiveQueryServer.keyPairs) + ).not.toBeTruthy(); }); - it('will match non-public ACL when client has master key', function(done){ - + it('will match non-public ACL when client has master key', function(done) { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - }), - hasMasterKey: true + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({}), + hasMasterKey: true, }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(true); - done(); - }); - + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(true); + done(); + }); }); - it('won\'t match non-public ACL when client has no master key', function(done){ - + it("won't match non-public ACL when client has no master key", function(done) { const parseLiveQueryServer = new ParseLiveQueryServer(10, 10, {}); const acl = new Parse.ACL(); acl.setPublicReadAccess(false); const client = { - getSubscriptionInfo: jasmine.createSpy('getSubscriptionInfo').and.returnValue({ - }), - hasMasterKey: false + getSubscriptionInfo: jasmine + .createSpy('getSubscriptionInfo') + .and.returnValue({}), + hasMasterKey: false, }; const requestId = 0; - parseLiveQueryServer._matchesACL(acl, client, requestId).then(function(isMatched) { - expect(isMatched).toBe(false); - done(); - }); - + parseLiveQueryServer + ._matchesACL(acl, client, requestId) + .then(function(isMatched) { + expect(isMatched).toBe(false); + done(); + }); }); - afterEach(function(){ - jasmine.restoreLibrary('../lib/LiveQuery/ParseWebSocketServer', 'ParseWebSocketServer'); + afterEach(function() { + jasmine.restoreLibrary( + '../lib/LiveQuery/ParseWebSocketServer', + 'ParseWebSocketServer' + ); jasmine.restoreLibrary('../lib/LiveQuery/Client', 'Client'); jasmine.restoreLibrary('../lib/LiveQuery/Subscription', 'Subscription'); jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'queryHash'); jasmine.restoreLibrary('../lib/LiveQuery/QueryTools', 'matchesQuery'); jasmine.restoreLibrary('../lib/LiveQuery/ParsePubSub', 'ParsePubSub'); - jasmine.restoreLibrary('../lib/LiveQuery/SessionTokenCache', 'SessionTokenCache'); + jasmine.restoreLibrary( + '../lib/LiveQuery/SessionTokenCache', + 'SessionTokenCache' + ); }); // Helper functions to add mock client and subscription to a liveQueryServer @@ -1226,7 +1372,13 @@ describe('ParseLiveQueryServer', function() { return client; } - function addMockSubscription(parseLiveQueryServer, clientId, requestId, parseWebSocket, query) { + function addMockSubscription( + parseLiveQueryServer, + clientId, + requestId, + parseWebSocket, + query + ) { // If parseWebSocket is null, we use the default one if (!parseWebSocket) { const EventEmitter = require('events'); @@ -1238,26 +1390,31 @@ describe('ParseLiveQueryServer', function() { query = { className: testClassName, where: { - key: 'value' + key: 'value', }, - fields: [ 'test' ] + fields: ['test'], }; } const request = { query: query, requestId: requestId, - sessionToken: 'sessionToken' + sessionToken: 'sessionToken', }; parseLiveQueryServer._handleSubscribe(parseWebSocket, request); // Make mock subscription - const subscription = parseLiveQueryServer.subscriptions.get(query.className).get(queryHashValue); + const subscription = parseLiveQueryServer.subscriptions + .get(query.className) + .get(queryHashValue); subscription.hasSubscribingClient = function() { return false; - } + }; subscription.className = query.className; subscription.hash = queryHashValue; - if (subscription.clientRequestIds && subscription.clientRequestIds.has(clientId)) { + if ( + subscription.clientRequestIds && + subscription.clientRequestIds.has(clientId) + ) { subscription.clientRequestIds.get(clientId).push(requestId); } else { subscription.clientRequestIds = new Map([[clientId, [requestId]]]); @@ -1270,16 +1427,16 @@ describe('ParseLiveQueryServer', function() { const parseObject = new Parse.Object(testClassName); parseObject._finishFetch({ key: 'value', - className: testClassName + className: testClassName, }); const message = { - currentParseObject: parseObject + currentParseObject: parseObject, }; if (hasOriginalParseObject) { const originalParseObject = new Parse.Object(testClassName); originalParseObject._finishFetch({ key: 'originalValue', - className: testClassName + className: testClassName, }); message.originalParseObject = originalParseObject; } diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index ecac81baf8..ca3a5767ba 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; // This is a port of the test suite: // hungry/js/test/parse_object_test.js // @@ -13,74 +13,81 @@ // single-instance mode. describe('Parse.Object testing', () => { - it("create", function(done) { - create({ "test" : "test" }, function(model) { - ok(model.id, "Should have an objectId set"); - equal(model.get("test"), "test", "Should have the right attribute"); + it('create', function(done) { + create({ test: 'test' }, function(model) { + ok(model.id, 'Should have an objectId set'); + equal(model.get('test'), 'test', 'Should have the right attribute'); done(); }); }); - it("update", function(done) { - create({ "test" : "test" }, function(model) { + it('update', function(done) { + create({ test: 'test' }, function(model) { const t2 = new TestObject({ objectId: model.id }); - t2.set("test", "changed"); + t2.set('test', 'changed'); t2.save().then(function(model) { - equal(model.get("test"), "changed", "Update should have succeeded"); + equal(model.get('test'), 'changed', 'Update should have succeeded'); done(); }); }); }); - it("save without null", function(done) { + it('save without null', function(done) { const object = new TestObject(); - object.set("favoritePony", "Rainbow Dash"); - object.save().then(function(objectAgain) { - equal(objectAgain, object); - done(); - }, function(objectAgain, error) { - ok(null, "Error " + error.code + ": " + error.message); - done(); - }); - }); - - it("save cycle", done => { - const a = new Parse.Object("TestObject"); - const b = new Parse.Object("TestObject"); - a.set("b", b); - a.save().then(function() { - b.set("a", a); - return b.save(); - - }).then(function() { - ok(a.id); - ok(b.id); - strictEqual(a.get("b"), b); - strictEqual(b.get("a"), a); - - }).then(function() { - done(); - }, function(error) { - ok(false, error); - done(); - }); + object.set('favoritePony', 'Rainbow Dash'); + object.save().then( + function(objectAgain) { + equal(objectAgain, object); + done(); + }, + function(objectAgain, error) { + ok(null, 'Error ' + error.code + ': ' + error.message); + done(); + } + ); + }); + + it('save cycle', done => { + const a = new Parse.Object('TestObject'); + const b = new Parse.Object('TestObject'); + a.set('b', b); + a.save() + .then(function() { + b.set('a', a); + return b.save(); + }) + .then(function() { + ok(a.id); + ok(b.id); + strictEqual(a.get('b'), b); + strictEqual(b.get('a'), a); + }) + .then( + function() { + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it("get", function(done) { - create({ "test" : "test" }, function(model) { + it('get', function(done) { + create({ test: 'test' }, function(model) { const t2 = new TestObject({ objectId: model.id }); t2.fetch().then(function(model2) { - equal(model2.get("test"), "test", "Update should have succeeded"); + equal(model2.get('test'), 'test', 'Update should have succeeded'); ok(model2.id); - equal(model2.id, model.id, "Ids should match"); + equal(model2.id, model.id, 'Ids should match'); done(); }); }); }); - it("delete", function(done) { + it('delete', function(done) { const t = new TestObject(); - t.set("test", "test"); + t.set('test', 'test'); t.save().then(function() { t.destroy().then(function() { const t2 = new TestObject({ objectId: t.id }); @@ -89,12 +96,12 @@ describe('Parse.Object testing', () => { }); }); - it("find", function(done) { + it('find', function(done) { const t = new TestObject(); - t.set("foo", "bar"); + t.set('foo', 'bar'); t.save().then(function() { const query = new Parse.Query(TestObject); - query.equalTo("foo", "bar"); + query.equalTo('foo', 'bar'); query.find().then(function(results) { equal(results.length, 1); done(); @@ -102,65 +109,64 @@ describe('Parse.Object testing', () => { }); }); - it("relational fields", function(done) { + it('relational fields', function(done) { const item = new Item(); - item.set("property", "x"); + item.set('property', 'x'); const container = new Container(); - container.set("item", item); + container.set('item', item); Parse.Object.saveAll([item, container]).then(function() { const query = new Parse.Query(Container); query.find().then(function(results) { equal(results.length, 1); const containerAgain = results[0]; - const itemAgain = containerAgain.get("item"); + const itemAgain = containerAgain.get('item'); itemAgain.fetch().then(function() { - equal(itemAgain.get("property"), "x"); + equal(itemAgain.get('property'), 'x'); done(); }); }); }); }); - it("save adds no data keys (other than createdAt and updatedAt)", - function(done) { - const object = new TestObject(); - object.save().then(function() { - const keys = Object.keys(object.attributes).sort(); - equal(keys.length, 2); - done(); - }); + it('save adds no data keys (other than createdAt and updatedAt)', function(done) { + const object = new TestObject(); + object.save().then(function() { + const keys = Object.keys(object.attributes).sort(); + equal(keys.length, 2); + done(); }); + }); - it("recursive save", function(done) { + it('recursive save', function(done) { const item = new Item(); - item.set("property", "x"); + item.set('property', 'x'); const container = new Container(); - container.set("item", item); + container.set('item', item); container.save().then(function() { const query = new Parse.Query(Container); query.find().then(function(results) { equal(results.length, 1); const containerAgain = results[0]; - const itemAgain = containerAgain.get("item"); + const itemAgain = containerAgain.get('item'); itemAgain.fetch().then(function() { - equal(itemAgain.get("property"), "x"); + equal(itemAgain.get('property'), 'x'); done(); }); }); }); }); - it("fetch", function(done) { - const item = new Item({ foo: "bar" }); + it('fetch', function(done) { + const item = new Item({ foo: 'bar' }); item.save().then(function() { const itemAgain = new Item(); itemAgain.id = item.id; itemAgain.fetch().then(function() { - itemAgain.save({ foo: "baz" }).then(function() { + itemAgain.save({ foo: 'baz' }).then(function() { item.fetch().then(function() { - equal(item.get("foo"), itemAgain.get("foo")); + equal(item.get('foo'), itemAgain.get('foo')); done(); }); }); @@ -169,7 +175,7 @@ describe('Parse.Object testing', () => { }); it("createdAt doesn't change", function(done) { - const object = new TestObject({ foo: "bar" }); + const object = new TestObject({ foo: 'bar' }); object.save().then(function() { const objectAgain = new TestObject(); objectAgain.id = object.id; @@ -180,8 +186,8 @@ describe('Parse.Object testing', () => { }); }); - it("createdAt and updatedAt exposed", function(done) { - const object = new TestObject({ foo: "bar" }); + it('createdAt and updatedAt exposed', function(done) { + const object = new TestObject({ foo: 'bar' }); object.save().then(function() { notEqual(object.updatedAt, undefined); notEqual(object.createdAt, undefined); @@ -189,65 +195,71 @@ describe('Parse.Object testing', () => { }); }); - it("updatedAt gets updated", function(done) { - const object = new TestObject({ foo: "bar" }); + it('updatedAt gets updated', function(done) { + const object = new TestObject({ foo: 'bar' }); object.save().then(function() { - ok(object.updatedAt, "initial save should cause updatedAt to exist"); + ok(object.updatedAt, 'initial save should cause updatedAt to exist'); const firstUpdatedAt = object.updatedAt; - object.save({ foo: "baz" }).then(function() { - ok(object.updatedAt, "two saves should cause updatedAt to exist"); + object.save({ foo: 'baz' }).then(function() { + ok(object.updatedAt, 'two saves should cause updatedAt to exist'); notEqual(firstUpdatedAt, object.updatedAt); done(); }); }); }); - it("createdAt is reasonable", function(done) { + it('createdAt is reasonable', function(done) { const startTime = new Date(); - const object = new TestObject({ foo: "bar" }); + const object = new TestObject({ foo: 'bar' }); object.save().then(function() { const endTime = new Date(); - const startDiff = Math.abs(startTime.getTime() - - object.createdAt.getTime()); + const startDiff = Math.abs( + startTime.getTime() - object.createdAt.getTime() + ); ok(startDiff < 5000); - const endDiff = Math.abs(endTime.getTime() - - object.createdAt.getTime()); + const endDiff = Math.abs(endTime.getTime() - object.createdAt.getTime()); ok(endDiff < 5000); done(); }); }); - it_exclude_dbs(['postgres'])("can set null", function(done) { - const obj = new Parse.Object("TestObject"); - obj.set("foo", null); - obj.save().then(function(obj) { - on_db('mongo', () => { - equal(obj.get("foo"), null); - }); - on_db('postgres', () => { - fail('should not succeed'); - }); - done(); - }, function() { - fail('should not fail'); - done(); - }); + it_exclude_dbs(['postgres'])('can set null', function(done) { + const obj = new Parse.Object('TestObject'); + obj.set('foo', null); + obj.save().then( + function(obj) { + on_db('mongo', () => { + equal(obj.get('foo'), null); + }); + on_db('postgres', () => { + fail('should not succeed'); + }); + done(); + }, + function() { + fail('should not fail'); + done(); + } + ); }); - it("can set boolean", function(done) { - const obj = new Parse.Object("TestObject"); - obj.set("yes", true); - obj.set("no", false); - obj.save().then(function(obj) { - equal(obj.get("yes"), true); - equal(obj.get("no"), false); - done(); - }, function(obj, error) { - ok(false, error.message); - done(); - }); + it('can set boolean', function(done) { + const obj = new Parse.Object('TestObject'); + obj.set('yes', true); + obj.set('no', false); + obj.save().then( + function(obj) { + equal(obj.get('yes'), true); + equal(obj.get('no'), false); + done(); + }, + function(obj, error) { + ok(false, error.message); + done(); + } + ); }); it('cannot set invalid date', async function(done) { @@ -264,33 +276,38 @@ describe('Parse.Object testing', () => { done(); }); - it("invalid class name", function(done) { - const item = new Parse.Object("Foo^bar"); - item.save().then(function() { - ok(false, "The name should have been invalid."); - done(); - }, function() { - // Because the class name is invalid, the router will not be able to route - // it, so it will actually return a -1 error code. - // equal(error.code, Parse.Error.INVALID_CLASS_NAME); - done(); - }); + it('invalid class name', function(done) { + const item = new Parse.Object('Foo^bar'); + item.save().then( + function() { + ok(false, 'The name should have been invalid.'); + done(); + }, + function() { + // Because the class name is invalid, the router will not be able to route + // it, so it will actually return a -1 error code. + // equal(error.code, Parse.Error.INVALID_CLASS_NAME); + done(); + } + ); }); - it("invalid key name", function(done) { - const item = new Parse.Object("Item"); - ok(!item.set({"foo^bar": "baz"}), - 'Item should not be updated with invalid key.'); - item.save({ "foo^bar": "baz" }).then(fail, done); + it('invalid key name', function(done) { + const item = new Parse.Object('Item'); + ok( + !item.set({ 'foo^bar': 'baz' }), + 'Item should not be updated with invalid key.' + ); + item.save({ 'foo^bar': 'baz' }).then(fail, done); }); - it("invalid __type", function(done) { - const item = new Parse.Object("Item"); + it('invalid __type', function(done) { + const item = new Parse.Object('Item'); const types = ['Pointer', 'File', 'Date', 'GeoPoint', 'Bytes', 'Polygon']; const tests = types.map(type => { - const test = new Parse.Object("Item"); + const test = new Parse.Object('Item'); test.set('foo', { - __type: type + __type: type, }); return test; }); @@ -303,573 +320,647 @@ describe('Parse.Object testing', () => { } else { done(); } - } - item.save({ - "foo": { - __type: "IvalidName" - } - }).then(fail, () => next(0)); - }); - - it("simple field deletion", function(done) { - const simple = new Parse.Object("SimpleObject"); - simple.save({ - foo: "bar" - }).then(function(simple) { - simple.unset("foo"); - ok(!simple.has("foo"), "foo should have been unset."); - ok(simple.dirty("foo"), "foo should be dirty."); - ok(simple.dirty(), "the whole object should be dirty."); - simple.save().then(function(simple) { - ok(!simple.has("foo"), "foo should have been unset."); - ok(!simple.dirty("foo"), "the whole object was just saved."); - ok(!simple.dirty(), "the whole object was just saved."); - - const query = new Parse.Query("SimpleObject"); - query.get(simple.id).then(function(simpleAgain) { - ok(!simpleAgain.has("foo"), "foo should have been removed."); - done(); - }, function(simpleAgain, error) { - ok(false, "Error " + error.code + ": " + error.message); + }; + item + .save({ + foo: { + __type: 'IvalidName', + }, + }) + .then(fail, () => next(0)); + }); + + it('simple field deletion', function(done) { + const simple = new Parse.Object('SimpleObject'); + simple + .save({ + foo: 'bar', + }) + .then( + function(simple) { + simple.unset('foo'); + ok(!simple.has('foo'), 'foo should have been unset.'); + ok(simple.dirty('foo'), 'foo should be dirty.'); + ok(simple.dirty(), 'the whole object should be dirty.'); + simple.save().then( + function(simple) { + ok(!simple.has('foo'), 'foo should have been unset.'); + ok(!simple.dirty('foo'), 'the whole object was just saved.'); + ok(!simple.dirty(), 'the whole object was just saved.'); + + const query = new Parse.Query('SimpleObject'); + query.get(simple.id).then( + function(simpleAgain) { + ok(!simpleAgain.has('foo'), 'foo should have been removed.'); + done(); + }, + function(simpleAgain, error) { + ok(false, 'Error ' + error.code + ': ' + error.message); + done(); + } + ); + }, + function(simple, error) { + ok(false, 'Error ' + error.code + ': ' + error.message); + done(); + } + ); + }, + function(simple, error) { + ok(false, 'Error ' + error.code + ': ' + error.message); done(); - }); - }, function(simple, error) { - ok(false, "Error " + error.code + ": " + error.message); - done(); - }); - }, function(simple, error) { - ok(false, "Error " + error.code + ": " + error.message); - done(); - }); - }); - - it("field deletion before first save", function(done) { - const simple = new Parse.Object("SimpleObject"); - simple.set("foo", "bar"); - simple.unset("foo"); - - ok(!simple.has("foo"), "foo should have been unset."); - ok(simple.dirty("foo"), "foo should be dirty."); - ok(simple.dirty(), "the whole object should be dirty."); - simple.save().then(function(simple) { - ok(!simple.has("foo"), "foo should have been unset."); - ok(!simple.dirty("foo"), "the whole object was just saved."); - ok(!simple.dirty(), "the whole object was just saved."); - - const query = new Parse.Query("SimpleObject"); - query.get(simple.id).then(function(simpleAgain) { - ok(!simpleAgain.has("foo"), "foo should have been removed."); - done(); - }, function(simpleAgain, error) { - ok(false, "Error " + error.code + ": " + error.message); + } + ); + }); + + it('field deletion before first save', function(done) { + const simple = new Parse.Object('SimpleObject'); + simple.set('foo', 'bar'); + simple.unset('foo'); + + ok(!simple.has('foo'), 'foo should have been unset.'); + ok(simple.dirty('foo'), 'foo should be dirty.'); + ok(simple.dirty(), 'the whole object should be dirty.'); + simple.save().then( + function(simple) { + ok(!simple.has('foo'), 'foo should have been unset.'); + ok(!simple.dirty('foo'), 'the whole object was just saved.'); + ok(!simple.dirty(), 'the whole object was just saved.'); + + const query = new Parse.Query('SimpleObject'); + query.get(simple.id).then( + function(simpleAgain) { + ok(!simpleAgain.has('foo'), 'foo should have been removed.'); + done(); + }, + function(simpleAgain, error) { + ok(false, 'Error ' + error.code + ': ' + error.message); + done(); + } + ); + }, + function(simple, error) { + ok(false, 'Error ' + error.code + ': ' + error.message); done(); - }); - }, function(simple, error) { - ok(false, "Error " + error.code + ": " + error.message); - done(); - }); - }); - - it("relation deletion", function(done) { - const simple = new Parse.Object("SimpleObject"); - const child = new Parse.Object("Child"); - simple.save({ - child: child - }).then(function(simple) { - simple.unset("child"); - ok(!simple.has("child"), "child should have been unset."); - ok(simple.dirty("child"), "child should be dirty."); - ok(simple.dirty(), "the whole object should be dirty."); - simple.save().then(function(simple) { - ok(!simple.has("child"), "child should have been unset."); - ok(!simple.dirty("child"), "the whole object was just saved."); - ok(!simple.dirty(), "the whole object was just saved."); - - const query = new Parse.Query("SimpleObject"); - query.get(simple.id).then(function(simpleAgain) { - ok(!simpleAgain.has("child"), "child should have been removed."); - done(); - }, function(simpleAgain, error) { - ok(false, "Error " + error.code + ": " + error.message); + } + ); + }); + + it('relation deletion', function(done) { + const simple = new Parse.Object('SimpleObject'); + const child = new Parse.Object('Child'); + simple + .save({ + child: child, + }) + .then( + function(simple) { + simple.unset('child'); + ok(!simple.has('child'), 'child should have been unset.'); + ok(simple.dirty('child'), 'child should be dirty.'); + ok(simple.dirty(), 'the whole object should be dirty.'); + simple.save().then( + function(simple) { + ok(!simple.has('child'), 'child should have been unset.'); + ok(!simple.dirty('child'), 'the whole object was just saved.'); + ok(!simple.dirty(), 'the whole object was just saved.'); + + const query = new Parse.Query('SimpleObject'); + query.get(simple.id).then( + function(simpleAgain) { + ok( + !simpleAgain.has('child'), + 'child should have been removed.' + ); + done(); + }, + function(simpleAgain, error) { + ok(false, 'Error ' + error.code + ': ' + error.message); + done(); + } + ); + }, + function(simple, error) { + ok(false, 'Error ' + error.code + ': ' + error.message); + done(); + } + ); + }, + function(simple, error) { + ok(false, 'Error ' + error.code + ': ' + error.message); done(); - }); - }, function(simple, error) { - ok(false, "Error " + error.code + ": " + error.message); - done(); - }); - }, function(simple, error) { - ok(false, "Error " + error.code + ": " + error.message); - done(); - }); + } + ); }); - it("deleted keys get cleared", function(done) { - const simpleObject = new Parse.Object("SimpleObject"); - simpleObject.set("foo", "bar"); - simpleObject.unset("foo"); + it('deleted keys get cleared', function(done) { + const simpleObject = new Parse.Object('SimpleObject'); + simpleObject.set('foo', 'bar'); + simpleObject.unset('foo'); simpleObject.save().then(function(simpleObject) { - simpleObject.set("foo", "baz"); + simpleObject.set('foo', 'baz'); simpleObject.save().then(function(simpleObject) { - const query = new Parse.Query("SimpleObject"); + const query = new Parse.Query('SimpleObject'); query.get(simpleObject.id).then(function(simpleObjectAgain) { - equal(simpleObjectAgain.get("foo"), "baz"); + equal(simpleObjectAgain.get('foo'), 'baz'); done(); }, done.fail); }, done.fail); }, done.fail); }); - it("setting after deleting", function(done) { - const simpleObject = new Parse.Object("SimpleObject"); - simpleObject.set("foo", "bar"); - simpleObject.save().then(function(simpleObject) { - simpleObject.unset("foo"); - simpleObject.set("foo", "baz"); - simpleObject.save().then(function(simpleObject) { - const query = new Parse.Query("SimpleObject"); - query.get(simpleObject.id).then(function(simpleObjectAgain) { - equal(simpleObjectAgain.get("foo"), "baz"); - done(); - }, function(error) { - ok(false, "Error " + error.code + ": " + error.message); - done(); - }); - }, function(error) { - ok(false, "Error " + error.code + ": " + error.message); + it('setting after deleting', function(done) { + const simpleObject = new Parse.Object('SimpleObject'); + simpleObject.set('foo', 'bar'); + simpleObject.save().then( + function(simpleObject) { + simpleObject.unset('foo'); + simpleObject.set('foo', 'baz'); + simpleObject.save().then( + function(simpleObject) { + const query = new Parse.Query('SimpleObject'); + query.get(simpleObject.id).then( + function(simpleObjectAgain) { + equal(simpleObjectAgain.get('foo'), 'baz'); + done(); + }, + function(error) { + ok(false, 'Error ' + error.code + ': ' + error.message); + done(); + } + ); + }, + function(error) { + ok(false, 'Error ' + error.code + ': ' + error.message); + done(); + } + ); + }, + function(error) { + ok(false, 'Error ' + error.code + ': ' + error.message); done(); - }); - }, function(error) { - ok(false, "Error " + error.code + ": " + error.message); - done(); - }); - }); - - it("increment", function(done) { - const simple = new Parse.Object("SimpleObject"); - simple.save({ - foo: 5 - }).then(function(simple) { - simple.increment("foo"); - equal(simple.get("foo"), 6); - ok(simple.dirty("foo"), "foo should be dirty."); - ok(simple.dirty(), "the whole object should be dirty."); - simple.save().then(function(simple) { - equal(simple.get("foo"), 6); - ok(!simple.dirty("foo"), "the whole object was just saved."); - ok(!simple.dirty(), "the whole object was just saved."); - - const query = new Parse.Query("SimpleObject"); - query.get(simple.id).then(function(simpleAgain) { - equal(simpleAgain.get("foo"), 6); - done(); + } + ); + }); + + it('increment', function(done) { + const simple = new Parse.Object('SimpleObject'); + simple + .save({ + foo: 5, + }) + .then(function(simple) { + simple.increment('foo'); + equal(simple.get('foo'), 6); + ok(simple.dirty('foo'), 'foo should be dirty.'); + ok(simple.dirty(), 'the whole object should be dirty.'); + simple.save().then(function(simple) { + equal(simple.get('foo'), 6); + ok(!simple.dirty('foo'), 'the whole object was just saved.'); + ok(!simple.dirty(), 'the whole object was just saved.'); + + const query = new Parse.Query('SimpleObject'); + query.get(simple.id).then(function(simpleAgain) { + equal(simpleAgain.get('foo'), 6); + done(); + }); }); }); - }); }); - it("addUnique", function(done) { + it('addUnique', function(done) { const x1 = new Parse.Object('X'); x1.set('stuff', [1, 2]); - x1.save().then(() => { - const objectId = x1.id; - const x2 = new Parse.Object('X', {objectId: objectId}); - x2.addUnique('stuff', 2); - x2.addUnique('stuff', 4); - expect(x2.get('stuff')).toEqual([2, 4]); - return x2.save(); - }).then(() => { - const query = new Parse.Query('X'); - return query.get(x1.id); - }).then((x3) => { - const stuff = x3.get('stuff'); - const expected = [1, 2, 4]; - expect(stuff.length).toBe(expected.length); - for (const i of stuff) { - expect(expected.indexOf(i) >= 0).toBe(true); - } - done(); - }, (error) => { - on_db('mongo', () => { - jfail(error); - }); - on_db('postgres', () => { - expect(error.message).toEqual("Postgres does not support AddUnique operator."); - }); - done(); - }); + x1.save() + .then(() => { + const objectId = x1.id; + const x2 = new Parse.Object('X', { objectId: objectId }); + x2.addUnique('stuff', 2); + x2.addUnique('stuff', 4); + expect(x2.get('stuff')).toEqual([2, 4]); + return x2.save(); + }) + .then(() => { + const query = new Parse.Query('X'); + return query.get(x1.id); + }) + .then( + x3 => { + const stuff = x3.get('stuff'); + const expected = [1, 2, 4]; + expect(stuff.length).toBe(expected.length); + for (const i of stuff) { + expect(expected.indexOf(i) >= 0).toBe(true); + } + done(); + }, + error => { + on_db('mongo', () => { + jfail(error); + }); + on_db('postgres', () => { + expect(error.message).toEqual( + 'Postgres does not support AddUnique operator.' + ); + }); + done(); + } + ); }); - it("addUnique with object", function(done) { + it('addUnique with object', function(done) { const x1 = new Parse.Object('X'); - x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]); - x1.save().then(() => { - const objectId = x1.id; - const x2 = new Parse.Object('X', {objectId: objectId}); - x2.addUnique('stuff', {'hello': 'world'}); - x2.addUnique('stuff', {'bar': 'baz'}); - expect(x2.get('stuff')).toEqual([{'hello': 'world'}, {'bar': 'baz'}]); - return x2.save(); - }).then(() => { - const query = new Parse.Query('X'); - return query.get(x1.id); - }).then((x3) => { - const stuff = x3.get('stuff'); - const target = [1, {'hello': 'world'}, {'foo': 'bar'}, {'bar': 'baz'}]; - expect(stuff.length).toEqual(target.length); - let found = 0; - for (const thing in target) { - for (const st in stuff) { - if (st == thing) { - found++; + x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]); + x1.save() + .then(() => { + const objectId = x1.id; + const x2 = new Parse.Object('X', { objectId: objectId }); + x2.addUnique('stuff', { hello: 'world' }); + x2.addUnique('stuff', { bar: 'baz' }); + expect(x2.get('stuff')).toEqual([{ hello: 'world' }, { bar: 'baz' }]); + return x2.save(); + }) + .then(() => { + const query = new Parse.Query('X'); + return query.get(x1.id); + }) + .then( + x3 => { + const stuff = x3.get('stuff'); + const target = [ + 1, + { hello: 'world' }, + { foo: 'bar' }, + { bar: 'baz' }, + ]; + expect(stuff.length).toEqual(target.length); + let found = 0; + for (const thing in target) { + for (const st in stuff) { + if (st == thing) { + found++; + } + } } + expect(found).toBe(target.length); + done(); + }, + error => { + jfail(error); + done(); } - } - expect(found).toBe(target.length); - done(); - }, (error) => { - jfail(error); - done(); - }); + ); }); - it("removes with object", function(done) { + it('removes with object', function(done) { const x1 = new Parse.Object('X'); - x1.set('stuff', [ 1, {'hello': 'world'}, {'foo': 'bar'}]); - x1.save().then(() => { - const objectId = x1.id; - const x2 = new Parse.Object('X', {objectId: objectId}); - x2.remove('stuff', {'hello': 'world'}); - expect(x2.get('stuff')).toEqual([]); - return x2.save(); - }).then(() => { - const query = new Parse.Query('X'); - return query.get(x1.id); - }).then((x3) => { - expect(x3.get('stuff')).toEqual([1, {'foo': 'bar'}]); - done(); - }, (error) => { - jfail(error); - done(); - }); + x1.set('stuff', [1, { hello: 'world' }, { foo: 'bar' }]); + x1.save() + .then(() => { + const objectId = x1.id; + const x2 = new Parse.Object('X', { objectId: objectId }); + x2.remove('stuff', { hello: 'world' }); + expect(x2.get('stuff')).toEqual([]); + return x2.save(); + }) + .then(() => { + const query = new Parse.Query('X'); + return query.get(x1.id); + }) + .then( + x3 => { + expect(x3.get('stuff')).toEqual([1, { foo: 'bar' }]); + done(); + }, + error => { + jfail(error); + done(); + } + ); }); - it("dirty attributes", function(done) { - const object = new Parse.Object("TestObject"); - object.set("cat", "good"); - object.set("dog", "bad"); - object.save().then(function(object) { - ok(!object.dirty()); - ok(!object.dirty("cat")); - ok(!object.dirty("dog")); + it('dirty attributes', function(done) { + const object = new Parse.Object('TestObject'); + object.set('cat', 'good'); + object.set('dog', 'bad'); + object.save().then( + function(object) { + ok(!object.dirty()); + ok(!object.dirty('cat')); + ok(!object.dirty('dog')); - object.set("dog", "okay"); + object.set('dog', 'okay'); - ok(object.dirty()); - ok(!object.dirty("cat")); - ok(object.dirty("dog")); + ok(object.dirty()); + ok(!object.dirty('cat')); + ok(object.dirty('dog')); - done(); - }, function() { - ok(false, "This should have saved."); - done(); - }); + done(); + }, + function() { + ok(false, 'This should have saved.'); + done(); + } + ); }); - it("dirty keys", function(done) { - const object = new Parse.Object("TestObject"); - object.set("gogo", "good"); - object.set("sito", "sexy"); + it('dirty keys', function(done) { + const object = new Parse.Object('TestObject'); + object.set('gogo', 'good'); + object.set('sito', 'sexy'); ok(object.dirty()); let dirtyKeys = object.dirtyKeys(); equal(dirtyKeys.length, 2); - ok(arrayContains(dirtyKeys, "gogo")); - ok(arrayContains(dirtyKeys, "sito")); - - object.save().then(function(obj) { - ok(!obj.dirty()); - dirtyKeys = obj.dirtyKeys(); - equal(dirtyKeys.length, 0); - ok(!arrayContains(dirtyKeys, "gogo")); - ok(!arrayContains(dirtyKeys, "sito")); - - // try removing keys - obj.unset("sito"); - ok(obj.dirty()); - dirtyKeys = obj.dirtyKeys(); - equal(dirtyKeys.length, 1); - ok(!arrayContains(dirtyKeys, "gogo")); - ok(arrayContains(dirtyKeys, "sito")); - - return obj.save(); - }).then(function(obj) { - ok(!obj.dirty()); - equal(obj.get("gogo"), "good"); - equal(obj.get("sito"), undefined); - dirtyKeys = obj.dirtyKeys(); - equal(dirtyKeys.length, 0); - ok(!arrayContains(dirtyKeys, "gogo")); - ok(!arrayContains(dirtyKeys, "sito")); + ok(arrayContains(dirtyKeys, 'gogo')); + ok(arrayContains(dirtyKeys, 'sito')); + + object + .save() + .then(function(obj) { + ok(!obj.dirty()); + dirtyKeys = obj.dirtyKeys(); + equal(dirtyKeys.length, 0); + ok(!arrayContains(dirtyKeys, 'gogo')); + ok(!arrayContains(dirtyKeys, 'sito')); + + // try removing keys + obj.unset('sito'); + ok(obj.dirty()); + dirtyKeys = obj.dirtyKeys(); + equal(dirtyKeys.length, 1); + ok(!arrayContains(dirtyKeys, 'gogo')); + ok(arrayContains(dirtyKeys, 'sito')); + + return obj.save(); + }) + .then(function(obj) { + ok(!obj.dirty()); + equal(obj.get('gogo'), 'good'); + equal(obj.get('sito'), undefined); + dirtyKeys = obj.dirtyKeys(); + equal(dirtyKeys.length, 0); + ok(!arrayContains(dirtyKeys, 'gogo')); + ok(!arrayContains(dirtyKeys, 'sito')); - done(); - }); + done(); + }); }); - it("length attribute", function(done) { - Parse.User.signUp("bob", "password") - .then(function(user) { - const TestObject = Parse.Object.extend("TestObject"); - const obj = new TestObject({ - length: 5, - ACL: new Parse.ACL(user) // ACLs cause things like validation to run - }); - equal(obj.get("length"), 5); - ok(obj.get("ACL") instanceof Parse.ACL); + it('length attribute', function(done) { + Parse.User.signUp('bob', 'password').then(function(user) { + const TestObject = Parse.Object.extend('TestObject'); + const obj = new TestObject({ + length: 5, + ACL: new Parse.ACL(user), // ACLs cause things like validation to run + }); + equal(obj.get('length'), 5); + ok(obj.get('ACL') instanceof Parse.ACL); - obj.save().then(function(obj) { - equal(obj.get("length"), 5); - ok(obj.get("ACL") instanceof Parse.ACL); + obj.save().then(function(obj) { + equal(obj.get('length'), 5); + ok(obj.get('ACL') instanceof Parse.ACL); - const query = new Parse.Query(TestObject); - query.get(obj.id).then(function(obj) { - equal(obj.get("length"), 5); - ok(obj.get("ACL") instanceof Parse.ACL); + const query = new Parse.Query(TestObject); + query.get(obj.id).then(function(obj) { + equal(obj.get('length'), 5); + ok(obj.get('ACL') instanceof Parse.ACL); - const query = new Parse.Query(TestObject); - query.find().then(function(results) { - obj = results[0]; - equal(obj.get("length"), 5); - ok(obj.get("ACL") instanceof Parse.ACL); + const query = new Parse.Query(TestObject); + query.find().then(function(results) { + obj = results[0]; + equal(obj.get('length'), 5); + ok(obj.get('ACL') instanceof Parse.ACL); - done(); - }); + done(); }); }); }); + }); }); - it("old attribute unset then unset", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('old attribute unset then unset', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.set("x", 3); + obj.set('x', 3); obj.save().then(function() { - obj.unset("x"); - obj.unset("x"); + obj.unset('x'); + obj.unset('x'); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); }); - it("new attribute unset then unset", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('new attribute unset then unset', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.set("x", 5); - obj.unset("x"); - obj.unset("x"); + obj.set('x', 5); + obj.unset('x'); + obj.unset('x'); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); - it("unknown attribute unset then unset", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('unknown attribute unset then unset', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.unset("x"); - obj.unset("x"); + obj.unset('x'); + obj.unset('x'); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); - it("old attribute unset then clear", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('old attribute unset then clear', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.set("x", 3); + obj.set('x', 3); obj.save().then(function() { - obj.unset("x"); + obj.unset('x'); obj.clear(); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); }); - it("new attribute unset then clear", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('new attribute unset then clear', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.set("x", 5); - obj.unset("x"); + obj.set('x', 5); + obj.unset('x'); obj.clear(); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); - it("unknown attribute unset then clear", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('unknown attribute unset then clear', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.unset("x"); + obj.unset('x'); obj.clear(); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); - it("old attribute clear then unset", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('old attribute clear then unset', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.set("x", 3); + obj.set('x', 3); obj.save().then(function() { obj.clear(); - obj.unset("x"); + obj.unset('x'); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); }); - it("new attribute clear then unset", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('new attribute clear then unset', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.set("x", 5); + obj.set('x', 5); obj.clear(); - obj.unset("x"); + obj.unset('x'); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); - query.get(obj.id,).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + query.get(obj.id).then(function(objAgain) { + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); - it("unknown attribute clear then unset", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('unknown attribute clear then unset', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); obj.clear(); - obj.unset("x"); + obj.unset('x'); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); - it("old attribute clear then clear", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('old attribute clear then clear', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.set("x", 3); + obj.set('x', 3); obj.save().then(function() { obj.clear(); obj.clear(); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); }); - it("new attribute clear then clear", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('new attribute clear then clear', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.set("x", 5); + obj.set('x', 5); obj.clear(); obj.clear(); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); - it("unknown attribute clear then clear", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('unknown attribute clear then clear', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); obj.clear(); obj.clear(); obj.save().then(function() { - equal(obj.has("x"), false); - equal(obj.get("x"), undefined); + equal(obj.has('x'), false); + equal(obj.get('x'), undefined); const query = new Parse.Query(TestObject); query.get(obj.id).then(function(objAgain) { - equal(objAgain.has("x"), false); - equal(objAgain.get("x"), undefined); + equal(objAgain.has('x'), false); + equal(objAgain.get('x'), undefined); done(); }); }); }); - it("saving children in an array", function(done) { - const Parent = Parse.Object.extend("Parent"); - const Child = Parse.Object.extend("Child"); + it('saving children in an array', function(done) { + const Parent = Parse.Object.extend('Parent'); + const Child = Parse.Object.extend('Child'); const child1 = new Child(); const child2 = new Child(); @@ -891,9 +982,8 @@ describe('Parse.Object testing', () => { }, done.fail); }); - it("two saves at the same time", function(done) { - - const object = new Parse.Object("TestObject"); + it('two saves at the same time', function(done) { + const object = new Parse.Object('TestObject'); let firstSave = true; const success = function() { @@ -902,17 +992,17 @@ describe('Parse.Object testing', () => { return; } - const query = new Parse.Query("TestObject"); + const query = new Parse.Query('TestObject'); query.find().then(function(results) { equal(results.length, 1); - equal(results[0].get("cat"), "meow"); - equal(results[0].get("dog"), "bark"); + equal(results[0].get('cat'), 'meow'); + equal(results[0].get('dog'), 'bark'); done(); }); }; - object.save({ cat: "meow" }).then(success, fail); - object.save({ dog: "bark" }).then(success, fail); + object.save({ cat: 'meow' }).then(success, fail); + object.save({ dog: 'bark' }).then(success, fail); }); // The schema-checking parts of this are working. @@ -925,66 +1015,74 @@ describe('Parse.Object testing', () => { const o1 = new Parse.Object('TestObject'); o1.set('number', 1); let object = null; - o1.save().then(() => { - object = new Parse.Object('TestObject'); - object.set('number', 'two'); - return object.save(); - }).then(fail, (error) => { - expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); - - object.set('other', 'foo'); - return object.save(); - }).then(fail, (error) => { - expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); - - object.set('other', 'bar'); - return object.save(); - }).then(fail, (error) => { - expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); + o1.save() + .then(() => { + object = new Parse.Object('TestObject'); + object.set('number', 'two'); + return object.save(); + }) + .then(fail, error => { + expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); + + object.set('other', 'foo'); + return object.save(); + }) + .then(fail, error => { + expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); + + object.set('other', 'bar'); + return object.save(); + }) + .then(fail, error => { + expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); - done(); - }); + done(); + }); }); - it("is not dirty after save", function(done) { - const obj = new Parse.Object("TestObject"); + it('is not dirty after save', function(done) { + const obj = new Parse.Object('TestObject'); obj.save().then(function() { - obj.set({ "content": "x" }); - obj.fetch().then(function(){ - equal(false, obj.dirty("content")); + obj.set({ content: 'x' }); + obj.fetch().then(function() { + equal(false, obj.dirty('content')); done(); }); }); }); - it("add with an object", function(done) { - const child = new Parse.Object("Person"); - const parent = new Parse.Object("Person"); - - Promise.resolve().then(function() { - return child.save(); - - }).then(function() { - parent.add("children", child); - return parent.save(); - - }).then(function() { - const query = new Parse.Query("Person"); - return query.get(parent.id); - - }).then(function(parentAgain) { - equal(parentAgain.get("children")[0].id, child.id); - - }).then(function() { - done(); - }, function(error) { - ok(false, error); - done(); - }); + it('add with an object', function(done) { + const child = new Parse.Object('Person'); + const parent = new Parse.Object('Person'); + + Promise.resolve() + .then(function() { + return child.save(); + }) + .then(function() { + parent.add('children', child); + return parent.save(); + }) + .then(function() { + const query = new Parse.Query('Person'); + return query.get(parent.id); + }) + .then(function(parentAgain) { + equal(parentAgain.get('children')[0].id, child.id); + }) + .then( + function() { + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it("toJSON saved object", function(done) { - create({ "foo" : "bar" }, function(model) { + it('toJSON saved object', function(done) { + create({ foo: 'bar' }, function(model) { const objJSON = model.toJSON(); ok(objJSON.foo, "expected json to contain key 'foo'"); ok(objJSON.objectId, "expected json to contain key 'objectId'"); @@ -994,74 +1092,84 @@ describe('Parse.Object testing', () => { }); }); - it("remove object from array", function(done) { + it('remove object from array', function(done) { const obj = new TestObject(); obj.save().then(function() { const container = new TestObject(); - container.add("array", obj); - equal(container.get("array").length, 1); + container.add('array', obj); + equal(container.get('array').length, 1); container.save(null).then(function() { const objAgain = new TestObject(); objAgain.id = obj.id; - container.remove("array", objAgain); - equal(container.get("array").length, 0); + container.remove('array', objAgain); + equal(container.get('array').length, 0); done(); }); }); }); - it("async methods", function(done) { + it('async methods', function(done) { const obj = new TestObject(); - obj.set("time", "adventure"); - - obj.save().then(function(obj) { - ok(obj.id, "objectId should not be null."); - const objAgain = new TestObject(); - objAgain.id = obj.id; - return objAgain.fetch(); - - }).then(function(objAgain) { - equal(objAgain.get("time"), "adventure"); - return objAgain.destroy(); - - }).then(function() { - const query = new Parse.Query(TestObject); - return query.find(); + obj.set('time', 'adventure'); - }).then(function(results) { - equal(results.length, 0); - - }).then(function() { - done(); - - }); + obj + .save() + .then(function(obj) { + ok(obj.id, 'objectId should not be null.'); + const objAgain = new TestObject(); + objAgain.id = obj.id; + return objAgain.fetch(); + }) + .then(function(objAgain) { + equal(objAgain.get('time'), 'adventure'); + return objAgain.destroy(); + }) + .then(function() { + const query = new Parse.Query(TestObject); + return query.find(); + }) + .then(function(results) { + equal(results.length, 0); + }) + .then(function() { + done(); + }); }); - it("fail validation with promise", function(done) { - const PickyEater = Parse.Object.extend("PickyEater", { + it('fail validation with promise', function(done) { + const PickyEater = Parse.Object.extend('PickyEater', { validate: function(attrs) { - if (attrs.meal === "tomatoes") { - return "Ew. Tomatoes are gross."; + if (attrs.meal === 'tomatoes') { + return 'Ew. Tomatoes are gross.'; } return Parse.Object.prototype.validate.apply(this, arguments); - } + }, }); const bryan = new PickyEater(); - bryan.save({ - meal: "burrito" - }).then(function() { - return bryan.save({ - meal: "tomatoes" - }); - }, function() { - ok(false, "Save should have succeeded."); - }).then(function() { - ok(false, "Save should have failed."); - }, function(error) { - equal(error, "Ew. Tomatoes are gross."); - done(); - }); + bryan + .save({ + meal: 'burrito', + }) + .then( + function() { + return bryan.save({ + meal: 'tomatoes', + }); + }, + function() { + ok(false, 'Save should have succeeded.'); + } + ) + .then( + function() { + ok(false, 'Save should have failed.'); + }, + function(error) { + equal(error, 'Ew. Tomatoes are gross.'); + done(); + } + ); }); it("beforeSave doesn't make object dirty with new field", function(done) { @@ -1069,20 +1177,22 @@ describe('Parse.Object testing', () => { const r = restController.request; restController.request = function() { return r.apply(this, arguments).then(function(result) { - result.aDate = {"__type":"Date", "iso":"2014-06-24T06:06:06.452Z"}; + result.aDate = { __type: 'Date', iso: '2014-06-24T06:06:06.452Z' }; return result; }); }; - const obj = new Parse.Object("Thing"); - obj.save().then(function() { - ok(!obj.dirty(), "The object should not be dirty"); - ok(obj.get('aDate')); - - }).finally(function() { - restController.request = r; - done(); - }); + const obj = new Parse.Object('Thing'); + obj + .save() + .then(function() { + ok(!obj.dirty(), 'The object should not be dirty'); + ok(obj.get('aDate')); + }) + .finally(function() { + restController.request = r; + done(); + }); }); xit("beforeSave doesn't make object dirty with existing field", function(done) { @@ -1090,194 +1200,228 @@ describe('Parse.Object testing', () => { const r = restController.request; restController.request = function() { return r.apply(restController, arguments).then(function(result) { - result.aDate = {"__type":"Date", "iso":"2014-06-24T06:06:06.452Z"}; + result.aDate = { __type: 'Date', iso: '2014-06-24T06:06:06.452Z' }; return result; }); }; const now = new Date(); - const obj = new Parse.Object("Thing"); + const obj = new Parse.Object('Thing'); const promise = obj.save(); obj.set('aDate', now); - promise.then(function() { - ok(obj.dirty(), "The object should be dirty"); - equal(now, obj.get('aDate')); - }).finally(function() { - restController.request = r; - done(); - }); + promise + .then(function() { + ok(obj.dirty(), 'The object should be dirty'); + equal(now, obj.get('aDate')); + }) + .finally(function() { + restController.request = r; + done(); + }); }); - it("bytes work", function(done) { - Promise.resolve().then(function() { - const obj = new TestObject(); - obj.set("bytes", { __type: "Bytes", base64: "ZnJveW8=" }); - return obj.save(); - - }).then(function(obj) { - const query = new Parse.Query(TestObject); - return query.get(obj.id); - - }).then(function(obj) { - equal(obj.get("bytes").__type, "Bytes"); - equal(obj.get("bytes").base64, "ZnJveW8="); - done(); - - }, function(error) { - ok(false, JSON.stringify(error)); - done(); - - }); + it('bytes work', function(done) { + Promise.resolve() + .then(function() { + const obj = new TestObject(); + obj.set('bytes', { __type: 'Bytes', base64: 'ZnJveW8=' }); + return obj.save(); + }) + .then(function(obj) { + const query = new Parse.Query(TestObject); + return query.get(obj.id); + }) + .then( + function(obj) { + equal(obj.get('bytes').__type, 'Bytes'); + equal(obj.get('bytes').base64, 'ZnJveW8='); + done(); + }, + function(error) { + ok(false, JSON.stringify(error)); + done(); + } + ); }); - it("destroyAll no objects", function(done) { - Parse.Object.destroyAll([]).then(function(success) { - ok(success, "Should be able to destroy no objects"); - done(); - }).catch(done.fail); + it('destroyAll no objects', function(done) { + Parse.Object.destroyAll([]) + .then(function(success) { + ok(success, 'Should be able to destroy no objects'); + done(); + }) + .catch(done.fail); }); - it("destroyAll new objects only", function(done) { + it('destroyAll new objects only', function(done) { const objects = [new TestObject(), new TestObject()]; - Parse.Object.destroyAll(objects).then(function(success) { - ok(success, "Should be able to destroy only new objects"); - done(); - }).catch(done.fail); + Parse.Object.destroyAll(objects) + .then(function(success) { + ok(success, 'Should be able to destroy only new objects'); + done(); + }) + .catch(done.fail); }); - it("fetchAll", function(done) { + it('fetchAll', function(done) { const numItems = 11; const container = new Container(); const items = []; - for (let i = 0; i < numItems; i++) { + for (let i = 0; i < numItems; i++) { const item = new Item(); - item.set("x", i); + item.set('x', i); items.push(item); } - Parse.Object.saveAll(items).then(function() { - container.set("items", items); - return container.save(); - }).then(function() { - const query = new Parse.Query(Container); - return query.get(container.id); - }).then(function(containerAgain) { - const itemsAgain = containerAgain.get("items"); - if (!itemsAgain || !itemsAgain.forEach) { - fail('no itemsAgain retrieved', itemsAgain); + Parse.Object.saveAll(items) + .then(function() { + container.set('items', items); + return container.save(); + }) + .then(function() { + const query = new Parse.Query(Container); + return query.get(container.id); + }) + .then(function(containerAgain) { + const itemsAgain = containerAgain.get('items'); + if (!itemsAgain || !itemsAgain.forEach) { + fail('no itemsAgain retrieved', itemsAgain); + done(); + return; + } + equal(itemsAgain.length, numItems, 'Should get the array back'); + itemsAgain.forEach(function(item, i) { + const newValue = i * 2; + item.set('x', newValue); + }); + return Parse.Object.saveAll(itemsAgain); + }) + .then(function() { + return Parse.Object.fetchAll(items); + }) + .then(function(fetchedItemsAgain) { + equal( + fetchedItemsAgain.length, + numItems, + 'Number of items fetched should not change' + ); + fetchedItemsAgain.forEach(function(item, i) { + equal(item.get('x'), i * 2); + }); done(); - return; - } - equal(itemsAgain.length, numItems, "Should get the array back"); - itemsAgain.forEach(function(item, i) { - const newValue = i * 2; - item.set("x", newValue); - }); - return Parse.Object.saveAll(itemsAgain); - }).then(function() { - return Parse.Object.fetchAll(items); - }).then(function(fetchedItemsAgain) { - equal(fetchedItemsAgain.length, numItems, - "Number of items fetched should not change"); - fetchedItemsAgain.forEach(function(item, i) { - equal(item.get("x"), i * 2); }); - done(); - }); }); - it("fetchAll no objects", function(done) { - Parse.Object.fetchAll([]).then(function(success) { - ok(Array.isArray(success), "Should be able to fetchAll no objects"); - done(); - }).catch(done.fail); + it('fetchAll no objects', function(done) { + Parse.Object.fetchAll([]) + .then(function(success) { + ok(Array.isArray(success), 'Should be able to fetchAll no objects'); + done(); + }) + .catch(done.fail); }); - it("fetchAll updates dates", function(done) { + it('fetchAll updates dates', function(done) { let updatedObject; const object = new TestObject(); - object.set("x", 7); - object.save().then(function() { - const query = new Parse.Query(TestObject); - return query.find(object.id); - }).then(function(results) { - updatedObject = results[0]; - updatedObject.set("x", 11); - return updatedObject.save(); - }).then(function() { - return Parse.Object.fetchAll([object]); - }).then(function() { - equal(object.createdAt.getTime(), updatedObject.createdAt.getTime()); - equal(object.updatedAt.getTime(), updatedObject.updatedAt.getTime()); - done(); - }); + object.set('x', 7); + object + .save() + .then(function() { + const query = new Parse.Query(TestObject); + return query.find(object.id); + }) + .then(function(results) { + updatedObject = results[0]; + updatedObject.set('x', 11); + return updatedObject.save(); + }) + .then(function() { + return Parse.Object.fetchAll([object]); + }) + .then(function() { + equal(object.createdAt.getTime(), updatedObject.createdAt.getTime()); + equal(object.updatedAt.getTime(), updatedObject.updatedAt.getTime()); + done(); + }); }); - xit("fetchAll backbone-style callbacks", function(done) { + xit('fetchAll backbone-style callbacks', function(done) { const numItems = 11; const container = new Container(); const items = []; - for (let i = 0; i < numItems; i++) { + for (let i = 0; i < numItems; i++) { const item = new Item(); - item.set("x", i); + item.set('x', i); items.push(item); } - Parse.Object.saveAll(items).then(function() { - container.set("items", items); - return container.save(); - }).then(function() { - const query = new Parse.Query(Container); - return query.get(container.id); - }).then(function(containerAgain) { - const itemsAgain = containerAgain.get("items"); - if (!itemsAgain || !itemsAgain.forEach) { - fail('no itemsAgain retrieved', itemsAgain); - done(); - return; - } - equal(itemsAgain.length, numItems, "Should get the array back"); - itemsAgain.forEach(function(item, i) { - const newValue = i * 2; - item.set("x", newValue); - }); - return Parse.Object.saveAll(itemsAgain); - }).then(function() { - return Parse.Object.fetchAll(items) - .then(function(fetchedItemsAgain) { - equal(fetchedItemsAgain.length, numItems, - "Number of items fetched should not change"); - fetchedItemsAgain.forEach(function(item, i) { - equal(item.get("x"), i * 2); - }); - done(); - }, function() { - ok(false, "Failed to fetchAll"); + Parse.Object.saveAll(items) + .then(function() { + container.set('items', items); + return container.save(); + }) + .then(function() { + const query = new Parse.Query(Container); + return query.get(container.id); + }) + .then(function(containerAgain) { + const itemsAgain = containerAgain.get('items'); + if (!itemsAgain || !itemsAgain.forEach) { + fail('no itemsAgain retrieved', itemsAgain); done(); + return; + } + equal(itemsAgain.length, numItems, 'Should get the array back'); + itemsAgain.forEach(function(item, i) { + const newValue = i * 2; + item.set('x', newValue); }); - }); + return Parse.Object.saveAll(itemsAgain); + }) + .then(function() { + return Parse.Object.fetchAll(items).then( + function(fetchedItemsAgain) { + equal( + fetchedItemsAgain.length, + numItems, + 'Number of items fetched should not change' + ); + fetchedItemsAgain.forEach(function(item, i) { + equal(item.get('x'), i * 2); + }); + done(); + }, + function() { + ok(false, 'Failed to fetchAll'); + done(); + } + ); + }); }); - it("fetchAll error on multiple classes", function(done) { + it('fetchAll error on multiple classes', function(done) { const container = new Container(); - container.set("item", new Item()); - container.set("subcontainer", new Container()); - return container.save().then(function() { - const query = new Parse.Query(Container); - return query.get(container.id); - }).then(function(containerAgain) { - const subContainerAgain = containerAgain.get("subcontainer"); - const itemAgain = containerAgain.get("item"); - const multiClassArray = [subContainerAgain, itemAgain]; - return Parse.Object.fetchAll(multiClassArray) - .catch(e => { + container.set('item', new Item()); + container.set('subcontainer', new Container()); + return container + .save() + .then(function() { + const query = new Parse.Query(Container); + return query.get(container.id); + }) + .then(function(containerAgain) { + const subContainerAgain = containerAgain.get('subcontainer'); + const itemAgain = containerAgain.get('item'); + const multiClassArray = [subContainerAgain, itemAgain]; + return Parse.Object.fetchAll(multiClassArray).catch(e => { expect(e.code).toBe(Parse.Error.INVALID_CLASS_NAME); done(); }); - }); + }); }); - it("fetchAll error on unsaved object", async function(done) { + it('fetchAll error on unsaved object', async function(done) { const unsavedObjectArray = [new TestObject()]; await Parse.Object.fetchAll(unsavedObjectArray).catch(e => { expect(e.code).toBe(Parse.Error.MISSING_OBJECT_ID); @@ -1285,307 +1429,374 @@ describe('Parse.Object testing', () => { }); }); - it("fetchAll error on deleted object", function(done) { + it('fetchAll error on deleted object', function(done) { const numItems = 11; const items = []; for (let i = 0; i < numItems; i++) { const item = new Item(); - item.set("x", i); + item.set('x', i); items.push(item); } - Parse.Object.saveAll(items).then(function() { - const query = new Parse.Query(Item); - return query.get(items[0].id); - }).then(function(objectToDelete) { - return objectToDelete.destroy(); - }).then(function(deletedObject) { - const nonExistentObject = new Item({ objectId: deletedObject.id }); - const nonExistentObjectArray = [nonExistentObject, items[1]]; - return Parse.Object.fetchAll(nonExistentObjectArray).catch(e => { - expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); - done(); + Parse.Object.saveAll(items) + .then(function() { + const query = new Parse.Query(Item); + return query.get(items[0].id); + }) + .then(function(objectToDelete) { + return objectToDelete.destroy(); + }) + .then(function(deletedObject) { + const nonExistentObject = new Item({ objectId: deletedObject.id }); + const nonExistentObjectArray = [nonExistentObject, items[1]]; + return Parse.Object.fetchAll(nonExistentObjectArray).catch(e => { + expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + done(); + }); }); - }); }); // TODO: Verify that with Sessions, this test is wrong... A fetch on // user should not bring down a session token. - xit("fetchAll User attributes get merged", function(done) { + xit('fetchAll User attributes get merged', function(done) { let sameUser; let user = new Parse.User(); - user.set("username", "asdf"); - user.set("password", "zxcv"); - user.set("foo", "bar"); - user.signUp().then(function() { - Parse.User.logOut(); - const query = new Parse.Query(Parse.User); - return query.get(user.id); - }).then(function(userAgain) { - user = userAgain; - sameUser = new Parse.User(); - sameUser.set("username", "asdf"); - sameUser.set("password", "zxcv"); - return sameUser.logIn(); - }).then(function() { - ok(!user.getSessionToken(), "user should not have a sessionToken"); - ok(sameUser.getSessionToken(), "sameUser should have a sessionToken"); - sameUser.set("baz", "qux"); - return sameUser.save(); - }).then(function() { - return Parse.Object.fetchAll([user]); - }).then(function() { - equal(user.getSessionToken(), sameUser.getSessionToken()); - equal(user.createdAt.getTime(), sameUser.createdAt.getTime()); - equal(user.updatedAt.getTime(), sameUser.updatedAt.getTime()); - Parse.User.logOut(); - done(); - }); + user.set('username', 'asdf'); + user.set('password', 'zxcv'); + user.set('foo', 'bar'); + user + .signUp() + .then(function() { + Parse.User.logOut(); + const query = new Parse.Query(Parse.User); + return query.get(user.id); + }) + .then(function(userAgain) { + user = userAgain; + sameUser = new Parse.User(); + sameUser.set('username', 'asdf'); + sameUser.set('password', 'zxcv'); + return sameUser.logIn(); + }) + .then(function() { + ok(!user.getSessionToken(), 'user should not have a sessionToken'); + ok(sameUser.getSessionToken(), 'sameUser should have a sessionToken'); + sameUser.set('baz', 'qux'); + return sameUser.save(); + }) + .then(function() { + return Parse.Object.fetchAll([user]); + }) + .then(function() { + equal(user.getSessionToken(), sameUser.getSessionToken()); + equal(user.createdAt.getTime(), sameUser.createdAt.getTime()); + equal(user.updatedAt.getTime(), sameUser.updatedAt.getTime()); + Parse.User.logOut(); + done(); + }); }); - it("fetchAllIfNeeded", function(done) { + it('fetchAllIfNeeded', function(done) { const numItems = 11; const container = new Container(); const items = []; for (let i = 0; i < numItems; i++) { const item = new Item(); - item.set("x", i); + item.set('x', i); items.push(item); } - Parse.Object.saveAll(items).then(function() { - container.set("items", items); - return container.save(); - }).then(function() { - const query = new Parse.Query(Container); - return query.get(container.id); - }).then(function(containerAgain) { - const itemsAgain = containerAgain.get("items"); - if (!itemsAgain || !itemsAgain.forEach) { - fail('no itemsAgain retrieved', itemsAgain); + Parse.Object.saveAll(items) + .then(function() { + container.set('items', items); + return container.save(); + }) + .then(function() { + const query = new Parse.Query(Container); + return query.get(container.id); + }) + .then(function(containerAgain) { + const itemsAgain = containerAgain.get('items'); + if (!itemsAgain || !itemsAgain.forEach) { + fail('no itemsAgain retrieved', itemsAgain); + done(); + return; + } + itemsAgain.forEach(function(item, i) { + item.set('x', i * 2); + }); + return Parse.Object.saveAll(itemsAgain); + }) + .then(function() { + return Parse.Object.fetchAllIfNeeded(items); + }) + .then(function(fetchedItems) { + equal( + fetchedItems.length, + numItems, + 'Number of items should not change' + ); + fetchedItems.forEach(function(item, i) { + equal(item.get('x'), i); + }); done(); - return; - } - itemsAgain.forEach(function(item, i) { - item.set("x", i * 2); - }); - return Parse.Object.saveAll(itemsAgain); - }).then(function() { - return Parse.Object.fetchAllIfNeeded(items); - }).then(function(fetchedItems) { - equal(fetchedItems.length, numItems, - "Number of items should not change"); - fetchedItems.forEach(function(item, i) { - equal(item.get("x"), i); }); - done(); - }); }); - xit("fetchAllIfNeeded backbone-style callbacks", function(done) { + xit('fetchAllIfNeeded backbone-style callbacks', function(done) { const numItems = 11; const container = new Container(); const items = []; for (let i = 0; i < numItems; i++) { const item = new Item(); - item.set("x", i); + item.set('x', i); items.push(item); } - Parse.Object.saveAll(items).then(function() { - container.set("items", items); - return container.save(); - }).then(function() { - const query = new Parse.Query(Container); - return query.get(container.id); - }).then(function(containerAgain) { - const itemsAgain = containerAgain.get("items"); - if (!itemsAgain || !itemsAgain.forEach) { - fail('no itemsAgain retrieved', itemsAgain); - done(); - return; - } - itemsAgain.forEach(function(item, i) { - item.set("x", i * 2); - }); - return Parse.Object.saveAll(itemsAgain); - }).then(function() { - const items = container.get("items"); - return Parse.Object.fetchAllIfNeeded(items) - .then(function(fetchedItems) { - equal(fetchedItems.length, numItems, - "Number of items should not change"); - fetchedItems.forEach(function(item, j) { - equal(item.get("x"), j); - }); - done(); - }, function() { - ok(false, "Failed to fetchAll"); + Parse.Object.saveAll(items) + .then(function() { + container.set('items', items); + return container.save(); + }) + .then(function() { + const query = new Parse.Query(Container); + return query.get(container.id); + }) + .then(function(containerAgain) { + const itemsAgain = containerAgain.get('items'); + if (!itemsAgain || !itemsAgain.forEach) { + fail('no itemsAgain retrieved', itemsAgain); done(); + return; + } + itemsAgain.forEach(function(item, i) { + item.set('x', i * 2); }); - }); + return Parse.Object.saveAll(itemsAgain); + }) + .then(function() { + const items = container.get('items'); + return Parse.Object.fetchAllIfNeeded(items).then( + function(fetchedItems) { + equal( + fetchedItems.length, + numItems, + 'Number of items should not change' + ); + fetchedItems.forEach(function(item, j) { + equal(item.get('x'), j); + }); + done(); + }, + function() { + ok(false, 'Failed to fetchAll'); + done(); + } + ); + }); }); - it("fetchAllIfNeeded no objects", function(done) { - Parse.Object.fetchAllIfNeeded([]).then(function(success) { - ok(Array.isArray(success), "Should be able to fetchAll no objects"); - done(); - }).catch(done.fail); + it('fetchAllIfNeeded no objects', function(done) { + Parse.Object.fetchAllIfNeeded([]) + .then(function(success) { + ok(Array.isArray(success), 'Should be able to fetchAll no objects'); + done(); + }) + .catch(done.fail); }); - it("fetchAllIfNeeded unsaved object", async function(done) { + it('fetchAllIfNeeded unsaved object', async function(done) { const unsavedObjectArray = [new TestObject()]; - await Parse.Object.fetchAllIfNeeded(unsavedObjectArray) - .catch(e => { - expect(e.code).toBe(Parse.Error.MISSING_OBJECT_ID); - done(); - }); + await Parse.Object.fetchAllIfNeeded(unsavedObjectArray).catch(e => { + expect(e.code).toBe(Parse.Error.MISSING_OBJECT_ID); + done(); + }); }); - it("fetchAllIfNeeded error on multiple classes", function(done) { + it('fetchAllIfNeeded error on multiple classes', function(done) { const container = new Container(); - container.set("item", new Item()); - container.set("subcontainer", new Container()); - return container.save().then(function() { - const query = new Parse.Query(Container); - return query.get(container.id); - }).then(function(containerAgain) { - const subContainerAgain = containerAgain.get("subcontainer"); - const itemAgain = containerAgain.get("item"); - const multiClassArray = [subContainerAgain, itemAgain]; - return Parse.Object.fetchAllIfNeeded( - multiClassArray) - .catch(e => { + container.set('item', new Item()); + container.set('subcontainer', new Container()); + return container + .save() + .then(function() { + const query = new Parse.Query(Container); + return query.get(container.id); + }) + .then(function(containerAgain) { + const subContainerAgain = containerAgain.get('subcontainer'); + const itemAgain = containerAgain.get('item'); + const multiClassArray = [subContainerAgain, itemAgain]; + return Parse.Object.fetchAllIfNeeded(multiClassArray).catch(e => { expect(e.code).toBe(Parse.Error.INVALID_CLASS_NAME); done(); }); - }); + }); }); - it("Objects with className User", function(done) { + it('Objects with className User', function(done) { equal(Parse.CoreManager.get('PERFORM_USER_REWRITE'), true); const User1 = Parse.Object.extend({ - className: "User" + className: 'User', }); - equal(User1.className, "_User", - "className is rewritten by default"); + equal(User1.className, '_User', 'className is rewritten by default'); Parse.User.allowCustomUserClass(true); equal(Parse.CoreManager.get('PERFORM_USER_REWRITE'), false); const User2 = Parse.Object.extend({ - className: "User" + className: 'User', }); - equal(User2.className, "User", - "className is not rewritten when allowCustomUserClass(true)"); + equal( + User2.className, + 'User', + 'className is not rewritten when allowCustomUserClass(true)' + ); // Set back to default so as not to break other tests. Parse.User.allowCustomUserClass(false); - equal(Parse.CoreManager.get('PERFORM_USER_REWRITE'), true, "PERFORM_USER_REWRITE is reset"); + equal( + Parse.CoreManager.get('PERFORM_USER_REWRITE'), + true, + 'PERFORM_USER_REWRITE is reset' + ); const user = new User2(); - user.set("name", "Me"); - user.save({height: 181}).then(function(user) { - equal(user.get("name"), "Me"); - equal(user.get("height"), 181); + user.set('name', 'Me'); + user.save({ height: 181 }).then(function(user) { + equal(user.get('name'), 'Me'); + equal(user.get('height'), 181); const query = new Parse.Query(User2); query.get(user.id).then(function(user) { - equal(user.className, "User"); - equal(user.get("name"), "Me"); - equal(user.get("height"), 181); + equal(user.className, 'User'); + equal(user.get('name'), 'Me'); + equal(user.get('height'), 181); done(); }); }); }); - it("create without data", function(done) { - const t1 = new TestObject({ "test" : "test" }); - t1.save().then(function(t1) { - const t2 = TestObject.createWithoutData(t1.id); - return t2.fetch(); - }).then(function(t2) { - equal(t2.get("test"), "test", "Fetch should have grabbed " + - "'test' property."); - const t3 = TestObject.createWithoutData(t2.id); - t3.set("test", "not test"); - return t3.fetch(); - }).then(function(t3) { - equal(t3.get("test"), "test", - "Fetch should have grabbed server 'test' property."); - done(); - }, function(error) { - ok(false, error); - done(); - }); + it('create without data', function(done) { + const t1 = new TestObject({ test: 'test' }); + t1.save() + .then(function(t1) { + const t2 = TestObject.createWithoutData(t1.id); + return t2.fetch(); + }) + .then(function(t2) { + equal( + t2.get('test'), + 'test', + 'Fetch should have grabbed ' + "'test' property." + ); + const t3 = TestObject.createWithoutData(t2.id); + t3.set('test', 'not test'); + return t3.fetch(); + }) + .then( + function(t3) { + equal( + t3.get('test'), + 'test', + "Fetch should have grabbed server 'test' property." + ); + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it("remove from new field creates array key", (done) => { + it('remove from new field creates array key', done => { const obj = new TestObject(); obj.remove('shouldBeArray', 'foo'); - obj.save().then(() => { - const query = new Parse.Query('TestObject'); - return query.get(obj.id); - }).then((objAgain) => { - const arr = objAgain.get('shouldBeArray'); - ok(Array.isArray(arr), 'Should have created array key'); - ok(!arr || arr.length === 0, 'Should have an empty array.'); - done(); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('TestObject'); + return query.get(obj.id); + }) + .then(objAgain => { + const arr = objAgain.get('shouldBeArray'); + ok(Array.isArray(arr), 'Should have created array key'); + ok(!arr || arr.length === 0, 'Should have an empty array.'); + done(); + }); }); - it("increment with type conflict fails", (done) => { + it('increment with type conflict fails', done => { const obj = new TestObject(); obj.set('astring', 'foo'); - obj.save().then(() => { - const obj2 = new TestObject(); - obj2.increment('astring'); - return obj2.save(); - }).then(() => { - fail('Should not have saved.'); - done(); - }, (error) => { - expect(error.code).toEqual(111); - done(); - }); + obj + .save() + .then(() => { + const obj2 = new TestObject(); + obj2.increment('astring'); + return obj2.save(); + }) + .then( + () => { + fail('Should not have saved.'); + done(); + }, + error => { + expect(error.code).toEqual(111); + done(); + } + ); }); - it("increment with empty field solidifies type", (done) => { + it('increment with empty field solidifies type', done => { const obj = new TestObject(); obj.increment('aninc'); - obj.save().then(() => { - const obj2 = new TestObject(); - obj2.set('aninc', 'foo'); - return obj2.save(); - }).then(() => { - fail('Should not have saved.'); - done(); - }, (error) => { - expect(error.code).toEqual(111); - done(); - }); + obj + .save() + .then(() => { + const obj2 = new TestObject(); + obj2.set('aninc', 'foo'); + return obj2.save(); + }) + .then( + () => { + fail('Should not have saved.'); + done(); + }, + error => { + expect(error.code).toEqual(111); + done(); + } + ); }); - it("increment update with type conflict fails", (done) => { + it('increment update with type conflict fails', done => { const obj = new TestObject(); obj.set('someString', 'foo'); - obj.save().then((objAgain) => { - const obj2 = new TestObject(); - obj2.id = objAgain.id; - obj2.increment('someString'); - return obj2.save(); - }).then(() => { - fail('Should not have saved.'); - done(); - }, (error) => { - expect(error.code).toEqual(111); - done(); - }); + obj + .save() + .then(objAgain => { + const obj2 = new TestObject(); + obj2.id = objAgain.id; + obj2.increment('someString'); + return obj2.save(); + }) + .then( + () => { + fail('Should not have saved.'); + done(); + }, + error => { + expect(error.code).toEqual(111); + done(); + } + ); }); - it('dictionary fetched pointers do not lose data on fetch', (done) => { + it('dictionary fetched pointers do not lose data on fetch', done => { const parent = new Parse.Object('Parent'); const dict = {}; - for (let i = 0; i < 5; i++) { - const proc = (iter) => { + for (let i = 0; i < 5; i++) { + const proc = iter => { const child = new Parse.Object('Child'); child.set('name', 'testname' + i); dict[iter] = child; @@ -1593,230 +1804,267 @@ describe('Parse.Object testing', () => { proc(i); } parent.set('childDict', dict); - parent.save().then(() => { - return parent.fetch(); - }).then((parentAgain) => { - const dictAgain = parentAgain.get('childDict'); - if (!dictAgain) { - fail('Should have been a dictionary.'); - return done(); - } - expect(typeof dictAgain).toEqual('object'); - expect(typeof dictAgain['0']).toEqual('object'); - expect(typeof dictAgain['1']).toEqual('object'); - expect(typeof dictAgain['2']).toEqual('object'); - expect(typeof dictAgain['3']).toEqual('object'); - expect(typeof dictAgain['4']).toEqual('object'); - done(); - }); + parent + .save() + .then(() => { + return parent.fetch(); + }) + .then(parentAgain => { + const dictAgain = parentAgain.get('childDict'); + if (!dictAgain) { + fail('Should have been a dictionary.'); + return done(); + } + expect(typeof dictAgain).toEqual('object'); + expect(typeof dictAgain['0']).toEqual('object'); + expect(typeof dictAgain['1']).toEqual('object'); + expect(typeof dictAgain['2']).toEqual('object'); + expect(typeof dictAgain['3']).toEqual('object'); + expect(typeof dictAgain['4']).toEqual('object'); + done(); + }); }); - - it("should create nested keys with _", done => { - const object = new Parse.Object("AnObject"); - object.set("foo", { - "_bar": "_", - "baz_bar": 1, - "__foo_bar": true, - "_0": "underscore_zero", - "_more": { - "_nested": "key" - } - }); - object.save().then(res => { - ok(res); - return res.fetch(); - }).then(res => { - const foo = res.get("foo"); - expect(foo["_bar"]).toEqual("_"); - expect(foo["baz_bar"]).toEqual(1); - expect(foo["__foo_bar"]).toBe(true); - expect(foo["_0"]).toEqual("underscore_zero"); - expect(foo["_more"]["_nested"]).toEqual("key"); - done(); - }).catch(err => { - jfail(err); - fail("should not fail"); - done(); - }); + it('should create nested keys with _', done => { + const object = new Parse.Object('AnObject'); + object.set('foo', { + _bar: '_', + baz_bar: 1, + __foo_bar: true, + _0: 'underscore_zero', + _more: { + _nested: 'key', + }, + }); + object + .save() + .then(res => { + ok(res); + return res.fetch(); + }) + .then(res => { + const foo = res.get('foo'); + expect(foo['_bar']).toEqual('_'); + expect(foo['baz_bar']).toEqual(1); + expect(foo['__foo_bar']).toBe(true); + expect(foo['_0']).toEqual('underscore_zero'); + expect(foo['_more']['_nested']).toEqual('key'); + done(); + }) + .catch(err => { + jfail(err); + fail('should not fail'); + done(); + }); }); - it('should have undefined includes when object is missing', (done) => { - const obj1 = new Parse.Object("AnObject"); - const obj2 = new Parse.Object("AnObject"); - - Parse.Object.saveAll([obj1, obj2]).then(() => { - obj1.set("obj", obj2); - // Save the pointer, delete the pointee - return obj1.save().then(() => { return obj2.destroy() }); - }).then(() => { - const query = new Parse.Query("AnObject"); - query.include("obj"); - return query.find(); - }).then((res) => { - expect(res.length).toBe(1); - if (res[0]) { - expect(res[0].get("obj")).toBe(undefined); - } - const query = new Parse.Query("AnObject"); - return query.find(); - }).then((res) => { - expect(res.length).toBe(1); - if (res[0]) { - expect(res[0].get("obj")).not.toBe(undefined); - return res[0].get("obj").fetch(); - } else { + it('should have undefined includes when object is missing', done => { + const obj1 = new Parse.Object('AnObject'); + const obj2 = new Parse.Object('AnObject'); + + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + obj1.set('obj', obj2); + // Save the pointer, delete the pointee + return obj1.save().then(() => { + return obj2.destroy(); + }); + }) + .then(() => { + const query = new Parse.Query('AnObject'); + query.include('obj'); + return query.find(); + }) + .then(res => { + expect(res.length).toBe(1); + if (res[0]) { + expect(res[0].get('obj')).toBe(undefined); + } + const query = new Parse.Query('AnObject'); + return query.find(); + }) + .then(res => { + expect(res.length).toBe(1); + if (res[0]) { + expect(res[0].get('obj')).not.toBe(undefined); + return res[0].get('obj').fetch(); + } else { + done(); + } + }) + .then( + () => { + fail('Should not fetch a deleted object'); + }, + err => { + expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + done(); + } + ); + }); + + it('should have undefined includes when object is missing on deeper path', done => { + const obj1 = new Parse.Object('AnObject'); + const obj2 = new Parse.Object('AnObject'); + const obj3 = new Parse.Object('AnObject'); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + obj1.set('obj', obj2); + obj2.set('obj', obj3); + // Save the pointer, delete the pointee + return Parse.Object.saveAll([obj1, obj2]).then(() => { + return obj3.destroy(); + }); + }) + .then(() => { + const query = new Parse.Query('AnObject'); + query.include('obj.obj'); + return query.get(obj1.id); + }) + .then(res => { + expect(res.get('obj')).not.toBe(undefined); + expect(res.get('obj').get('obj')).toBe(undefined); done(); - } - }).then(() => { - fail("Should not fetch a deleted object"); - }, (err) => { - expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND); - done(); - }) - }); - - it('should have undefined includes when object is missing on deeper path', (done) => { - const obj1 = new Parse.Object("AnObject"); - const obj2 = new Parse.Object("AnObject"); - const obj3 = new Parse.Object("AnObject"); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - obj1.set("obj", obj2); - obj2.set("obj", obj3); - // Save the pointer, delete the pointee - return Parse.Object.saveAll([obj1, obj2]).then(() => { return obj3.destroy() }); - }).then(() => { - const query = new Parse.Query("AnObject"); - query.include("obj.obj"); - return query.get(obj1.id); - }).then((res) => { - expect(res.get("obj")).not.toBe(undefined); - expect(res.get("obj").get("obj")).toBe(undefined); - done(); - }).catch(err => { - jfail(err); - done(); - }) + }) + .catch(err => { + jfail(err); + done(); + }); }); - it('should handle includes on null arrays #2752', (done) => { - const obj1 = new Parse.Object("AnObject"); - const obj2 = new Parse.Object("AnotherObject"); - const obj3 = new Parse.Object("NestedObject"); + it('should handle includes on null arrays #2752', done => { + const obj1 = new Parse.Object('AnObject'); + const obj2 = new Parse.Object('AnotherObject'); + const obj3 = new Parse.Object('NestedObject'); obj3.set({ - "foo": "bar" - }) + foo: 'bar', + }); obj2.set({ - "key": obj3 - }) - - Parse.Object.saveAll([obj1, obj2]).then(() => { - obj1.set("objects", [null, null, obj2]); - return obj1.save(); - }).then(() => { - const query = new Parse.Query("AnObject"); - query.include("objects.key"); - return query.find(); - }).then((res) => { - const obj = res[0]; - expect(obj.get("objects")).not.toBe(undefined); - const array = obj.get("objects"); - expect(Array.isArray(array)).toBe(true); - expect(array[0]).toBe(null); - expect(array[1]).toBe(null); - expect(array[2].get("key").get("foo")).toEqual("bar"); - done(); - }).catch(err => { - jfail(err); - done(); - }) + key: obj3, + }); + + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + obj1.set('objects', [null, null, obj2]); + return obj1.save(); + }) + .then(() => { + const query = new Parse.Query('AnObject'); + query.include('objects.key'); + return query.find(); + }) + .then(res => { + const obj = res[0]; + expect(obj.get('objects')).not.toBe(undefined); + const array = obj.get('objects'); + expect(Array.isArray(array)).toBe(true); + expect(array[0]).toBe(null); + expect(array[1]).toBe(null); + expect(array[2].get('key').get('foo')).toEqual('bar'); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); - it('should handle select and include #2786', (done) => { - const score = new Parse.Object("GameScore"); - const player = new Parse.Object("Player"); + it('should handle select and include #2786', done => { + const score = new Parse.Object('GameScore'); + const player = new Parse.Object('Player'); score.set({ - "score": 1234 - }); - - score.save().then(() => { - player.set("gameScore", score); - player.set("other", "value"); - return player.save(); - }).then(() => { - const query = new Parse.Query("Player"); - query.include("gameScore"); - query.select("gameScore"); - return query.find(); - }).then((res) => { - const obj = res[0]; - const gameScore = obj.get("gameScore"); - const other = obj.get("other"); - expect(other).toBeUndefined(); - expect(gameScore).not.toBeUndefined(); - expect(gameScore.get("score")).toBe(1234); - done(); - }).catch(err => { - jfail(err); - done(); - }) + score: 1234, + }); + + score + .save() + .then(() => { + player.set('gameScore', score); + player.set('other', 'value'); + return player.save(); + }) + .then(() => { + const query = new Parse.Query('Player'); + query.include('gameScore'); + query.select('gameScore'); + return query.find(); + }) + .then(res => { + const obj = res[0]; + const gameScore = obj.get('gameScore'); + const other = obj.get('other'); + expect(other).toBeUndefined(); + expect(gameScore).not.toBeUndefined(); + expect(gameScore.get('score')).toBe(1234); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); - it('should include ACLs with select', (done) => { - const score = new Parse.Object("GameScore"); - const player = new Parse.Object("Player"); + it('should include ACLs with select', done => { + const score = new Parse.Object('GameScore'); + const player = new Parse.Object('Player'); score.set({ - "score": 1234 + score: 1234, }); const acl = new Parse.ACL(); acl.setPublicReadAccess(true); acl.setPublicWriteAccess(false); - score.save().then(() => { - player.set("gameScore", score); - player.set("other", "value"); - player.setACL(acl); - return player.save(); - }).then(() => { - const query = new Parse.Query("Player"); - query.include("gameScore"); - query.select("gameScore"); - return query.find(); - }).then((res) => { - const obj = res[0]; - const gameScore = obj.get("gameScore"); - const other = obj.get("other"); - expect(other).toBeUndefined(); - expect(gameScore).not.toBeUndefined(); - expect(gameScore.get("score")).toBe(1234); - expect(obj.getACL().getPublicReadAccess()).toBe(true); - expect(obj.getACL().getPublicWriteAccess()).toBe(false); - }).then(done).catch(done.fail); - }); - - it ('Update object field should store exactly same sent object', async (done) => { + score + .save() + .then(() => { + player.set('gameScore', score); + player.set('other', 'value'); + player.setACL(acl); + return player.save(); + }) + .then(() => { + const query = new Parse.Query('Player'); + query.include('gameScore'); + query.select('gameScore'); + return query.find(); + }) + .then(res => { + const obj = res[0]; + const gameScore = obj.get('gameScore'); + const other = obj.get('other'); + expect(other).toBeUndefined(); + expect(gameScore).not.toBeUndefined(); + expect(gameScore.get('score')).toBe(1234); + expect(obj.getACL().getPublicReadAccess()).toBe(true); + expect(obj.getACL().getPublicWriteAccess()).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + + it('Update object field should store exactly same sent object', async done => { let object = new TestObject(); // Set initial data - object.set("jsonData", { a: "b" }); + object.set('jsonData', { a: 'b' }); object = await object.save(); - equal(object.get('jsonData'), { a: "b" }); + equal(object.get('jsonData'), { a: 'b' }); // Set empty JSON - object.set("jsonData", {}); + object.set('jsonData', {}); object = await object.save(); equal(object.get('jsonData'), {}); // Set new JSON data - object.unset('jsonData') - object.set("jsonData", { c: "d" }); + object.unset('jsonData'); + object.set('jsonData', { c: 'd' }); object = await object.save(); - equal(object.get('jsonData'), { c: "d" }); + equal(object.get('jsonData'), { c: 'd' }); // Fetch object from server - object = await object.fetch() - console.log(object.id, object.get('jsonData')) - equal(object.get('jsonData'), { c: "d" }); + object = await object.fetch(); + console.log(object.id, object.get('jsonData')); + equal(object.get('jsonData'), { c: 'd' }); done(); }); diff --git a/spec/ParsePolygon.spec.js b/spec/ParsePolygon.spec.js index 2527a09e55..8c909c0077 100644 --- a/spec/ParsePolygon.spec.js +++ b/spec/ParsePolygon.spec.js @@ -1,347 +1,401 @@ const TestObject = Parse.Object.extend('TestObject'); -const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; -const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; +const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter') + .default; +const mongoURI = + 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; const rp = require('request-promise'); const defaultHeaders = { 'X-Parse-Application-Id': 'test', - 'X-Parse-Rest-API-Key': 'rest' -} + 'X-Parse-Rest-API-Key': 'rest', +}; describe('Parse.Polygon testing', () => { - beforeAll(() => require('../lib/TestUtils').destroyAllDataPermanently()); - it('polygon save open path', (done) => { - const coords = [[0,0],[0,1],[1,1],[1,0]]; - const closed = [[0,0],[0,1],[1,1],[1,0],[0,0]]; + it('polygon save open path', done => { + const coords = [[0, 0], [0, 1], [1, 1], [1, 0]]; + const closed = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; const obj = new TestObject(); obj.set('polygon', new Parse.Polygon(coords)); - return obj.save().then(() => { - const query = new Parse.Query(TestObject); - return query.get(obj.id); - }).then((result) => { - const polygon = result.get('polygon'); - equal(polygon instanceof Parse.Polygon, true); - equal(polygon.coordinates, closed); - done(); - }, done.fail); + return obj + .save() + .then(() => { + const query = new Parse.Query(TestObject); + return query.get(obj.id); + }) + .then(result => { + const polygon = result.get('polygon'); + equal(polygon instanceof Parse.Polygon, true); + equal(polygon.coordinates, closed); + done(); + }, done.fail); }); - it('polygon save closed path', (done) => { - const coords = [[0,0],[0,1],[1,1],[1,0],[0,0]]; + it('polygon save closed path', done => { + const coords = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; const obj = new TestObject(); obj.set('polygon', new Parse.Polygon(coords)); - return obj.save().then(() => { - const query = new Parse.Query(TestObject); - return query.get(obj.id); - }).then((result) => { - const polygon = result.get('polygon'); - equal(polygon instanceof Parse.Polygon, true); - equal(polygon.coordinates, coords); - done(); - }, done.fail); + return obj + .save() + .then(() => { + const query = new Parse.Query(TestObject); + return query.get(obj.id); + }) + .then(result => { + const polygon = result.get('polygon'); + equal(polygon instanceof Parse.Polygon, true); + equal(polygon.coordinates, coords); + done(); + }, done.fail); }); - it('polygon equalTo (open/closed) path', (done) => { - const openPoints = [[0,0],[0,1],[1,1],[1,0]]; - const closedPoints = [[0,0],[0,1],[1,1],[1,0],[0,0]]; + it('polygon equalTo (open/closed) path', done => { + const openPoints = [[0, 0], [0, 1], [1, 1], [1, 0]]; + const closedPoints = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; const openPolygon = new Parse.Polygon(openPoints); const closedPolygon = new Parse.Polygon(closedPoints); const obj = new TestObject(); obj.set('polygon', openPolygon); - return obj.save().then(() => { - const query = new Parse.Query(TestObject); - query.equalTo('polygon', openPolygon); - return query.find(); - }).then((results) => { - const polygon = results[0].get('polygon'); - equal(polygon instanceof Parse.Polygon, true); - equal(polygon.coordinates, closedPoints); - const query = new Parse.Query(TestObject); - query.equalTo('polygon', closedPolygon); - return query.find(); - }).then((results) => { - const polygon = results[0].get('polygon'); - equal(polygon instanceof Parse.Polygon, true); - equal(polygon.coordinates, closedPoints); - done(); - }, done.fail); + return obj + .save() + .then(() => { + const query = new Parse.Query(TestObject); + query.equalTo('polygon', openPolygon); + return query.find(); + }) + .then(results => { + const polygon = results[0].get('polygon'); + equal(polygon instanceof Parse.Polygon, true); + equal(polygon.coordinates, closedPoints); + const query = new Parse.Query(TestObject); + query.equalTo('polygon', closedPolygon); + return query.find(); + }) + .then(results => { + const polygon = results[0].get('polygon'); + equal(polygon instanceof Parse.Polygon, true); + equal(polygon.coordinates, closedPoints); + done(); + }, done.fail); }); - it('polygon update', (done) => { - const oldCoords = [[0,0],[0,1],[1,1],[1,0]]; + it('polygon update', done => { + const oldCoords = [[0, 0], [0, 1], [1, 1], [1, 0]]; const oldPolygon = new Parse.Polygon(oldCoords); - const newCoords = [[2,2],[2,3],[3,3],[3,2]]; + const newCoords = [[2, 2], [2, 3], [3, 3], [3, 2]]; const newPolygon = new Parse.Polygon(newCoords); const obj = new TestObject(); obj.set('polygon', oldPolygon); - return obj.save().then(() => { - obj.set('polygon', newPolygon); - return obj.save(); - }).then(() => { - const query = new Parse.Query(TestObject); - return query.get(obj.id); - }).then((result) => { - const polygon = result.get('polygon'); - newCoords.push(newCoords[0]); - equal(polygon instanceof Parse.Polygon, true); - equal(polygon.coordinates, newCoords); - done(); - }, done.fail); + return obj + .save() + .then(() => { + obj.set('polygon', newPolygon); + return obj.save(); + }) + .then(() => { + const query = new Parse.Query(TestObject); + return query.get(obj.id); + }) + .then(result => { + const polygon = result.get('polygon'); + newCoords.push(newCoords[0]); + equal(polygon instanceof Parse.Polygon, true); + equal(polygon.coordinates, newCoords); + done(); + }, done.fail); }); - it('polygon invalid value', (done) => { - const coords = [['foo','bar'],[0,1],[1,0],[1,1],[0,0]]; + it('polygon invalid value', done => { + const coords = [['foo', 'bar'], [0, 1], [1, 0], [1, 1], [0, 0]]; const obj = new TestObject(); - obj.set('polygon', {__type: 'Polygon', coordinates: coords}); - return obj.save().then(() => { - const query = new Parse.Query(TestObject); - return query.get(obj.id); - }).then(done.fail, done); + obj.set('polygon', { __type: 'Polygon', coordinates: coords }); + return obj + .save() + .then(() => { + const query = new Parse.Query(TestObject); + return query.get(obj.id); + }) + .then(done.fail, done); }); - it('polygon three points minimum', (done) => { - const coords = [[0,0]]; + it('polygon three points minimum', done => { + const coords = [[0, 0]]; const obj = new TestObject(); // use raw so we test the server validates properly - obj.set('polygon', {__type: 'Polygon', coordinates: coords}); + obj.set('polygon', { __type: 'Polygon', coordinates: coords }); obj.save().then(done.fail, done); }); - it('polygon three different points minimum', (done) => { - const coords = [[0,0],[0,1],[0,0]]; + it('polygon three different points minimum', done => { + const coords = [[0, 0], [0, 1], [0, 0]]; const obj = new TestObject(); obj.set('polygon', new Parse.Polygon(coords)); obj.save().then(done.fail, done); }); - it('polygon counterclockwise', (done) => { - const coords = [[1,1],[0,1],[0,0],[1,0]]; - const closed = [[1,1],[0,1],[0,0],[1,0],[1,1]]; + it('polygon counterclockwise', done => { + const coords = [[1, 1], [0, 1], [0, 0], [1, 0]]; + const closed = [[1, 1], [0, 1], [0, 0], [1, 0], [1, 1]]; const obj = new TestObject(); obj.set('polygon', new Parse.Polygon(coords)); - obj.save().then(() => { - const query = new Parse.Query(TestObject); - return query.get(obj.id); - }).then((result) => { - const polygon = result.get('polygon'); - equal(polygon instanceof Parse.Polygon, true); - equal(polygon.coordinates, closed); - done(); - }, done.fail); + obj + .save() + .then(() => { + const query = new Parse.Query(TestObject); + return query.get(obj.id); + }) + .then(result => { + const polygon = result.get('polygon'); + equal(polygon instanceof Parse.Polygon, true); + equal(polygon.coordinates, closed); + done(); + }, done.fail); }); describe('with location', () => { beforeAll(() => require('../lib/TestUtils').destroyAllDataPermanently()); - it('polygonContain query', (done) => { - const points1 = [[0,0],[0,1],[1,1],[1,0]]; - const points2 = [[0,0],[0,2],[2,2],[2,0]]; - const points3 = [[10,10],[10,15],[15,15],[15,10],[10,10]]; + it('polygonContain query', done => { + const points1 = [[0, 0], [0, 1], [1, 1], [1, 0]]; + const points2 = [[0, 0], [0, 2], [2, 2], [2, 0]]; + const points3 = [[10, 10], [10, 15], [15, 15], [15, 10], [10, 10]]; const polygon1 = new Parse.Polygon(points1); const polygon2 = new Parse.Polygon(points2); const polygon3 = new Parse.Polygon(points3); - const obj1 = new TestObject({location: polygon1}); - const obj2 = new TestObject({location: polygon2}); - const obj3 = new TestObject({location: polygon3}); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const where = { - location: { - $geoIntersects: { - $point: { __type: 'GeoPoint', latitude: 0.5, longitude: 0.5 } - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(2); - done(); - }, done.fail); + const obj1 = new TestObject({ location: polygon1 }); + const obj2 = new TestObject({ location: polygon2 }); + const obj3 = new TestObject({ location: polygon3 }); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const where = { + location: { + $geoIntersects: { + $point: { __type: 'GeoPoint', latitude: 0.5, longitude: 0.5 }, + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(2); + done(); + }, done.fail); }); - it('polygonContain query no reverse input (Regression test for #4608)', (done) => { - const points1 = [[.25,0],[.25,1.25],[.75,1.25],[.75,0]]; - const points2 = [[0,0],[0,2],[2,2],[2,0]]; - const points3 = [[10,10],[10,15],[15,15],[15,10],[10,10]]; + it('polygonContain query no reverse input (Regression test for #4608)', done => { + const points1 = [[0.25, 0], [0.25, 1.25], [0.75, 1.25], [0.75, 0]]; + const points2 = [[0, 0], [0, 2], [2, 2], [2, 0]]; + const points3 = [[10, 10], [10, 15], [15, 15], [15, 10], [10, 10]]; const polygon1 = new Parse.Polygon(points1); const polygon2 = new Parse.Polygon(points2); const polygon3 = new Parse.Polygon(points3); - const obj1 = new TestObject({location: polygon1}); - const obj2 = new TestObject({location: polygon2}); - const obj3 = new TestObject({location: polygon3}); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const where = { - location: { - $geoIntersects: { - $point: { __type: 'GeoPoint', latitude: 0.5, longitude:1.0 } - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(2); - done(); - }, done.fail); + const obj1 = new TestObject({ location: polygon1 }); + const obj2 = new TestObject({ location: polygon2 }); + const obj3 = new TestObject({ location: polygon3 }); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const where = { + location: { + $geoIntersects: { + $point: { __type: 'GeoPoint', latitude: 0.5, longitude: 1.0 }, + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(2); + done(); + }, done.fail); }); - it('polygonContain query real data (Regression test for #4608)', (done) => { - const detroit = [[42.631655189280224,-83.78406753121705],[42.633047793854814,-83.75333640366955],[42.61625254348911,-83.75149921669944],[42.61526926650296,-83.78161794858735],[42.631655189280224,-83.78406753121705]]; + it('polygonContain query real data (Regression test for #4608)', done => { + const detroit = [ + [42.631655189280224, -83.78406753121705], + [42.633047793854814, -83.75333640366955], + [42.61625254348911, -83.75149921669944], + [42.61526926650296, -83.78161794858735], + [42.631655189280224, -83.78406753121705], + ]; const polygon = new Parse.Polygon(detroit); - const obj = new TestObject({location: polygon}); - obj.save().then(() => { - const where = { - location: { - $geoIntersects: { - $point: { __type: 'GeoPoint', latitude: 42.624599, longitude:-83.770162 } - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(1); - done(); - }, done.fail); + const obj = new TestObject({ location: polygon }); + obj + .save() + .then(() => { + const where = { + location: { + $geoIntersects: { + $point: { + __type: 'GeoPoint', + latitude: 42.624599, + longitude: -83.770162, + }, + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(1); + done(); + }, done.fail); }); - it('polygonContain invalid input', (done) => { - const points = [[0,0],[0,1],[1,1],[1,0]]; + it('polygonContain invalid input', done => { + const points = [[0, 0], [0, 1], [1, 1], [1, 0]]; const polygon = new Parse.Polygon(points); - const obj = new TestObject({location: polygon}); - obj.save().then(() => { - const where = { - location: { - $geoIntersects: { - $point: { __type: 'GeoPoint', latitude: 181, longitude: 181 } - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }).then(done.fail, () => done()); + const obj = new TestObject({ location: polygon }); + obj + .save() + .then(() => { + const where = { + location: { + $geoIntersects: { + $point: { __type: 'GeoPoint', latitude: 181, longitude: 181 }, + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(done.fail, () => done()); }); - it('polygonContain invalid geoPoint', (done) => { - const points = [[0,0],[0,1],[1,1],[1,0]]; + it('polygonContain invalid geoPoint', done => { + const points = [[0, 0], [0, 1], [1, 1], [1, 0]]; const polygon = new Parse.Polygon(points); - const obj = new TestObject({location: polygon}); - obj.save().then(() => { - const where = { - location: { - $geoIntersects: { - $point: [] - } - } - }; - return rp.post({ - url: Parse.serverURL + '/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }).then(done.fail, () => done()); + const obj = new TestObject({ location: polygon }); + obj + .save() + .then(() => { + const where = { + location: { + $geoIntersects: { + $point: [], + }, + }, + }; + return rp.post({ + url: Parse.serverURL + '/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(done.fail, () => done()); }); }); }); describe_only_db('mongo')('Parse.Polygon testing', () => { - beforeEach(() => require('../lib/TestUtils').destroyAllDataPermanently()); - it('support 2d and 2dsphere', (done) => { - const coords = [[0,0],[0,1],[1,1],[1,0],[0,0]]; + it('support 2d and 2dsphere', done => { + const coords = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; // testings against REST API, use raw formats - const polygon = {__type: 'Polygon', coordinates: coords}; - const location = {__type: 'GeoPoint', latitude:10, longitude:10}; + const polygon = { __type: 'Polygon', coordinates: coords }; + const location = { __type: 'GeoPoint', latitude: 10, longitude: 10 }; const databaseAdapter = new MongoStorageAdapter({ uri: mongoURI }); return reconfigureServer({ appId: 'test', restAPIKey: 'rest', publicServerURL: 'http://localhost:8378/1', - databaseAdapter - }).then(() => { - return databaseAdapter.createIndex('TestObject', {location: '2d'}); - }).then(() => { - return databaseAdapter.createIndex('TestObject', {polygon: '2dsphere'}); - }).then(() => { - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { - '_method': 'POST', - location, - polygon, - polygon2: polygon - }, - headers: defaultHeaders - }); - }).then((resp) => { - return rp.post({ - url: `http://localhost:8378/1/classes/TestObject/${resp.objectId}`, - json: {'_method': 'GET'}, - headers: defaultHeaders - }); - }).then((resp) => { - equal(resp.location, location); - equal(resp.polygon, polygon); - equal(resp.polygon2, polygon); - return databaseAdapter.getIndexes('TestObject'); - }).then((indexes) => { - equal(indexes.length, 4); - equal(indexes[0].key, {_id: 1}); - equal(indexes[1].key, {location: '2d'}); - equal(indexes[2].key, {polygon: '2dsphere'}); - equal(indexes[3].key, {polygon2: '2dsphere'}); - done(); - }, done.fail); + databaseAdapter, + }) + .then(() => { + return databaseAdapter.createIndex('TestObject', { location: '2d' }); + }) + .then(() => { + return databaseAdapter.createIndex('TestObject', { + polygon: '2dsphere', + }); + }) + .then(() => { + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { + _method: 'POST', + location, + polygon, + polygon2: polygon, + }, + headers: defaultHeaders, + }); + }) + .then(resp => { + return rp.post({ + url: `http://localhost:8378/1/classes/TestObject/${resp.objectId}`, + json: { _method: 'GET' }, + headers: defaultHeaders, + }); + }) + .then(resp => { + equal(resp.location, location); + equal(resp.polygon, polygon); + equal(resp.polygon2, polygon); + return databaseAdapter.getIndexes('TestObject'); + }) + .then(indexes => { + equal(indexes.length, 4); + equal(indexes[0].key, { _id: 1 }); + equal(indexes[1].key, { location: '2d' }); + equal(indexes[2].key, { polygon: '2dsphere' }); + equal(indexes[3].key, { polygon2: '2dsphere' }); + done(); + }, done.fail); }); - it('polygon coordinates reverse input', (done) => { + it('polygon coordinates reverse input', done => { const Config = require('../lib/Config'); const config = Config.get('test'); // When stored the first point should be the last point - const input = [[12,11],[14,13],[16,15],[18,17]]; - const output = [[[11,12],[13,14],[15,16],[17,18],[11,12]]]; + const input = [[12, 11], [14, 13], [16, 15], [18, 17]]; + const output = [[[11, 12], [13, 14], [15, 16], [17, 18], [11, 12]]]; const obj = new TestObject(); obj.set('polygon', new Parse.Polygon(input)); - obj.save().then(() => { - return config.database.adapter._rawFind('TestObject', {_id: obj.id}); - }).then((results) => { - expect(results.length).toBe(1); - expect(results[0].polygon.coordinates).toEqual(output); - done(); - }); + obj + .save() + .then(() => { + return config.database.adapter._rawFind('TestObject', { _id: obj.id }); + }) + .then(results => { + expect(results.length).toBe(1); + expect(results[0].polygon.coordinates).toEqual(output); + done(); + }); }); - it('polygon loop is not valid', (done) => { - const coords = [[0,0],[0,1],[1,0],[1,1]]; + it('polygon loop is not valid', done => { + const coords = [[0, 0], [0, 1], [1, 0], [1, 1]]; const obj = new TestObject(); obj.set('polygon', new Parse.Polygon(coords)); obj.save().then(done.fail, done); diff --git a/spec/ParsePubSub.spec.js b/spec/ParsePubSub.spec.js index fe8db147d6..91f4c1a763 100644 --- a/spec/ParsePubSub.spec.js +++ b/spec/ParsePubSub.spec.js @@ -1,80 +1,101 @@ const ParsePubSub = require('../lib/LiveQuery/ParsePubSub').ParsePubSub; describe('ParsePubSub', function() { - beforeEach(function(done) { // Mock RedisPubSub const mockRedisPubSub = { createPublisher: jasmine.createSpy('createPublisherRedis'), - createSubscriber: jasmine.createSpy('createSubscriberRedis') + createSubscriber: jasmine.createSpy('createSubscriberRedis'), }; - jasmine.mockLibrary('../lib/Adapters/PubSub/RedisPubSub', 'RedisPubSub', mockRedisPubSub); + jasmine.mockLibrary( + '../lib/Adapters/PubSub/RedisPubSub', + 'RedisPubSub', + mockRedisPubSub + ); // Mock EventEmitterPubSub const mockEventEmitterPubSub = { createPublisher: jasmine.createSpy('createPublisherEventEmitter'), - createSubscriber: jasmine.createSpy('createSubscriberEventEmitter') + createSubscriber: jasmine.createSpy('createSubscriberEventEmitter'), }; - jasmine.mockLibrary('../lib/Adapters/PubSub/EventEmitterPubSub', 'EventEmitterPubSub', mockEventEmitterPubSub); + jasmine.mockLibrary( + '../lib/Adapters/PubSub/EventEmitterPubSub', + 'EventEmitterPubSub', + mockEventEmitterPubSub + ); done(); }); it('can create redis publisher', function() { ParsePubSub.createPublisher({ - redisURL: 'redisURL' + redisURL: 'redisURL', }); - const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; - expect(RedisPubSub.createPublisher).toHaveBeenCalledWith({redisURL: 'redisURL'}); + const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub') + .RedisPubSub; + const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') + .EventEmitterPubSub; + expect(RedisPubSub.createPublisher).toHaveBeenCalledWith({ + redisURL: 'redisURL', + }); expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled(); }); it('can create event emitter publisher', function() { ParsePubSub.createPublisher({}); - const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; + const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub') + .RedisPubSub; + const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') + .EventEmitterPubSub; expect(RedisPubSub.createPublisher).not.toHaveBeenCalled(); expect(EventEmitterPubSub.createPublisher).toHaveBeenCalled(); }); it('can create redis subscriber', function() { ParsePubSub.createSubscriber({ - redisURL: 'redisURL' + redisURL: 'redisURL', }); - const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; - expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith({redisURL: 'redisURL'}); + const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub') + .RedisPubSub; + const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') + .EventEmitterPubSub; + expect(RedisPubSub.createSubscriber).toHaveBeenCalledWith({ + redisURL: 'redisURL', + }); expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled(); }); it('can create event emitter subscriber', function() { ParsePubSub.createSubscriber({}); - const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; + const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub') + .RedisPubSub; + const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') + .EventEmitterPubSub; expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled(); expect(EventEmitterPubSub.createSubscriber).toHaveBeenCalled(); }); it('can create publisher/sub with custom adapter', function() { - const adapter = { + const adapter = { createPublisher: jasmine.createSpy('createPublisher'), - createSubscriber: jasmine.createSpy('createSubscriber') - } + createSubscriber: jasmine.createSpy('createSubscriber'), + }; ParsePubSub.createPublisher({ - pubSubAdapter: adapter + pubSubAdapter: adapter, }); expect(adapter.createPublisher).toHaveBeenCalled(); ParsePubSub.createSubscriber({ - pubSubAdapter: adapter + pubSubAdapter: adapter, }); expect(adapter.createSubscriber).toHaveBeenCalled(); - const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; + const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub') + .RedisPubSub; + const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') + .EventEmitterPubSub; expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled(); expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled(); expect(RedisPubSub.createPublisher).not.toHaveBeenCalled(); @@ -82,34 +103,39 @@ describe('ParsePubSub', function() { }); it('can create publisher/sub with custom function adapter', function() { - const adapter = { + const adapter = { createPublisher: jasmine.createSpy('createPublisher'), - createSubscriber: jasmine.createSpy('createSubscriber') - } + createSubscriber: jasmine.createSpy('createSubscriber'), + }; ParsePubSub.createPublisher({ pubSubAdapter: function() { return adapter; - } + }, }); expect(adapter.createPublisher).toHaveBeenCalled(); ParsePubSub.createSubscriber({ pubSubAdapter: function() { return adapter; - } + }, }); expect(adapter.createSubscriber).toHaveBeenCalled(); - const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; - const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub').EventEmitterPubSub; + const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub') + .RedisPubSub; + const EventEmitterPubSub = require('../lib/Adapters/PubSub/EventEmitterPubSub') + .EventEmitterPubSub; expect(RedisPubSub.createSubscriber).not.toHaveBeenCalled(); expect(EventEmitterPubSub.createSubscriber).not.toHaveBeenCalled(); expect(RedisPubSub.createPublisher).not.toHaveBeenCalled(); expect(EventEmitterPubSub.createPublisher).not.toHaveBeenCalled(); }); - afterEach(function(){ + afterEach(function() { jasmine.restoreLibrary('../lib/Adapters/PubSub/RedisPubSub', 'RedisPubSub'); - jasmine.restoreLibrary('../lib/Adapters/PubSub/EventEmitterPubSub', 'EventEmitterPubSub'); + jasmine.restoreLibrary( + '../lib/Adapters/PubSub/EventEmitterPubSub', + 'EventEmitterPubSub' + ); }); }); diff --git a/spec/ParseQuery.Aggregate.spec.js b/spec/ParseQuery.Aggregate.spec.js index 377d4d96a2..3449e61729 100644 --- a/spec/ParseQuery.Aggregate.spec.js +++ b/spec/ParseQuery.Aggregate.spec.js @@ -5,90 +5,113 @@ const rp = require('request-promise'); const masterKeyHeaders = { 'X-Parse-Application-Id': 'test', 'X-Parse-Rest-API-Key': 'test', - 'X-Parse-Master-Key': 'test' -} + 'X-Parse-Master-Key': 'test', +}; const masterKeyOptions = { headers: masterKeyHeaders, - json: true -} + json: true, +}; const PointerObject = Parse.Object.extend({ - className: "PointerObject" + className: 'PointerObject', }); const loadTestData = () => { - const data1 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 900, size: ['S', 'M']}; - const data2 = {score: 10, name: 'foo', sender: {group: 'A'}, views: 800, size: ['M', 'L']}; - const data3 = {score: 10, name: 'bar', sender: {group: 'B'}, views: 700, size: ['S']}; - const data4 = {score: 20, name: 'dpl', sender: {group: 'B'}, views: 700, size: ['S']}; + const data1 = { + score: 10, + name: 'foo', + sender: { group: 'A' }, + views: 900, + size: ['S', 'M'], + }; + const data2 = { + score: 10, + name: 'foo', + sender: { group: 'A' }, + views: 800, + size: ['M', 'L'], + }; + const data3 = { + score: 10, + name: 'bar', + sender: { group: 'B' }, + views: 700, + size: ['S'], + }; + const data4 = { + score: 20, + name: 'dpl', + sender: { group: 'B' }, + views: 700, + size: ['S'], + }; const obj1 = new TestObject(data1); const obj2 = new TestObject(data2); const obj3 = new TestObject(data3); const obj4 = new TestObject(data4); return Parse.Object.saveAll([obj1, obj2, obj3, obj4]); -} +}; describe('Parse.Query Aggregate testing', () => { - beforeEach((done) => { + beforeEach(done => { loadTestData().then(done, done); }); - it('should only query aggregate with master key', (done) => { - Parse._request('GET', `aggregate/someClass`, {}) - .then(() => {}, (error) => { + it('should only query aggregate with master key', done => { + Parse._request('GET', `aggregate/someClass`, {}).then( + () => {}, + error => { expect(error.message).toEqual('unauthorized: master key is required'); done(); - }); + } + ); }); - it('invalid query invalid key', (done) => { + it('invalid query invalid key', done => { const options = Object.assign({}, masterKeyOptions, { body: { unknown: {}, - } + }, + }); + rp.get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => { + expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY); + done(); }); - rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .catch((error) => { - expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY); - done(); - }); }); - it('invalid query group _id', (done) => { + it('invalid query group _id', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: { _id: null }, - } + }, + }); + rp.get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => { + expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY); + done(); }); - rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .catch((error) => { - expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY); - done(); - }); }); - it('invalid query group objectId required', (done) => { + it('invalid query group objectId required', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: {}, - } + }, + }); + rp.get(Parse.serverURL + '/aggregate/TestObject', options).catch(error => { + expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY); + done(); }); - rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .catch((error) => { - expect(error.error.code).toEqual(Parse.Error.INVALID_QUERY); - done(); - }); }); - it('group by field', (done) => { + it('group by field', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: { objectId: '$name' }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(3); expect(resp.results[0].hasOwnProperty('objectId')).toBe(true); expect(resp.results[1].hasOwnProperty('objectId')).toBe(true); @@ -97,7 +120,8 @@ describe('Parse.Query Aggregate testing', () => { expect(resp.results[1].objectId).not.toBe(undefined); expect(resp.results[2].objectId).not.toBe(undefined); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); it('group by pipeline operator', async () => { @@ -105,10 +129,13 @@ describe('Parse.Query Aggregate testing', () => { body: { pipeline: { group: { objectId: '$name' }, - } - } + }, + }, }); - const resp = await rp.get(Parse.serverURL + '/aggregate/TestObject', options); + const resp = await rp.get( + Parse.serverURL + '/aggregate/TestObject', + options + ); expect(resp.results.length).toBe(3); expect(resp.results[0].hasOwnProperty('objectId')).toBe(true); expect(resp.results[1].hasOwnProperty('objectId')).toBe(true); @@ -118,178 +145,218 @@ describe('Parse.Query Aggregate testing', () => { expect(resp.results[2].objectId).not.toBe(undefined); }); - it('group by empty object', (done) => { + it('group by empty object', done => { const obj = new TestObject(); - const pipeline = [{ - group: { objectId: {} } - }]; - obj.save().then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results[0].objectId).toEqual(null); - done(); - }); + const pipeline = [ + { + group: { objectId: {} }, + }, + ]; + obj + .save() + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results[0].objectId).toEqual(null); + done(); + }); }); - it('group by empty string', (done) => { + it('group by empty string', done => { const obj = new TestObject(); - const pipeline = [{ - group: { objectId: '' } - }]; - obj.save().then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results[0].objectId).toEqual(null); - done(); - }); + const pipeline = [ + { + group: { objectId: '' }, + }, + ]; + obj + .save() + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results[0].objectId).toEqual(null); + done(); + }); }); - it('group by empty array', (done) => { + it('group by empty array', done => { const obj = new TestObject(); - const pipeline = [{ - group: { objectId: [] } - }]; - obj.save().then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results[0].objectId).toEqual(null); - done(); - }); + const pipeline = [ + { + group: { objectId: [] }, + }, + ]; + obj + .save() + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results[0].objectId).toEqual(null); + done(); + }); }); - it('group by date object', (done) => { + it('group by date object', done => { const obj1 = new TestObject(); const obj2 = new TestObject(); const obj3 = new TestObject(); - const pipeline = [{ - group: { - objectId: { day: { $dayOfMonth: "$_updated_at" }, month: { $month: "$_created_at" }, year: { $year: "$_created_at" } }, - count: { $sum: 1 } - } - }]; - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - const createdAt = new Date(obj1.createdAt); - expect(results[0].objectId.day).toEqual(createdAt.getUTCDate()); - expect(results[0].objectId.month).toEqual(createdAt.getMonth() + 1); - expect(results[0].objectId.year).toEqual(createdAt.getUTCFullYear()); - done(); - }); + const pipeline = [ + { + group: { + objectId: { + day: { $dayOfMonth: '$_updated_at' }, + month: { $month: '$_created_at' }, + year: { $year: '$_created_at' }, + }, + count: { $sum: 1 }, + }, + }, + ]; + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + const createdAt = new Date(obj1.createdAt); + expect(results[0].objectId.day).toEqual(createdAt.getUTCDate()); + expect(results[0].objectId.month).toEqual(createdAt.getMonth() + 1); + expect(results[0].objectId.year).toEqual(createdAt.getUTCFullYear()); + done(); + }); }); - it('group by date object transform', (done) => { + it('group by date object transform', done => { const obj1 = new TestObject(); const obj2 = new TestObject(); const obj3 = new TestObject(); - const pipeline = [{ - group: { - objectId: { day: { $dayOfMonth: "$updatedAt" }, month: { $month: "$createdAt" }, year: { $year: "$createdAt" } }, - count: { $sum: 1 } - } - }]; - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - const createdAt = new Date(obj1.createdAt); - expect(results[0].objectId.day).toEqual(createdAt.getUTCDate()); - expect(results[0].objectId.month).toEqual(createdAt.getMonth() + 1); - expect(results[0].objectId.year).toEqual(createdAt.getUTCFullYear()); - done(); - }); + const pipeline = [ + { + group: { + objectId: { + day: { $dayOfMonth: '$updatedAt' }, + month: { $month: '$createdAt' }, + year: { $year: '$createdAt' }, + }, + count: { $sum: 1 }, + }, + }, + ]; + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + const createdAt = new Date(obj1.createdAt); + expect(results[0].objectId.day).toEqual(createdAt.getUTCDate()); + expect(results[0].objectId.month).toEqual(createdAt.getMonth() + 1); + expect(results[0].objectId.year).toEqual(createdAt.getUTCFullYear()); + done(); + }); }); - it_exclude_dbs(['postgres'])('group and multiply transform', (done) => { + it_exclude_dbs(['postgres'])('group and multiply transform', done => { const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 }); const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); - const pipeline = [{ - group: { - objectId: null, - total: { $sum: { $multiply: [ '$quantity', '$price' ] } } - } - }]; - Parse.Object.saveAll([obj1, obj2]).then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(1); - expect(results[0].total).toEqual(45); - done(); - }); + const pipeline = [ + { + group: { + objectId: null, + total: { $sum: { $multiply: ['$quantity', '$price'] } }, + }, + }, + ]; + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].total).toEqual(45); + done(); + }); }); - it_exclude_dbs(['postgres'])('project and multiply transform', (done) => { + it_exclude_dbs(['postgres'])('project and multiply transform', done => { const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 }); const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); const pipeline = [ { - match: { quantity: { $exists: true } } + match: { quantity: { $exists: true } }, }, { project: { name: 1, - total: { $multiply: [ '$quantity', '$price' ] } - } - } + total: { $multiply: ['$quantity', '$price'] }, + }, + }, ]; - Parse.Object.saveAll([obj1, obj2]).then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(2); - if (results[0].name === 'item a') { - expect(results[0].total).toEqual(20); - expect(results[1].total).toEqual(25); - } - else { - expect(results[0].total).toEqual(25); - expect(results[1].total).toEqual(20); - } - done(); - }); + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(2); + if (results[0].name === 'item a') { + expect(results[0].total).toEqual(20); + expect(results[1].total).toEqual(25); + } else { + expect(results[0].total).toEqual(25); + expect(results[1].total).toEqual(20); + } + done(); + }); }); - it_exclude_dbs(['postgres'])('project without objectId transform', (done) => { + it_exclude_dbs(['postgres'])('project without objectId transform', done => { const obj1 = new TestObject({ name: 'item a', quantity: 2, price: 10 }); const obj2 = new TestObject({ name: 'item b', quantity: 5, price: 5 }); const pipeline = [ { - match: { quantity: { $exists: true } } + match: { quantity: { $exists: true } }, }, { project: { objectId: 0, - total: { $multiply: [ '$quantity', '$price' ] } - } + total: { $multiply: ['$quantity', '$price'] }, + }, }, { - sort: { total: 1 } - } + sort: { total: 1 }, + }, ]; - Parse.Object.saveAll([obj1, obj2]).then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(2); - expect(results[0].total).toEqual(20); - expect(results[0].objectId).toEqual(undefined); - expect(results[1].total).toEqual(25); - expect(results[1].objectId).toEqual(undefined); - done(); - }); + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(2); + expect(results[0].total).toEqual(20); + expect(results[0].objectId).toEqual(undefined); + expect(results[1].total).toEqual(25); + expect(results[1].objectId).toEqual(undefined); + done(); + }); }); - it_exclude_dbs(['postgres'])('project updatedAt only transform', (done) => { - const pipeline = [{ - project: { objectId: 0, updatedAt: 1 } - }]; + it_exclude_dbs(['postgres'])('project updatedAt only transform', done => { + const pipeline = [ + { + project: { objectId: 0, updatedAt: 1 }, + }, + ]; const query = new Parse.Query(TestObject); - query.aggregate(pipeline).then((results) => { + query.aggregate(pipeline).then(results => { expect(results.length).toEqual(4); for (let i = 0; i < results.length; i++) { const item = results[i]; @@ -300,182 +367,207 @@ describe('Parse.Query Aggregate testing', () => { }); }); - it_exclude_dbs(['postgres'])('cannot group by date field (excluding createdAt and updatedAt)', (done) => { - const obj1 = new TestObject({ dateField: new Date(1990, 11, 1) }); - const obj2 = new TestObject({ dateField: new Date(1990, 5, 1) }); - const obj3 = new TestObject({ dateField: new Date(1990, 11, 1) }); - const pipeline = [{ - group: { - objectId: { day: { $dayOfMonth: "$dateField" }, month: { $month: "$dateField" }, year: { $year: "$dateField" } }, - count: { $sum: 1 } - } - }]; - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then(done.fail).catch((error) => { - expect(error.code).toEqual(Parse.Error.INVALID_QUERY); - done(); - }); - }); + it_exclude_dbs(['postgres'])( + 'cannot group by date field (excluding createdAt and updatedAt)', + done => { + const obj1 = new TestObject({ dateField: new Date(1990, 11, 1) }); + const obj2 = new TestObject({ dateField: new Date(1990, 5, 1) }); + const obj3 = new TestObject({ dateField: new Date(1990, 11, 1) }); + const pipeline = [ + { + group: { + objectId: { + day: { $dayOfMonth: '$dateField' }, + month: { $month: '$dateField' }, + year: { $year: '$dateField' }, + }, + count: { $sum: 1 }, + }, + }, + ]; + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(done.fail) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_QUERY); + done(); + }); + } + ); - it('group by pointer', (done) => { + it('group by pointer', done => { const pointer1 = new TestObject(); const pointer2 = new TestObject(); const obj1 = new TestObject({ pointer: pointer1 }); const obj2 = new TestObject({ pointer: pointer2 }); const obj3 = new TestObject({ pointer: pointer1 }); - const pipeline = [ - { group: { objectId: '$pointer' } } - ]; - Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]).then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(3); - expect(results.some(result => result.objectId === pointer1.id)).toEqual(true); - expect(results.some(result => result.objectId === pointer2.id)).toEqual(true); - expect(results.some(result => result.objectId === null)).toEqual(true); - done(); - }); + const pipeline = [{ group: { objectId: '$pointer' } }]; + Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(3); + expect(results.some(result => result.objectId === pointer1.id)).toEqual( + true + ); + expect(results.some(result => result.objectId === pointer2.id)).toEqual( + true + ); + expect(results.some(result => result.objectId === null)).toEqual(true); + done(); + }); }); - it('group sum query', (done) => { + it('group sum query', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: { objectId: null, total: { $sum: '$score' } }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results[0].hasOwnProperty('objectId')).toBe(true); expect(resp.results[0].objectId).toBe(null); expect(resp.results[0].total).toBe(50); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('group count query', (done) => { + it('group count query', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: { objectId: null, total: { $sum: 1 } }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results[0].hasOwnProperty('objectId')).toBe(true); expect(resp.results[0].objectId).toBe(null); expect(resp.results[0].total).toBe(4); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('group min query', (done) => { + it('group min query', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: { objectId: null, minScore: { $min: '$score' } }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results[0].hasOwnProperty('objectId')).toBe(true); expect(resp.results[0].objectId).toBe(null); expect(resp.results[0].minScore).toBe(10); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('group max query', (done) => { + it('group max query', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: { objectId: null, maxScore: { $max: '$score' } }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results[0].hasOwnProperty('objectId')).toBe(true); expect(resp.results[0].objectId).toBe(null); expect(resp.results[0].maxScore).toBe(20); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('group avg query', (done) => { + it('group avg query', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: { objectId: null, avgScore: { $avg: '$score' } }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results[0].hasOwnProperty('objectId')).toBe(true); expect(resp.results[0].objectId).toBe(null); expect(resp.results[0].avgScore).toBe(12.5); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('limit query', (done) => { + it('limit query', done => { const options = Object.assign({}, masterKeyOptions, { body: { limit: 2, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(2); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('sort ascending query', (done) => { + it('sort ascending query', done => { const options = Object.assign({}, masterKeyOptions, { body: { sort: { name: 1 }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(4); expect(resp.results[0].name).toBe('bar'); expect(resp.results[1].name).toBe('dpl'); expect(resp.results[2].name).toBe('foo'); expect(resp.results[3].name).toBe('foo'); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('sort decending query', (done) => { + it('sort decending query', done => { const options = Object.assign({}, masterKeyOptions, { body: { sort: { name: -1 }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(4); expect(resp.results[0].name).toBe('foo'); expect(resp.results[1].name).toBe('foo'); expect(resp.results[2].name).toBe('dpl'); expect(resp.results[3].name).toBe('bar'); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('skip query', (done) => { + it('skip query', done => { const options = Object.assign({}, masterKeyOptions, { body: { skip: 2, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(2); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('match comparison date query', (done) => { + it('match comparison date query', done => { const today = new Date(); const yesterday = new Date(); const tomorrow = new Date(); @@ -484,86 +576,95 @@ describe('Parse.Query Aggregate testing', () => { const obj1 = new TestObject({ dateField: yesterday }); const obj2 = new TestObject({ dateField: today }); const obj3 = new TestObject({ dateField: tomorrow }); - const pipeline = [ - { match: { dateField: { $lt: tomorrow } } } - ]; - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toBe(2); - done(); - }); + const pipeline = [{ match: { dateField: { $lt: tomorrow } } }]; + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toBe(2); + done(); + }); }); - it('match comparison query', (done) => { + it('match comparison query', done => { const options = Object.assign({}, masterKeyOptions, { body: { - match: { score: { $gt: 15 }}, - } + match: { score: { $gt: 15 } }, + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(1); expect(resp.results[0].score).toBe(20); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('match multiple comparison query', (done) => { + it('match multiple comparison query', done => { const options = Object.assign({}, masterKeyOptions, { body: { - match: { score: { $gt: 5, $lt: 15 }}, - } + match: { score: { $gt: 5, $lt: 15 } }, + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(3); expect(resp.results[0].score).toBe(10); expect(resp.results[1].score).toBe(10); expect(resp.results[2].score).toBe(10); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('match complex comparison query', (done) => { + it('match complex comparison query', done => { const options = Object.assign({}, masterKeyOptions, { body: { - match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 }}, - } + match: { score: { $gt: 5, $lt: 15 }, views: { $gt: 850, $lt: 1000 } }, + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(1); expect(resp.results[0].score).toBe(10); expect(resp.results[0].views).toBe(900); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('match comparison and equality query', (done) => { + it('match comparison and equality query', done => { const options = Object.assign({}, masterKeyOptions, { body: { - match: { score: { $gt: 5, $lt: 15 }, views: 900}, - } + match: { score: { $gt: 5, $lt: 15 }, views: 900 }, + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(1); expect(resp.results[0].score).toBe(10); expect(resp.results[0].views).toBe(900); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('match $or query', (done) => { + it('match $or query', done => { const options = Object.assign({}, masterKeyOptions, { body: { - match: { $or: [{ score: { $gt: 15, $lt: 25 } }, { views: { $gt: 750, $lt: 850 } }]}, - } + match: { + $or: [ + { score: { $gt: 15, $lt: 25 } }, + { views: { $gt: 750, $lt: 850 } }, + ], + }, + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(2); // Match score { $gt: 15, $lt: 25 } expect(resp.results.some(result => result.score === 20)).toEqual(true); @@ -573,163 +674,182 @@ describe('Parse.Query Aggregate testing', () => { expect(resp.results.some(result => result.score === 10)).toEqual(true); expect(resp.results.some(result => result.views === 800)).toEqual(true); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('match objectId query', (done) => { + it('match objectId query', done => { const obj1 = new TestObject(); const obj2 = new TestObject(); - Parse.Object.saveAll([obj1, obj2]).then(() => { - const pipeline = [ - { match: { objectId: obj1.id } } - ]; - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(1); - expect(results[0].objectId).toEqual(obj1.id); - done(); - }); + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const pipeline = [{ match: { objectId: obj1.id } }]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].objectId).toEqual(obj1.id); + done(); + }); }); - it('match field query', (done) => { - const obj1 = new TestObject({ name: 'TestObject1'}); - const obj2 = new TestObject({ name: 'TestObject2'}); - Parse.Object.saveAll([obj1, obj2]).then(() => { - const pipeline = [ - { match: { name: 'TestObject1' } } - ]; - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(1); - expect(results[0].objectId).toEqual(obj1.id); - done(); - }); + it('match field query', done => { + const obj1 = new TestObject({ name: 'TestObject1' }); + const obj2 = new TestObject({ name: 'TestObject2' }); + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const pipeline = [{ match: { name: 'TestObject1' } }]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].objectId).toEqual(obj1.id); + done(); + }); }); - it('match pointer query', (done) => { + it('match pointer query', done => { const pointer1 = new PointerObject(); const pointer2 = new PointerObject(); const obj1 = new TestObject({ pointer: pointer1 }); const obj2 = new TestObject({ pointer: pointer2 }); const obj3 = new TestObject({ pointer: pointer1 }); - Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]).then(() => { - const pipeline = [ - { match: { pointer: pointer1.id } } - ]; - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(2); - expect(results[0].pointer.objectId).toEqual(pointer1.id); - expect(results[1].pointer.objectId).toEqual(pointer1.id); - expect(results.some(result => result.objectId === obj1.id)).toEqual(true); - expect(results.some(result => result.objectId === obj3.id)).toEqual(true); - done(); - }); + Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]) + .then(() => { + const pipeline = [{ match: { pointer: pointer1.id } }]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(2); + expect(results[0].pointer.objectId).toEqual(pointer1.id); + expect(results[1].pointer.objectId).toEqual(pointer1.id); + expect(results.some(result => result.objectId === obj1.id)).toEqual( + true + ); + expect(results.some(result => result.objectId === obj3.id)).toEqual( + true + ); + done(); + }); }); - it_exclude_dbs(['postgres'])('match exists query', (done) => { - const pipeline = [ - { match: { score: { $exists: true } } } - ]; + it_exclude_dbs(['postgres'])('match exists query', done => { + const pipeline = [{ match: { score: { $exists: true } } }]; const query = new Parse.Query(TestObject); - query.aggregate(pipeline).then((results) => { + query.aggregate(pipeline).then(results => { expect(results.length).toEqual(4); done(); }); }); - it('match date query - createdAt', (done) => { + it('match date query - createdAt', done => { const obj1 = new TestObject(); const obj2 = new TestObject(); - Parse.Object.saveAll([obj1, obj2]).then(() => { - const now = new Date(); - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const pipeline = [ - { match: { 'createdAt': { $gte: today } } } - ]; - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - // Four objects were created initially, we added two more. - expect(results.length).toEqual(6); - done(); - }); + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const now = new Date(); + const today = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + ); + const pipeline = [{ match: { createdAt: { $gte: today } } }]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + // Four objects were created initially, we added two more. + expect(results.length).toEqual(6); + done(); + }); }); - it('match date query - updatedAt', (done) => { + it('match date query - updatedAt', done => { const obj1 = new TestObject(); const obj2 = new TestObject(); - Parse.Object.saveAll([obj1, obj2]).then(() => { - const now = new Date(); - const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const pipeline = [ - { match: { 'updatedAt': { $gte: today } } } - ]; - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - // Four objects were added initially, we added two more. - expect(results.length).toEqual(6); - done(); - }); + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const now = new Date(); + const today = new Date( + now.getFullYear(), + now.getMonth(), + now.getDate() + ); + const pipeline = [{ match: { updatedAt: { $gte: today } } }]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + // Four objects were added initially, we added two more. + expect(results.length).toEqual(6); + done(); + }); }); - it('match date query - empty', (done) => { + it('match date query - empty', done => { const obj1 = new TestObject(); const obj2 = new TestObject(); - Parse.Object.saveAll([obj1, obj2]).then(() => { - const now = new Date(); - const future = new Date(now.getFullYear(), now.getMonth() + 1, now.getDate()); - const pipeline = [ - { match: { 'createdAt': future } } - ]; - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(0); - done(); - }); + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const now = new Date(); + const future = new Date( + now.getFullYear(), + now.getMonth() + 1, + now.getDate() + ); + const pipeline = [{ match: { createdAt: future } }]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(0); + done(); + }); }); - it_exclude_dbs(['postgres'])('match pointer with operator query', (done) => { + it_exclude_dbs(['postgres'])('match pointer with operator query', done => { const pointer = new PointerObject(); const obj1 = new TestObject({ pointer }); const obj2 = new TestObject({ pointer }); const obj3 = new TestObject(); - Parse.Object.saveAll([pointer, obj1, obj2, obj3]).then(() => { - const pipeline = [ - { match: { pointer: { $exists: true } } } - ]; - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(2); - expect(results[0].pointer.objectId).toEqual(pointer.id); - expect(results[1].pointer.objectId).toEqual(pointer.id); - expect(results.some(result => result.objectId === obj1.id)).toEqual(true); - expect(results.some(result => result.objectId === obj2.id)).toEqual(true); - done(); - }); + Parse.Object.saveAll([pointer, obj1, obj2, obj3]) + .then(() => { + const pipeline = [{ match: { pointer: { $exists: true } } }]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(2); + expect(results[0].pointer.objectId).toEqual(pointer.id); + expect(results[1].pointer.objectId).toEqual(pointer.id); + expect(results.some(result => result.objectId === obj1.id)).toEqual( + true + ); + expect(results.some(result => result.objectId === obj2.id)).toEqual( + true + ); + done(); + }); }); - it('project query', (done) => { + it('project query', done => { const options = Object.assign({}, masterKeyOptions, { body: { project: { name: 1 }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { - resp.results.forEach((result) => { + .then(resp => { + resp.results.forEach(result => { expect(result.objectId).not.toBe(undefined); expect(result.name).not.toBe(undefined); expect(result.sender).toBe(undefined); @@ -737,18 +857,19 @@ describe('Parse.Query Aggregate testing', () => { expect(result.score).toBe(undefined); }); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('multiple project query', (done) => { + it('multiple project query', done => { const options = Object.assign({}, masterKeyOptions, { body: { project: { name: 1, score: 1, sender: 1 }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { - resp.results.forEach((result) => { + .then(resp => { + resp.results.forEach(result => { expect(result.objectId).not.toBe(undefined); expect(result.name).not.toBe(undefined); expect(result.score).not.toBe(undefined); @@ -756,40 +877,44 @@ describe('Parse.Query Aggregate testing', () => { expect(result.size).toBe(undefined); }); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('project pointer query', (done) => { + it('project pointer query', done => { const pointer = new PointerObject(); const obj = new TestObject({ pointer, name: 'hello' }); - obj.save().then(() => { - const pipeline = [ - { match: { objectId: obj.id } }, - { project: { pointer: 1, name: 1, createdAt: 1 } } - ]; - const query = new Parse.Query(TestObject); - return query.aggregate(pipeline); - }).then((results) => { - expect(results.length).toEqual(1); - expect(results[0].name).toEqual('hello'); - expect(results[0].createdAt).not.toBe(undefined); - expect(results[0].pointer.objectId).toEqual(pointer.id); - done(); - }); + obj + .save() + .then(() => { + const pipeline = [ + { match: { objectId: obj.id } }, + { project: { pointer: 1, name: 1, createdAt: 1 } }, + ]; + const query = new Parse.Query(TestObject); + return query.aggregate(pipeline); + }) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].name).toEqual('hello'); + expect(results[0].createdAt).not.toBe(undefined); + expect(results[0].pointer.objectId).toEqual(pointer.id); + done(); + }); }); - it('project with group query', (done) => { + it('project with group query', done => { const options = Object.assign({}, masterKeyOptions, { body: { project: { score: 1 }, group: { objectId: '$score', score: { $sum: '$score' } }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(2); - resp.results.forEach((result) => { + resp.results.forEach(result => { expect(result.hasOwnProperty('objectId')).toBe(true); expect(result.name).toBe(undefined); expect(result.sender).toBe(undefined); @@ -803,149 +928,168 @@ describe('Parse.Query Aggregate testing', () => { } }); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('class does not exist return empty', (done) => { + it('class does not exist return empty', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: { objectId: null, total: { $sum: '$score' } }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/UnknownClass', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(0); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('field does not exist return empty', (done) => { + it('field does not exist return empty', done => { const options = Object.assign({}, masterKeyOptions, { body: { group: { objectId: null, total: { $sum: '$unknownfield' } }, - } + }, }); rp.get(Parse.serverURL + '/aggregate/UnknownClass', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(0); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('distinct query', (done) => { + it('distinct query', done => { const options = Object.assign({}, masterKeyOptions, { - body: { distinct: 'score' } + body: { distinct: 'score' }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(2); expect(resp.results.includes(10)).toBe(true); expect(resp.results.includes(20)).toBe(true); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('distinct query with where', (done) => { + it('distinct query with where', done => { const options = Object.assign({}, masterKeyOptions, { body: { distinct: 'score', where: { - name: 'bar' - } - } + name: 'bar', + }, + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results[0]).toBe(10); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('distinct query with where string', (done) => { + it('distinct query with where string', done => { const options = Object.assign({}, masterKeyOptions, { body: { distinct: 'score', - where: JSON.stringify({name:'bar'}), - } + where: JSON.stringify({ name: 'bar' }), + }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results[0]).toBe(10); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('distinct nested', (done) => { + it('distinct nested', done => { const options = Object.assign({}, masterKeyOptions, { - body: { distinct: 'sender.group' } + body: { distinct: 'sender.group' }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(2); expect(resp.results.includes('A')).toBe(true); expect(resp.results.includes('B')).toBe(true); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('distinct pointer', (done) => { + it('distinct pointer', done => { const pointer1 = new PointerObject(); const pointer2 = new PointerObject(); const obj1 = new TestObject({ pointer: pointer1 }); const obj2 = new TestObject({ pointer: pointer2 }); const obj3 = new TestObject({ pointer: pointer1 }); - Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]).then(() => { - const query = new Parse.Query(TestObject); - return query.distinct('pointer'); - }).then((results) => { - expect(results.length).toEqual(2); - expect(results.some(result => result.objectId === pointer1.id)).toEqual(true); - expect(results.some(result => result.objectId === pointer2.id)).toEqual(true); - done(); - }); + Parse.Object.saveAll([pointer1, pointer2, obj1, obj2, obj3]) + .then(() => { + const query = new Parse.Query(TestObject); + return query.distinct('pointer'); + }) + .then(results => { + expect(results.length).toEqual(2); + expect(results.some(result => result.objectId === pointer1.id)).toEqual( + true + ); + expect(results.some(result => result.objectId === pointer2.id)).toEqual( + true + ); + done(); + }); }); - it('distinct class does not exist return empty', (done) => { + it('distinct class does not exist return empty', done => { const options = Object.assign({}, masterKeyOptions, { - body: { distinct: 'unknown' } + body: { distinct: 'unknown' }, }); rp.get(Parse.serverURL + '/aggregate/UnknownClass', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(0); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('distinct field does not exist return empty', (done) => { + it('distinct field does not exist return empty', done => { const options = Object.assign({}, masterKeyOptions, { - body: { distinct: 'unknown' } + body: { distinct: 'unknown' }, }); const obj = new TestObject(); - obj.save().then(() => { - return rp.get(Parse.serverURL + '/aggregate/TestObject', options); - }).then((resp) => { - expect(resp.results.length).toBe(0); - done(); - }).catch(done.fail); + obj + .save() + .then(() => { + return rp.get(Parse.serverURL + '/aggregate/TestObject', options); + }) + .then(resp => { + expect(resp.results.length).toBe(0); + done(); + }) + .catch(done.fail); }); - it('distinct array', (done) => { + it('distinct array', done => { const options = Object.assign({}, masterKeyOptions, { - body: { distinct: 'size' } + body: { distinct: 'size' }, }); rp.get(Parse.serverURL + '/aggregate/TestObject', options) - .then((resp) => { + .then(resp => { expect(resp.results.length).toBe(3); expect(resp.results.includes('S')).toBe(true); expect(resp.results.includes('M')).toBe(true); expect(resp.results.includes('L')).toBe(true); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); - it('distinct null field', (done) => { + it('distinct null field', done => { const options = Object.assign({}, masterKeyOptions, { - body: { distinct: 'distinctField' } + body: { distinct: 'distinctField' }, }); const user1 = new Parse.User(); user1.setUsername('distinct_1'); @@ -956,26 +1100,31 @@ describe('Parse.Query Aggregate testing', () => { user2.setUsername('distinct_2'); user2.setPassword('password'); user2.set('distinctField', null); - user1.signUp().then(() => { - return user2.signUp(); - }).then(() => { - return rp.get(Parse.serverURL + '/aggregate/_User', options); - }).then((resp) => { - expect(resp.results.length).toEqual(1); - expect(resp.results).toEqual(['one']); - done(); - }).catch(done.fail); + user1 + .signUp() + .then(() => { + return user2.signUp(); + }) + .then(() => { + return rp.get(Parse.serverURL + '/aggregate/_User', options); + }) + .then(resp => { + expect(resp.results.length).toEqual(1); + expect(resp.results).toEqual(['one']); + done(); + }) + .catch(done.fail); }); - it('does not return sensitive hidden properties', (done) => { + it('does not return sensitive hidden properties', done => { const options = Object.assign({}, masterKeyOptions, { body: { match: { score: { - $gt: 5 - } + $gt: 5, + }, }, - } + }, }); const username = 'leaky_user'; @@ -985,73 +1134,88 @@ describe('Parse.Query Aggregate testing', () => { user.setUsername(username); user.setPassword('password'); user.set('score', score); - user.signUp().then(function() { - return rp.get(Parse.serverURL + '/aggregate/_User', options); - }).then(function(resp) { - expect(resp.results.length).toBe(1); - const result = resp.results[0]; - - // verify server-side keys are not present... - expect(result._hashed_password).toBe(undefined); - expect(result._wperm).toBe(undefined); - expect(result._rperm).toBe(undefined); - expect(result._acl).toBe(undefined); - expect(result._created_at).toBe(undefined); - expect(result._updated_at).toBe(undefined); - - // verify createdAt, updatedAt and others are present - expect(result.createdAt).not.toBe(undefined); - expect(result.updatedAt).not.toBe(undefined); - expect(result.objectId).not.toBe(undefined); - expect(result.username).toBe(username); - expect(result.score).toBe(score); + user + .signUp() + .then(function() { + return rp.get(Parse.serverURL + '/aggregate/_User', options); + }) + .then(function(resp) { + expect(resp.results.length).toBe(1); + const result = resp.results[0]; + + // verify server-side keys are not present... + expect(result._hashed_password).toBe(undefined); + expect(result._wperm).toBe(undefined); + expect(result._rperm).toBe(undefined); + expect(result._acl).toBe(undefined); + expect(result._created_at).toBe(undefined); + expect(result._updated_at).toBe(undefined); + + // verify createdAt, updatedAt and others are present + expect(result.createdAt).not.toBe(undefined); + expect(result.updatedAt).not.toBe(undefined); + expect(result.objectId).not.toBe(undefined); + expect(result.username).toBe(username); + expect(result.score).toBe(score); - done(); - }).catch(function(err) { - fail(err); - }); + done(); + }) + .catch(function(err) { + fail(err); + }); }); - it_exclude_dbs(['postgres'])('aggregate allow multiple of same stage', (done) => { - const pointer1 = new TestObject({ value: 1}); - const pointer2 = new TestObject({ value: 2}); - const pointer3 = new TestObject({ value: 3}); - - const obj1 = new TestObject({ pointer: pointer1, name: 'Hello' }); - const obj2 = new TestObject({ pointer: pointer2, name: 'Hello' }); - const obj3 = new TestObject({ pointer: pointer3, name: 'World' }); - - const options = Object.assign({}, masterKeyOptions, { - body: [{ - match: { name: "Hello" }, - }, { - // Transform className$objectId to objectId and store in new field tempPointer - project: { - tempPointer: { $substr: [ "$_p_pointer", 11, -1 ] }, // Remove TestObject$ - }, - }, { - // Left Join, replace objectId stored in tempPointer with an actual object - lookup: { - from: "test_TestObject", - localField: "tempPointer", - foreignField: "_id", - as: "tempPointer" - }, - }, { - // lookup returns an array, Deconstructs an array field to objects - unwind: { - path: "$tempPointer", - }, - }, { - match : { "tempPointer.value" : 2 }, - }] - }); - Parse.Object.saveAll([pointer1, pointer2, pointer3, obj1, obj2, obj3]).then(() => { - return rp.get(Parse.serverURL + '/aggregate/TestObject', options); - }).then((resp) => { - expect(resp.results.length).toEqual(1); - expect(resp.results[0].tempPointer.value).toEqual(2); - done(); - }); - }); + it_exclude_dbs(['postgres'])( + 'aggregate allow multiple of same stage', + done => { + const pointer1 = new TestObject({ value: 1 }); + const pointer2 = new TestObject({ value: 2 }); + const pointer3 = new TestObject({ value: 3 }); + + const obj1 = new TestObject({ pointer: pointer1, name: 'Hello' }); + const obj2 = new TestObject({ pointer: pointer2, name: 'Hello' }); + const obj3 = new TestObject({ pointer: pointer3, name: 'World' }); + + const options = Object.assign({}, masterKeyOptions, { + body: [ + { + match: { name: 'Hello' }, + }, + { + // Transform className$objectId to objectId and store in new field tempPointer + project: { + tempPointer: { $substr: ['$_p_pointer', 11, -1] }, // Remove TestObject$ + }, + }, + { + // Left Join, replace objectId stored in tempPointer with an actual object + lookup: { + from: 'test_TestObject', + localField: 'tempPointer', + foreignField: '_id', + as: 'tempPointer', + }, + }, + { + // lookup returns an array, Deconstructs an array field to objects + unwind: { + path: '$tempPointer', + }, + }, + { + match: { 'tempPointer.value': 2 }, + }, + ], + }); + Parse.Object.saveAll([pointer1, pointer2, pointer3, obj1, obj2, obj3]) + .then(() => { + return rp.get(Parse.serverURL + '/aggregate/TestObject', options); + }) + .then(resp => { + expect(resp.results.length).toEqual(1); + expect(resp.results[0].tempPointer.value).toEqual(2); + done(); + }); + } + ); }); diff --git a/spec/ParseQuery.FullTextSearch.spec.js b/spec/ParseQuery.FullTextSearch.spec.js index eac29c9268..2bd74c037d 100644 --- a/spec/ParseQuery.FullTextSearch.spec.js +++ b/spec/ParseQuery.FullTextSearch.spec.js @@ -1,9 +1,13 @@ 'use strict'; -const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; -const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter').default; -const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; +const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter') + .default; +const mongoURI = + 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; +const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') + .default; +const postgresURI = + 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; const Parse = require('parse/node'); const rp = require('request-promise'); let databaseAdapter; @@ -29,12 +33,12 @@ const fullTextHelper = () => { const requests = []; for (const i in subjects) { const request = { - method: "POST", + method: 'POST', body: { subject: subjects[i], comment: subjects[i], }, - path: "/1/classes/TestObject" + path: '/1/classes/TestObject', }; requests.push(request); } @@ -42,471 +46,540 @@ const fullTextHelper = () => { appId: 'test', restAPIKey: 'test', publicServerURL: 'http://localhost:8378/1', - databaseAdapter + databaseAdapter, }).then(() => { return rp.post({ url: 'http://localhost:8378/1/batch', body: { - requests + requests, }, json: true, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } + 'X-Parse-REST-API-Key': 'test', + }, }); }); -} +}; describe('Parse.Query Full Text Search testing', () => { - it('fullTextSearch: $search', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'coffee' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(3); - done(); - }, done.fail); - }); - - it('fullTextSearch: $search, sort', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'coffee' - } - } - } - }; - const order = '$score'; - const keys = '$score'; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, order, keys, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(3); - expect(resp.results[0].score); - expect(resp.results[1].score); - expect(resp.results[2].score); - done(); - }, done.fail); - }); - - it('fullTextSearch: $language', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'leche', - $language: 'spanish' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(2); - done(); - }, done.fail); + it('fullTextSearch: $search', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'coffee', + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(3); + done(); + }, done.fail); }); - it('fullTextSearch: $diacriticSensitive', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'CAFÉ', - $diacriticSensitive: true - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(1); - done(); - }, done.fail); + it('fullTextSearch: $search, sort', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'coffee', + }, + }, + }, + }; + const order = '$score'; + const keys = '$score'; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, order, keys, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(3); + expect(resp.results[0].score); + expect(resp.results[1].score); + expect(resp.results[2].score); + done(); + }, done.fail); }); - it('fullTextSearch: $search, invalid input', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: true - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); + it('fullTextSearch: $language', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'leche', + $language: 'spanish', + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(2); + done(); + }, done.fail); }); - it('fullTextSearch: $language, invalid input', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'leche', - $language: true - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); + it('fullTextSearch: $diacriticSensitive', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: true, + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(1); + done(); + }, done.fail); }); - it('fullTextSearch: $caseSensitive, invalid input', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'Coffee', - $caseSensitive: 'string' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } + it('fullTextSearch: $search, invalid input', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: true, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); }); - it('fullTextSearch: $diacriticSensitive, invalid input', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'CAFÉ', - $diacriticSensitive: 'string' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } + it('fullTextSearch: $language, invalid input', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'leche', + $language: true, + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); }); - }).then((resp) => { - fail(`no request should succeed: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); - }); }); -}); -describe_only_db('mongo')('[mongodb] Parse.Query Full Text Search testing', () => { - it('fullTextSearch: does not create text index if compound index exist', (done) => { - fullTextHelper().then(() => { - return databaseAdapter.dropAllIndexes('TestObject'); - }).then(() => { - return databaseAdapter.getIndexes('TestObject'); - }).then((indexes) => { - expect(indexes.length).toEqual(1); - return databaseAdapter.createIndex('TestObject', {subject: 'text', comment: 'text'}); - }).then(() => { - return databaseAdapter.getIndexes('TestObject'); - }).then((indexes) => { - expect(indexes.length).toEqual(2); - const where = { - subject: { - $text: { - $search: { - $term: 'coffee' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toEqual(3); - return databaseAdapter.getIndexes('TestObject'); - }).then((indexes) => { - expect(indexes.length).toEqual(2); - rp.get({ - url: 'http://localhost:8378/1/schemas/TestObject', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - json: true, - }, (error, response, body) => { - expect(body.indexes._id_).toBeDefined(); - expect(body.indexes._id_._id).toEqual(1); - expect(body.indexes.subject_text_comment_text).toBeDefined(); - expect(body.indexes.subject_text_comment_text.subject).toEqual('text'); - expect(body.indexes.subject_text_comment_text.comment).toEqual('text'); + it('fullTextSearch: $caseSensitive, invalid input', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'Coffee', + $caseSensitive: 'string', + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); done(); }); - }).catch(done.fail); }); - it('fullTextSearch: does not create text index if schema compound index exist', (done) => { - fullTextHelper().then(() => { - return databaseAdapter.dropAllIndexes('TestObject'); - }).then(() => { - return databaseAdapter.getIndexes('TestObject'); - }).then((indexes) => { - expect(indexes.length).toEqual(1); - return rp.put({ - url: 'http://localhost:8378/1/schemas/TestObject', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test', - 'X-Parse-Master-Key': 'test', - }, - body: { - indexes: { - text_test: { subject: 'text', comment: 'text'}, + it('fullTextSearch: $diacriticSensitive, invalid input', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: 'string', + }, + }, }, - }, - }); - }).then(() => { - return databaseAdapter.getIndexes('TestObject'); - }).then((indexes) => { - expect(indexes.length).toEqual(2); - const where = { - subject: { - $text: { - $search: { - $term: 'coffee' - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toEqual(3); - return databaseAdapter.getIndexes('TestObject'); - }).then((indexes) => { - expect(indexes.length).toEqual(2); - rp.get({ - url: 'http://localhost:8378/1/schemas/TestObject', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - json: true, - }, (error, response, body) => { - expect(body.indexes._id_).toBeDefined(); - expect(body.indexes._id_._id).toEqual(1); - expect(body.indexes.text_test).toBeDefined(); - expect(body.indexes.text_test.subject).toEqual('text'); - expect(body.indexes.text_test.comment).toEqual('text'); + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + fail(`no request should succeed: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); done(); }); - }).catch(done.fail); }); +}); - it('fullTextSearch: $diacriticSensitive - false', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'CAFÉ', - $diacriticSensitive: false +describe_only_db('mongo')( + '[mongodb] Parse.Query Full Text Search testing', + () => { + it('fullTextSearch: does not create text index if compound index exist', done => { + fullTextHelper() + .then(() => { + return databaseAdapter.dropAllIndexes('TestObject'); + }) + .then(() => { + return databaseAdapter.getIndexes('TestObject'); + }) + .then(indexes => { + expect(indexes.length).toEqual(1); + return databaseAdapter.createIndex('TestObject', { + subject: 'text', + comment: 'text', + }); + }) + .then(() => { + return databaseAdapter.getIndexes('TestObject'); + }) + .then(indexes => { + expect(indexes.length).toEqual(2); + const where = { + subject: { + $text: { + $search: { + $term: 'coffee', + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + expect(resp.results.length).toEqual(3); + return databaseAdapter.getIndexes('TestObject'); + }) + .then(indexes => { + expect(indexes.length).toEqual(2); + rp.get( + { + url: 'http://localhost:8378/1/schemas/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + json: true, + }, + (error, response, body) => { + expect(body.indexes._id_).toBeDefined(); + expect(body.indexes._id_._id).toEqual(1); + expect(body.indexes.subject_text_comment_text).toBeDefined(); + expect(body.indexes.subject_text_comment_text.subject).toEqual( + 'text' + ); + expect(body.indexes.subject_text_comment_text.comment).toEqual( + 'text' + ); + done(); } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(2); - done(); - }, done.fail); - }); + ); + }) + .catch(done.fail); + }); - it('fullTextSearch: $caseSensitive', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'Coffee', - $caseSensitive: true + it('fullTextSearch: does not create text index if schema compound index exist', done => { + fullTextHelper() + .then(() => { + return databaseAdapter.dropAllIndexes('TestObject'); + }) + .then(() => { + return databaseAdapter.getIndexes('TestObject'); + }) + .then(indexes => { + expect(indexes.length).toEqual(1); + return rp.put({ + url: 'http://localhost:8378/1/schemas/TestObject', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + 'X-Parse-Master-Key': 'test', + }, + body: { + indexes: { + text_test: { subject: 'text', comment: 'text' }, + }, + }, + }); + }) + .then(() => { + return databaseAdapter.getIndexes('TestObject'); + }) + .then(indexes => { + expect(indexes.length).toEqual(2); + const where = { + subject: { + $text: { + $search: { + $term: 'coffee', + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + expect(resp.results.length).toEqual(3); + return databaseAdapter.getIndexes('TestObject'); + }) + .then(indexes => { + expect(indexes.length).toEqual(2); + rp.get( + { + url: 'http://localhost:8378/1/schemas/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + json: true, + }, + (error, response, body) => { + expect(body.indexes._id_).toBeDefined(); + expect(body.indexes._id_._id).toEqual(1); + expect(body.indexes.text_test).toBeDefined(); + expect(body.indexes.text_test.subject).toEqual('text'); + expect(body.indexes.text_test.comment).toEqual('text'); + done(); } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - expect(resp.results.length).toBe(1); - done(); - }, done.fail); - }); -}); + ); + }) + .catch(done.fail); + }); -describe_only_db('postgres')('[postgres] Parse.Query Full Text Search testing', () => { - it('fullTextSearch: $diacriticSensitive - false', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'CAFÉ', - $diacriticSensitive: false - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`$diacriticSensitive - false should not supported: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); + it('fullTextSearch: $diacriticSensitive - false', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: false, + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(2); + done(); + }, done.fail); }); - }); - it('fullTextSearch: $caseSensitive', (done) => { - fullTextHelper().then(() => { - const where = { - subject: { - $text: { - $search: { - $term: 'Coffee', - $caseSensitive: true - } - } - } - }; - return rp.post({ - url: 'http://localhost:8378/1/classes/TestObject', - json: { where, '_method': 'GET' }, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'test' - } - }); - }).then((resp) => { - fail(`$caseSensitive should not supported: ${JSON.stringify(resp)}`); - done(); - }).catch((err) => { - expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); - done(); + it('fullTextSearch: $caseSensitive', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'Coffee', + $caseSensitive: true, + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + expect(resp.results.length).toBe(1); + done(); + }, done.fail); }); - }); -}); + } +); + +describe_only_db('postgres')( + '[postgres] Parse.Query Full Text Search testing', + () => { + it('fullTextSearch: $diacriticSensitive - false', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'CAFÉ', + $diacriticSensitive: false, + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + fail( + `$diacriticSensitive - false should not supported: ${JSON.stringify( + resp + )}` + ); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); + + it('fullTextSearch: $caseSensitive', done => { + fullTextHelper() + .then(() => { + const where = { + subject: { + $text: { + $search: { + $term: 'Coffee', + $caseSensitive: true, + }, + }, + }, + }; + return rp.post({ + url: 'http://localhost:8378/1/classes/TestObject', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'test', + }, + }); + }) + .then(resp => { + fail(`$caseSensitive should not supported: ${JSON.stringify(resp)}`); + done(); + }) + .catch(err => { + expect(err.error.code).toEqual(Parse.Error.INVALID_JSON); + done(); + }); + }); + } +); diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index d267216595..40d8dde106 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -10,16 +10,16 @@ const rp = require('request-promise'); const masterKeyHeaders = { 'X-Parse-Application-Id': 'test', 'X-Parse-Rest-API-Key': 'test', - 'X-Parse-Master-Key': 'test' -} + 'X-Parse-Master-Key': 'test', +}; const masterKeyOptions = { headers: masterKeyHeaders, - json: true -} + json: true, +}; describe('Parse.Query testing', () => { - it("basic query", function(done) { + it('basic query', function(done) { const baz = new TestObject({ foo: 'baz' }); const qux = new TestObject({ foo: 'qux' }); Parse.Object.saveAll([baz, qux]).then(function() { @@ -33,18 +33,18 @@ describe('Parse.Query testing', () => { }); }); - it("searching for null", function(done) { + it('searching for null', function(done) { const baz = new TestObject({ foo: null }); const qux = new TestObject({ foo: 'qux' }); - const qux2 = new TestObject({ }); + const qux2 = new TestObject({}); Parse.Object.saveAll([baz, qux, qux2]).then(function() { const query = new Parse.Query(TestObject); query.equalTo('foo', null); query.find().then(function(results) { equal(results.length, 2); qux.set('foo', null); - qux.save().then(function () { - query.find().then(function (results) { + qux.save().then(function() { + query.find().then(function(results) { equal(results.length, 3); done(); }); @@ -53,18 +53,18 @@ describe('Parse.Query testing', () => { }); }); - it("searching for not null", function(done) { + it('searching for not null', function(done) { const baz = new TestObject({ foo: null }); const qux = new TestObject({ foo: 'qux' }); - const qux2 = new TestObject({ }); + const qux2 = new TestObject({}); Parse.Object.saveAll([baz, qux, qux2]).then(function() { const query = new Parse.Query(TestObject); query.notEqualTo('foo', null); query.find().then(function(results) { equal(results.length, 1); qux.set('foo', null); - qux.save().then(function () { - query.find().then(function (results) { + qux.save().then(function() { + query.find().then(function(results) { equal(results.length, 0); done(); }); @@ -73,154 +73,172 @@ describe('Parse.Query testing', () => { }); }); - it("notEqualTo with Relation is working", function(done) { + it('notEqualTo with Relation is working', function(done) { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); + user.setPassword('asdf'); + user.setUsername('zxcv'); const user1 = new Parse.User(); - user1.setPassword("asdf"); - user1.setUsername("qwerty"); + user1.setPassword('asdf'); + user1.setUsername('qwerty'); const user2 = new Parse.User(); - user2.setPassword("asdf"); - user2.setUsername("asdf"); + user2.setPassword('asdf'); + user2.setUsername('asdf'); - const Cake = Parse.Object.extend("Cake"); + const Cake = Parse.Object.extend('Cake'); const cake1 = new Cake(); const cake2 = new Cake(); const cake3 = new Cake(); + user + .signUp() + .then(function() { + return user1.signUp(); + }) + .then(function() { + return user2.signUp(); + }) + .then(function() { + const relLike1 = cake1.relation('liker'); + relLike1.add([user, user1]); - user.signUp().then(function(){ - return user1.signUp(); - }).then(function(){ - return user2.signUp(); - }).then(function(){ - const relLike1 = cake1.relation("liker"); - relLike1.add([user, user1]); - - const relDislike1 = cake1.relation("hater"); - relDislike1.add(user2); - - return cake1.save(); - }).then(function(){ - const rellike2 = cake2.relation("liker"); - rellike2.add([user, user1]); - - const relDislike2 = cake2.relation("hater"); - relDislike2.add(user2); - - const relSomething = cake2.relation("something"); - relSomething.add(user); - - return cake2.save(); - }).then(function(){ - const rellike3 = cake3.relation("liker"); - rellike3.add(user); - - const relDislike3 = cake3.relation("hater"); - relDislike3.add([user1, user2]); - return cake3.save(); - }).then(function(){ - const 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(){ - const 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(){ - const 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(){ - const 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(){ - const 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(){ - const 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); - const cake = results[0]; - expect(cake.id).toBe(cake3.id); - }); - }).then(function(){ - const query = new Parse.Query(Cake); - // Exclude user1 - query.notEqualTo("liker", user1); - // Only cake1 - query.equalTo("objectId", cake1.id) - // user1 likes cake1 so this should return no results - return query.find().then(function(results){ - equal(results.length, 0); - }); - }).then(function(){ - const query = new Parse.Query(Cake); - query.notEqualTo("hater", user2); - query.notEqualTo("liker", user2); - // user2 doesn't like any cake so this should be 0 - return query.find().then(function(results){ - equal(results.length, 0); - }); - }).then(function(){ - const query = new Parse.Query(Cake); - query.equalTo("hater", user); - query.equalTo("liker", user); - // user doesn't hate any cake so this should be 0 - return query.find().then(function(results){ - equal(results.length, 0); - }); - }).then(function(){ - const query = new Parse.Query(Cake); - query.equalTo("hater", null); - query.equalTo("liker", null); - // user doesn't hate any cake so this should be 0 - return query.find().then(function(results){ - equal(results.length, 0); - }); - }).then(function(){ - const query = new Parse.Query(Cake); - query.equalTo("something", null); - // user doesn't hate any cake so this should be 0 - return query.find().then(function(results){ - equal(results.length, 0); + const relDislike1 = cake1.relation('hater'); + relDislike1.add(user2); + + return cake1.save(); + }) + .then(function() { + const rellike2 = cake2.relation('liker'); + rellike2.add([user, user1]); + + const relDislike2 = cake2.relation('hater'); + relDislike2.add(user2); + + const relSomething = cake2.relation('something'); + relSomething.add(user); + + return cake2.save(); + }) + .then(function() { + const rellike3 = cake3.relation('liker'); + rellike3.add(user); + + const relDislike3 = cake3.relation('hater'); + relDislike3.add([user1, user2]); + return cake3.save(); + }) + .then(function() { + const 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() { + const 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() { + const 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() { + const 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() { + const 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() { + const 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); + const cake = results[0]; + expect(cake.id).toBe(cake3.id); + }); + }) + .then(function() { + const query = new Parse.Query(Cake); + // Exclude user1 + query.notEqualTo('liker', user1); + // Only cake1 + query.equalTo('objectId', cake1.id); + // user1 likes cake1 so this should return no results + return query.find().then(function(results) { + equal(results.length, 0); + }); + }) + .then(function() { + const query = new Parse.Query(Cake); + query.notEqualTo('hater', user2); + query.notEqualTo('liker', user2); + // user2 doesn't like any cake so this should be 0 + return query.find().then(function(results) { + equal(results.length, 0); + }); + }) + .then(function() { + const query = new Parse.Query(Cake); + query.equalTo('hater', user); + query.equalTo('liker', user); + // user doesn't hate any cake so this should be 0 + return query.find().then(function(results) { + equal(results.length, 0); + }); + }) + .then(function() { + const query = new Parse.Query(Cake); + query.equalTo('hater', null); + query.equalTo('liker', null); + // user doesn't hate any cake so this should be 0 + return query.find().then(function(results) { + equal(results.length, 0); + }); + }) + .then(function() { + const query = new Parse.Query(Cake); + query.equalTo('something', null); + // user doesn't hate any cake so this should be 0 + return query.find().then(function(results) { + equal(results.length, 0); + }); + }) + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + done(); }); - }).then(function(){ - done(); - }).catch((err) => { - jfail(err); - done(); - }) }); - it("query with limit", function(done) { + it('query with limit', function(done) { const baz = new TestObject({ foo: 'baz' }); const qux = new TestObject({ foo: 'qux' }); Parse.Object.saveAll([baz, qux]).then(function() { @@ -233,10 +251,10 @@ describe('Parse.Query testing', () => { }); }); - it("query with limit equal to maxlimit", function(done) { + it('query with limit equal to maxlimit', function(done) { const baz = new TestObject({ foo: 'baz' }); const qux = new TestObject({ foo: 'qux' }); - reconfigureServer({ maxLimit: 1 }) + reconfigureServer({ maxLimit: 1 }); Parse.Object.saveAll([baz, qux]).then(function() { const query = new Parse.Query(TestObject); query.limit(1); @@ -247,10 +265,10 @@ describe('Parse.Query testing', () => { }); }); - it("query with limit exceeding maxlimit", function(done) { + it('query with limit exceeding maxlimit', function(done) { const baz = new TestObject({ foo: 'baz' }); const qux = new TestObject({ foo: 'qux' }); - reconfigureServer({ maxLimit: 1 }) + reconfigureServer({ maxLimit: 1 }); Parse.Object.saveAll([baz, qux]).then(function() { const query = new Parse.Query(TestObject); query.limit(2); @@ -261,9 +279,9 @@ describe('Parse.Query testing', () => { }); }); - it("containedIn object array queries", function(done) { + it('containedIn object array queries', function(done) { const messageList = []; - for (let i = 0; i < 4; ++i) { + for (let i = 0; i < 4; ++i) { const message = new TestObject({}); if (i > 0) { message.set('prior', messageList[i - 1]); @@ -271,128 +289,151 @@ describe('Parse.Query testing', () => { messageList.push(message); } - Parse.Object.saveAll(messageList).then(function() { - equal(messageList.length, 4); + Parse.Object.saveAll(messageList).then( + function() { + equal(messageList.length, 4); - const inList = []; - inList.push(messageList[0]); - inList.push(messageList[2]); + const inList = []; + inList.push(messageList[0]); + inList.push(messageList[2]); - const query = new Parse.Query(TestObject); - query.containedIn('prior', inList); - query.find().then(function(results) { - equal(results.length, 2); - done(); - }, function(e) { + const query = new Parse.Query(TestObject); + query.containedIn('prior', inList); + query.find().then( + function(results) { + equal(results.length, 2); + done(); + }, + function(e) { + jfail(e); + done(); + } + ); + }, + e => { jfail(e); done(); - }); - }, (e) => { - jfail(e); - done(); - }); + } + ); }); - it('containedIn null array', (done) => { + it('containedIn null array', done => { const emails = ['contact@xyz.com', 'contact@zyx.com', null]; const user = new Parse.User(); user.setUsername(emails[0]); user.setPassword('asdf'); - user.signUp().then(() => { - const query = new Parse.Query(Parse.User); - query.containedIn('username', emails); - return query.find({ useMasterKey: true }); - }).then((results) => { - equal(results.length, 1); - done(); - }, done.fail); + user + .signUp() + .then(() => { + const query = new Parse.Query(Parse.User); + query.containedIn('username', emails); + return query.find({ useMasterKey: true }); + }) + .then(results => { + equal(results.length, 1); + done(); + }, done.fail); }); - it('nested containedIn string', (done) => { + it('nested containedIn string', done => { const sender1 = { group: ['A', 'B'] }; const sender2 = { group: ['A', 'C'] }; const sender3 = { group: ['B', 'C'] }; const obj1 = new TestObject({ sender: sender1 }); const obj2 = new TestObject({ sender: sender2 }); const obj3 = new TestObject({ sender: sender3 }); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const query = new Parse.Query(TestObject); - query.containedIn('sender.group', ['A']); - return query.find(); - }).then((results) => { - equal(results.length, 2); - done(); - }, done.fail); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const query = new Parse.Query(TestObject); + query.containedIn('sender.group', ['A']); + return query.find(); + }) + .then(results => { + equal(results.length, 2); + done(); + }, done.fail); }); - it('nested containedIn number', (done) => { + it('nested containedIn number', done => { const sender1 = { group: [1, 2] }; const sender2 = { group: [1, 3] }; const sender3 = { group: [2, 3] }; const obj1 = new TestObject({ sender: sender1 }); const obj2 = new TestObject({ sender: sender2 }); const obj3 = new TestObject({ sender: sender3 }); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const query = new Parse.Query(TestObject); - query.containedIn('sender.group', [1]); - return query.find(); - }).then((results) => { - equal(results.length, 2); - done(); - }, done.fail); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const query = new Parse.Query(TestObject); + query.containedIn('sender.group', [1]); + return query.find(); + }) + .then(results => { + equal(results.length, 2); + done(); + }, done.fail); }); - it("containsAll number array queries", function(done) { - const NumberSet = Parse.Object.extend({ className: "NumberSet" }); + it('containsAll number array queries', function(done) { + const NumberSet = Parse.Object.extend({ className: 'NumberSet' }); const objectsList = []; - objectsList.push(new NumberSet({ "numbers" : [1, 2, 3, 4, 5] })); - objectsList.push(new NumberSet({ "numbers" : [1, 3, 4, 5] })); + objectsList.push(new NumberSet({ numbers: [1, 2, 3, 4, 5] })); + objectsList.push(new NumberSet({ numbers: [1, 3, 4, 5] })); - Parse.Object.saveAll(objectsList).then(function() { - const query = new Parse.Query(NumberSet); - query.containsAll("numbers", [1, 2, 3]); - query.find().then(function(results) { - equal(results.length, 1); - done(); - }, function(err) { + Parse.Object.saveAll(objectsList) + .then(function() { + const query = new Parse.Query(NumberSet); + query.containsAll('numbers', [1, 2, 3]); + query.find().then( + function(results) { + equal(results.length, 1); + done(); + }, + function(err) { + jfail(err); + done(); + } + ); + }) + .catch(err => { jfail(err); done(); }); - }).catch((err) => { - jfail(err); - done(); - }); }); - it("containsAll string array queries", function(done) { - const StringSet = Parse.Object.extend({ className: "StringSet" }); + it('containsAll string array queries', function(done) { + const StringSet = Parse.Object.extend({ className: 'StringSet' }); const objectsList = []; - objectsList.push(new StringSet({ "strings" : ["a", "b", "c", "d", "e"] })); - objectsList.push(new StringSet({ "strings" : ["a", "c", "d", "e"] })); + objectsList.push(new StringSet({ strings: ['a', 'b', 'c', 'd', 'e'] })); + objectsList.push(new StringSet({ strings: ['a', 'c', 'd', 'e'] })); - Parse.Object.saveAll(objectsList).then(function() { - const query = new Parse.Query(StringSet); - query.containsAll("strings", ["a", "b", "c"]); - query.find().then(function(results) { - equal(results.length, 1); + Parse.Object.saveAll(objectsList) + .then(function() { + const query = new Parse.Query(StringSet); + query.containsAll('strings', ['a', 'b', 'c']); + query.find().then(function(results) { + equal(results.length, 1); + done(); + }); + }) + .catch(err => { + jfail(err); done(); }); - }).catch((err) => { - jfail(err); - done(); - }); }); - it("containsAll date array queries", function(done) { - const DateSet = Parse.Object.extend({ className: "DateSet" }); + it('containsAll date array queries', function(done) { + const DateSet = Parse.Object.extend({ className: 'DateSet' }); function parseDate(iso8601) { const regexp = new RegExp( - '^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,2})' + 'T' + + '^([0-9]{1,4})-([0-9]{1,2})-([0-9]{1,2})' + + 'T' + '([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})' + - '(.([0-9]+))?' + 'Z$'); + '(.([0-9]+))?' + + 'Z$' + ); const match = regexp.exec(iso8601); if (!match) { return null; @@ -411,53 +452,65 @@ describe('Parse.Query testing', () => { const makeDates = function(stringArray) { return stringArray.map(function(dateStr) { - return parseDate(dateStr + "T00:00:00Z"); + return parseDate(dateStr + 'T00:00:00Z'); }); }; const objectsList = []; - objectsList.push(new DateSet({ - "dates" : makeDates(["2013-02-01", "2013-02-02", "2013-02-03", - "2013-02-04"]) - })); - objectsList.push(new DateSet({ - "dates" : makeDates(["2013-02-01", "2013-02-03", "2013-02-04"]) - })); + objectsList.push( + new DateSet({ + dates: makeDates([ + '2013-02-01', + '2013-02-02', + '2013-02-03', + '2013-02-04', + ]), + }) + ); + objectsList.push( + new DateSet({ + dates: makeDates(['2013-02-01', '2013-02-03', '2013-02-04']), + }) + ); Parse.Object.saveAll(objectsList).then(function() { const query = new Parse.Query(DateSet); - query.containsAll("dates", makeDates( - ["2013-02-01", "2013-02-02", "2013-02-03"])); - query.find().then(function(results) { - equal(results.length, 1); - done(); - },function(e) { - jfail(e); - done(); - }); + query.containsAll( + 'dates', + makeDates(['2013-02-01', '2013-02-02', '2013-02-03']) + ); + query.find().then( + function(results) { + equal(results.length, 1); + done(); + }, + function(e) { + jfail(e); + done(); + } + ); }); }); - it("containsAll object array queries", function(done) { - - const MessageSet = Parse.Object.extend({ className: "MessageSet" }); + it('containsAll object array queries', function(done) { + const MessageSet = Parse.Object.extend({ className: 'MessageSet' }); const messageList = []; - for (let i = 0; i < 4; ++i) { - messageList.push(new TestObject({ 'i' : i })); + for (let i = 0; i < 4; ++i) { + messageList.push(new TestObject({ i: i })); } Parse.Object.saveAll(messageList).then(function() { equal(messageList.length, 4); const messageSetList = []; - messageSetList.push(new MessageSet({ 'messages' : messageList })); + messageSetList.push(new MessageSet({ messages: messageList })); const someList = []; someList.push(messageList[0]); someList.push(messageList[1]); someList.push(messageList[3]); - messageSetList.push(new MessageSet({ 'messages' : someList })); + messageSetList.push(new MessageSet({ messages: someList })); Parse.Object.saveAll(messageSetList).then(function() { const inList = []; @@ -474,8 +527,7 @@ describe('Parse.Query testing', () => { }); }); - it('containsAllStartingWith should match all strings that starts with string', (done) => { - + it('containsAllStartingWith should match all strings that starts with string', done => { const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); const object2 = new Parse.Object('Object'); @@ -485,73 +537,68 @@ describe('Parse.Query testing', () => { const objectList = [object, object2, object3]; - Parse.Object.saveAll(objectList).then((results) => { + Parse.Object.saveAll(objectList).then(results => { equal(objectList.length, results.length); - return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", - json: { - where: { - strings: { - $all: [ - {$regex: '\^\\Qthe\\E'}, - {$regex: '\^\\Qfox\\E'}, - {$regex: '\^\\Qlazy\\E'} - ] - } - } - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }) - .then(function (results) { + return require('request-promise') + .get({ + url: Parse.serverURL + '/classes/Object', + json: { + where: { + strings: { + $all: [ + { $regex: '^\\Qthe\\E' }, + { $regex: '^\\Qfox\\E' }, + { $regex: '^\\Qlazy\\E' }, + ], + }, + }, + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }) + .then(function(results) { equal(results.results.length, 1); arrayContains(results.results, object); return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", + url: Parse.serverURL + '/classes/Object', json: { where: { strings: { - $all: [ - {$regex: '\^\\Qthe\\E'}, - {$regex: '\^\\Qlazy\\E'} - ] - } - } + $all: [{ $regex: '^\\Qthe\\E' }, { $regex: '^\\Qlazy\\E' }], + }, + }, }, headers: { 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, }); }) - .then(function (results) { + .then(function(results) { equal(results.results.length, 2); arrayContains(results.results, object); arrayContains(results.results, object3); return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", + url: Parse.serverURL + '/classes/Object', json: { where: { strings: { - $all: [ - {$regex: '\^\\Qhe\\E'}, - {$regex: '\^\\Qlazy\\E'} - ] - } - } + $all: [{ $regex: '^\\Qhe\\E' }, { $regex: '^\\Qlazy\\E' }], + }, + }, }, headers: { 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, }); }) - .then(function (results) { + .then(function(results) { equal(results.results.length, 0); done(); @@ -559,102 +606,110 @@ describe('Parse.Query testing', () => { }); }); - it('containsAllStartingWith values must be all of type starting with regex', (done) => { - + it('containsAllStartingWith values must be all of type starting with regex', done => { const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - object.save().then(() => { - equal(object.isNew(), false); - - return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", - json: { - where: { - strings: { - $all: [ - {$regex: '\^\\Qthe\\E'}, - {$regex: '\^\\Qlazy\\E'}, - {$regex: '\^\\Qfox\\E'}, - {$unknown: /unknown/} - ] - } - } - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey + object + .save() + .then(() => { + equal(object.isNew(), false); + + return require('request-promise').get({ + url: Parse.serverURL + '/classes/Object', + json: { + where: { + strings: { + $all: [ + { $regex: '^\\Qthe\\E' }, + { $regex: '^\\Qlazy\\E' }, + { $regex: '^\\Qfox\\E' }, + { $unknown: /unknown/ }, + ], + }, + }, + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then( + function() {}, + function() { + done(); } - }); - }) - .then(function () { - }, function () { - done(); - }); + ); }); - it('containsAllStartingWith empty array values should return empty results', (done) => { - + it('containsAllStartingWith empty array values should return empty results', done => { const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - object.save().then(() => { - equal(object.isNew(), false); - - return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", - json: { - where: { - strings: { - $all: [] - } - } + object + .save() + .then(() => { + equal(object.isNew(), false); + + return require('request-promise').get({ + url: Parse.serverURL + '/classes/Object', + json: { + where: { + strings: { + $all: [], + }, + }, + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then( + function(results) { + equal(results.results.length, 0); + done(); }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }) - .then(function (results) { - equal(results.results.length, 0); - done(); - }, function () { - }); + function() {} + ); }); - it('containsAllStartingWith single empty value returns empty results', (done) => { - + it('containsAllStartingWith single empty value returns empty results', done => { const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - object.save().then(() => { - equal(object.isNew(), false); - - return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", - json: { - where: { - strings: { - $all: [ {} ] - } - } + object + .save() + .then(() => { + equal(object.isNew(), false); + + return require('request-promise').get({ + url: Parse.serverURL + '/classes/Object', + json: { + where: { + strings: { + $all: [{}], + }, + }, + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then( + function(results) { + equal(results.results.length, 0); + done(); }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }) - .then(function (results) { - equal(results.results.length, 0); - done(); - }, function () { - }); + function() {} + ); }); - it('containsAllStartingWith single regex value should return corresponding matching results', (done) => { - + it('containsAllStartingWith single regex value should return corresponding matching results', done => { const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); const object2 = new Parse.Object('Object'); @@ -664,63 +719,69 @@ describe('Parse.Query testing', () => { const objectList = [object, object2, object3]; - Parse.Object.saveAll(objectList).then((results) => { - equal(objectList.length, results.length); - - return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", - json: { - where: { - strings: { - $all: [ {$regex: '\^\\Qlazy\\E'} ] - } - } + Parse.Object.saveAll(objectList) + .then(results => { + equal(objectList.length, results.length); + + return require('request-promise').get({ + url: Parse.serverURL + '/classes/Object', + json: { + where: { + strings: { + $all: [{ $regex: '^\\Qlazy\\E' }], + }, + }, + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then( + function(results) { + equal(results.results.length, 2); + done(); }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }) - .then(function (results) { - equal(results.results.length, 2); - done(); - }, function () { - }); + function() {} + ); }); - it('containsAllStartingWith single invalid regex returns empty results', (done) => { - + it('containsAllStartingWith single invalid regex returns empty results', done => { const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - object.save().then(() => { - equal(object.isNew(), false); - - return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", - json: { - where: { - strings: { - $all: [ {$unknown: '\^\\Qlazy\\E'} ] - } - } + object + .save() + .then(() => { + equal(object.isNew(), false); + + return require('request-promise').get({ + url: Parse.serverURL + '/classes/Object', + json: { + where: { + strings: { + $all: [{ $unknown: '^\\Qlazy\\E' }], + }, + }, + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then( + function(results) { + equal(results.results.length, 0); + done(); }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }) - .then(function (results) { - equal(results.results.length, 0); - done(); - }, function () { - }); + function() {} + ); }); - it('containedBy pointer array', (done) => { - const objects = Array.from(Array(10).keys()).map((idx) => { + it('containedBy pointer array', done => { + const objects = Array.from(Array(10).keys()).map(idx => { const obj = new Parse.Object('Object'); obj.set('key', idx); return obj; @@ -730,544 +791,582 @@ describe('Parse.Query testing', () => { const parent2 = new Parse.Object('Parent'); const parent3 = new Parse.Object('Parent'); - Parse.Object.saveAll(objects).then(() => { - // [0, 1, 2] - parent.set('objects', objects.slice(0, 3)); - - const shift = objects.shift(); - // [2, 0] - parent2.set('objects', [objects[1], shift]); - - // [1, 2, 3, 4] - parent3.set('objects', objects.slice(1, 4)); - - return Parse.Object.saveAll([parent, parent2, parent3]); - }).then(() => { - // [1, 2, 3, 4, 5, 6, 7, 8, 9] - const pointers = objects.map(object => object.toPointer()); - - // Return all Parent where all parent.objects are contained in objects - return rp.get({ - url: Parse.serverURL + "/classes/Parent", - json: { - where: { - objects: { - $containedBy: pointers - } - } - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } + Parse.Object.saveAll(objects) + .then(() => { + // [0, 1, 2] + parent.set('objects', objects.slice(0, 3)); + + const shift = objects.shift(); + // [2, 0] + parent2.set('objects', [objects[1], shift]); + + // [1, 2, 3, 4] + parent3.set('objects', objects.slice(1, 4)); + + return Parse.Object.saveAll([parent, parent2, parent3]); + }) + .then(() => { + // [1, 2, 3, 4, 5, 6, 7, 8, 9] + const pointers = objects.map(object => object.toPointer()); + + // Return all Parent where all parent.objects are contained in objects + return rp.get({ + url: Parse.serverURL + '/classes/Parent', + json: { + where: { + objects: { + $containedBy: pointers, + }, + }, + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(results => { + expect(results.results[0].objectId).not.toBeUndefined(); + expect(results.results[0].objectId).toBe(parent3.id); + expect(results.results.length).toBe(1); + done(); }); - }).then((results) => { - expect(results.results[0].objectId).not.toBeUndefined(); - expect(results.results[0].objectId).toBe(parent3.id); - expect(results.results.length).toBe(1); - done(); - }); }); - - it('containedBy number array', (done) => { + it('containedBy number array', done => { const options = Object.assign({}, masterKeyOptions, { body: { where: { numbers: { $containedBy: [1, 2, 3, 4, 5, 6, 7, 8, 9] } }, - } + }, }); const obj1 = new TestObject({ numbers: [0, 1, 2] }); const obj2 = new TestObject({ numbers: [2, 0] }); const obj3 = new TestObject({ numbers: [1, 2, 3, 4] }); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - return rp.get(Parse.serverURL + "/classes/TestObject", options); - }).then((results) => { - expect(results.results[0].objectId).not.toBeUndefined(); - expect(results.results[0].objectId).toBe(obj3.id); - expect(results.results.length).toBe(1); - done(); - }); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + return rp.get(Parse.serverURL + '/classes/TestObject', options); + }) + .then(results => { + expect(results.results[0].objectId).not.toBeUndefined(); + expect(results.results[0].objectId).toBe(obj3.id); + expect(results.results.length).toBe(1); + done(); + }); }); - it('containedBy empty array', (done) => { + it('containedBy empty array', done => { const options = Object.assign({}, masterKeyOptions, { body: { where: { numbers: { $containedBy: [] } }, - } + }, }); const obj1 = new TestObject({ numbers: [0, 1, 2] }); const obj2 = new TestObject({ numbers: [2, 0] }); const obj3 = new TestObject({ numbers: [1, 2, 3, 4] }); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - return rp.get(Parse.serverURL + "/classes/TestObject", options); - }).then((results) => { - expect(results.results.length).toBe(0); - done(); - }); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + return rp.get(Parse.serverURL + '/classes/TestObject', options); + }) + .then(results => { + expect(results.results.length).toBe(0); + done(); + }); }); - it('containedBy invalid query', (done) => { + it('containedBy invalid query', done => { const options = Object.assign({}, masterKeyOptions, { body: { where: { objects: { $containedBy: 1234 } }, - } + }, }); const obj = new TestObject(); - obj.save().then(() => { - return rp.get(Parse.serverURL + "/classes/TestObject", options); - }).then(done.fail).catch((error) => { - equal(error.error.code, Parse.Error.INVALID_JSON); - equal(error.error.error, 'bad $containedBy: should be an array'); - done(); - }); + obj + .save() + .then(() => { + return rp.get(Parse.serverURL + '/classes/TestObject', options); + }) + .then(done.fail) + .catch(error => { + equal(error.error.code, Parse.Error.INVALID_JSON); + equal(error.error.error, 'bad $containedBy: should be an array'); + done(); + }); }); const BoxedNumber = Parse.Object.extend({ - className: "BoxedNumber" + className: 'BoxedNumber', }); - it("equalTo queries", function(done) { + it('equalTo queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; - Parse.Object.saveAll([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.equalTo('number', 3); - query.find().then(function(results) { - equal(results.length, 1); - done(); - }); + Parse.Object.saveAll( + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.equalTo('number', 3); + query.find().then(function(results) { + equal(results.length, 1); + done(); }); + }); }); - it("equalTo undefined", function(done) { + it('equalTo undefined', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; - Parse.Object.saveAll([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.equalTo('number', undefined); - query.find().then(function(results) { - equal(results.length, 0); - done(); - }); + Parse.Object.saveAll( + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.equalTo('number', undefined); + query.find().then(function(results) { + equal(results.length, 0); + done(); }); + }); }); - it("lessThan queries", function(done) { + it('lessThan queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; - Parse.Object.saveAll([0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.lessThan('number', 7); - query.find().then(function(results) { - equal(results.length, 7); - done(); - }); + Parse.Object.saveAll( + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.lessThan('number', 7); + query.find().then(function(results) { + equal(results.length, 7); + done(); }); + }); }); - it("lessThanOrEqualTo queries", function(done) { + it('lessThanOrEqualTo queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.lessThanOrEqualTo('number', 7); - query.find().then(function(results) { - equal(results.length, 8); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.lessThanOrEqualTo('number', 7); + query.find().then(function(results) { + equal(results.length, 8); + done(); }); + }); }); - it("lessThan zero queries", (done) => { - const makeBoxedNumber = (i) => { + it('lessThan zero queries', done => { + const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); }; const numbers = [-3, -2, -1, 0, 1]; const boxedNumbers = numbers.map(makeBoxedNumber); - Parse.Object.saveAll(boxedNumbers).then(() => { - const query = new Parse.Query(BoxedNumber); - query.lessThan('number', 0); - return query.find(); - }).then((results) => { - equal(results.length, 3); - done(); - }); + Parse.Object.saveAll(boxedNumbers) + .then(() => { + const query = new Parse.Query(BoxedNumber); + query.lessThan('number', 0); + return query.find(); + }) + .then(results => { + equal(results.length, 3); + done(); + }); }); - it("lessThanOrEqualTo zero queries", (done) => { - const makeBoxedNumber = (i) => { + it('lessThanOrEqualTo zero queries', done => { + const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); }; const numbers = [-3, -2, -1, 0, 1]; const boxedNumbers = numbers.map(makeBoxedNumber); - Parse.Object.saveAll(boxedNumbers).then(() => { - const query = new Parse.Query(BoxedNumber); - query.lessThanOrEqualTo('number', 0); - return query.find(); - }).then((results) => { - equal(results.length, 4); - done(); - }); + Parse.Object.saveAll(boxedNumbers) + .then(() => { + const query = new Parse.Query(BoxedNumber); + query.lessThanOrEqualTo('number', 0); + return query.find(); + }) + .then(results => { + equal(results.length, 4); + done(); + }); }); - it("greaterThan queries", function(done) { + it('greaterThan queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.greaterThan('number', 7); - query.find().then(function(results) { - equal(results.length, 2); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.greaterThan('number', 7); + query.find().then(function(results) { + equal(results.length, 2); + done(); }); + }); }); - it("greaterThanOrEqualTo queries", function(done) { + it('greaterThanOrEqualTo queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.greaterThanOrEqualTo('number', 7); - query.find().then(function(results) { - equal(results.length, 3); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.greaterThanOrEqualTo('number', 7); + query.find().then(function(results) { + equal(results.length, 3); + done(); }); + }); }); - it("greaterThan zero queries", (done) => { - const makeBoxedNumber = (i) => { + it('greaterThan zero queries', done => { + const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); }; const numbers = [-3, -2, -1, 0, 1]; const boxedNumbers = numbers.map(makeBoxedNumber); - Parse.Object.saveAll(boxedNumbers).then(() => { - const query = new Parse.Query(BoxedNumber); - query.greaterThan('number', 0); - return query.find(); - }).then((results) => { - equal(results.length, 1); - done(); - }); + Parse.Object.saveAll(boxedNumbers) + .then(() => { + const query = new Parse.Query(BoxedNumber); + query.greaterThan('number', 0); + return query.find(); + }) + .then(results => { + equal(results.length, 1); + done(); + }); }); - it("greaterThanOrEqualTo zero queries", (done) => { - const makeBoxedNumber = (i) => { + it('greaterThanOrEqualTo zero queries', done => { + const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); }; const numbers = [-3, -2, -1, 0, 1]; const boxedNumbers = numbers.map(makeBoxedNumber); - Parse.Object.saveAll(boxedNumbers).then(() => { - const query = new Parse.Query(BoxedNumber); - query.greaterThanOrEqualTo('number', 0); - return query.find(); - }).then((results) => { - equal(results.length, 2); - done(); - }); + Parse.Object.saveAll(boxedNumbers) + .then(() => { + const query = new Parse.Query(BoxedNumber); + query.greaterThanOrEqualTo('number', 0); + return query.find(); + }) + .then(results => { + equal(results.length, 2); + done(); + }); }); - it("lessThanOrEqualTo greaterThanOrEqualTo queries", function(done) { + it('lessThanOrEqualTo greaterThanOrEqualTo queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.lessThanOrEqualTo('number', 7); - query.greaterThanOrEqualTo('number', 7); - query.find().then(function(results) { - equal(results.length, 1); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.lessThanOrEqualTo('number', 7); + query.greaterThanOrEqualTo('number', 7); + query.find().then(function(results) { + equal(results.length, 1); + done(); }); + }); }); - it("lessThan greaterThan queries", function(done) { + it('lessThan greaterThan queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.lessThan('number', 9); - query.greaterThan('number', 3); - query.find().then(function(results) { - equal(results.length, 5); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.lessThan('number', 9); + query.greaterThan('number', 3); + query.find().then(function(results) { + equal(results.length, 5); + done(); }); + }); }); - it("notEqualTo queries", function(done) { + it('notEqualTo queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.notEqualTo('number', 5); - query.find().then(function(results) { - equal(results.length, 9); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.notEqualTo('number', 5); + query.find().then(function(results) { + equal(results.length, 9); + done(); }); + }); }); - it("notEqualTo zero queries", (done) => { - const makeBoxedNumber = (i) => { + it('notEqualTo zero queries', done => { + const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); }; const numbers = [-3, -2, -1, 0, 1]; const boxedNumbers = numbers.map(makeBoxedNumber); - Parse.Object.saveAll(boxedNumbers).then(() => { - const query = new Parse.Query(BoxedNumber); - query.notEqualTo('number', 0); - return query.find(); - }).then((results) => { - equal(results.length, 4); - done(); - }); + Parse.Object.saveAll(boxedNumbers) + .then(() => { + const query = new Parse.Query(BoxedNumber); + query.notEqualTo('number', 0); + return query.find(); + }) + .then(results => { + equal(results.length, 4); + done(); + }); }); - it("equalTo zero queries", (done) => { - const makeBoxedNumber = (i) => { + it('equalTo zero queries', done => { + const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); }; const numbers = [-3, -2, -1, 0, 1]; const boxedNumbers = numbers.map(makeBoxedNumber); - Parse.Object.saveAll(boxedNumbers).then(() => { - const query = new Parse.Query(BoxedNumber); - query.equalTo('number', 0); - return query.find(); - }).then((results) => { - equal(results.length, 1); - done(); - }); + Parse.Object.saveAll(boxedNumbers) + .then(() => { + const query = new Parse.Query(BoxedNumber); + query.equalTo('number', 0); + return query.find(); + }) + .then(results => { + equal(results.length, 1); + done(); + }); }); - it("number equalTo boolean queries", (done) => { - const makeBoxedNumber = (i) => { + it('number equalTo boolean queries', done => { + const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); }; const numbers = [-3, -2, -1, 0, 1]; const boxedNumbers = numbers.map(makeBoxedNumber); - Parse.Object.saveAll(boxedNumbers).then(() => { - const query = new Parse.Query(BoxedNumber); - query.equalTo('number', false); - return query.find(); - }).then((results) => { - equal(results.length, 0); - done(); - }); + Parse.Object.saveAll(boxedNumbers) + .then(() => { + const query = new Parse.Query(BoxedNumber); + query.equalTo('number', false); + return query.find(); + }) + .then(results => { + equal(results.length, 0); + done(); + }); }); - it("equalTo false queries", (done) => { + it('equalTo false queries', done => { const obj1 = new TestObject({ field: false }); const obj2 = new TestObject({ field: true }); - Parse.Object.saveAll([obj1, obj2]).then(() => { - const query = new Parse.Query(TestObject); - query.equalTo('field', false); - return query.find(); - }).then((results) => { - equal(results.length, 1); - done(); - }); + Parse.Object.saveAll([obj1, obj2]) + .then(() => { + const query = new Parse.Query(TestObject); + query.equalTo('field', false); + return query.find(); + }) + .then(results => { + equal(results.length, 1); + done(); + }); }); - it("where $eq false queries (rest)", (done) => { + it('where $eq false queries (rest)', done => { const options = Object.assign({}, masterKeyOptions, { body: { where: { field: { $eq: false } }, - } + }, }); const obj1 = new TestObject({ field: false }); const obj2 = new TestObject({ field: true }); Parse.Object.saveAll([obj1, obj2]).then(() => { - rp.get(Parse.serverURL + '/classes/TestObject', options) - .then((resp) => { - equal(resp.results.length, 1); - done(); - }); - }) + rp.get(Parse.serverURL + '/classes/TestObject', options).then(resp => { + equal(resp.results.length, 1); + done(); + }); + }); }); - it("where $eq null queries (rest)", (done) => { + it('where $eq null queries (rest)', done => { const options = Object.assign({}, masterKeyOptions, { body: { where: { field: { $eq: null } }, - } + }, }); const obj1 = new TestObject({ field: false }); const obj2 = new TestObject({ field: null }); Parse.Object.saveAll([obj1, obj2]).then(() => { - rp.get(Parse.serverURL + '/classes/TestObject', options) - .then((resp) => { - equal(resp.results.length, 1); - done(); - }); - }) + rp.get(Parse.serverURL + '/classes/TestObject', options).then(resp => { + equal(resp.results.length, 1); + done(); + }); + }); }); - it("containedIn queries", function(done) { + it('containedIn queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.containedIn('number', [3,5,7,9,11]); - query.find().then(function(results) { - equal(results.length, 4); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.containedIn('number', [3, 5, 7, 9, 11]); + query.find().then(function(results) { + equal(results.length, 4); + done(); }); + }); }); - it("containedIn false queries", (done) => { - const makeBoxedNumber = (i) => { + it('containedIn false queries', done => { + const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); }; const numbers = [-3, -2, -1, 0, 1]; const boxedNumbers = numbers.map(makeBoxedNumber); - Parse.Object.saveAll(boxedNumbers).then(() => { - const query = new Parse.Query(BoxedNumber); - query.containedIn('number', false); - return query.find(); - }).then(done.fail).catch((error) => { - equal(error.code, Parse.Error.INVALID_JSON); - equal(error.message, 'bad $in value'); - done(); - }); + Parse.Object.saveAll(boxedNumbers) + .then(() => { + const query = new Parse.Query(BoxedNumber); + query.containedIn('number', false); + return query.find(); + }) + .then(done.fail) + .catch(error => { + equal(error.code, Parse.Error.INVALID_JSON); + equal(error.message, 'bad $in value'); + done(); + }); }); - it("notContainedIn false queries", (done) => { - const makeBoxedNumber = (i) => { + it('notContainedIn false queries', done => { + const makeBoxedNumber = i => { return new BoxedNumber({ number: i }); }; const numbers = [-3, -2, -1, 0, 1]; const boxedNumbers = numbers.map(makeBoxedNumber); - Parse.Object.saveAll(boxedNumbers).then(() => { - const query = new Parse.Query(BoxedNumber); - query.notContainedIn('number', false); - return query.find(); - }).then(done.fail).catch((error) => { - equal(error.code, Parse.Error.INVALID_JSON); - equal(error.message, 'bad $nin value'); - done(); - }); + Parse.Object.saveAll(boxedNumbers) + .then(() => { + const query = new Parse.Query(BoxedNumber); + query.notContainedIn('number', false); + return query.find(); + }) + .then(done.fail) + .catch(error => { + equal(error.code, Parse.Error.INVALID_JSON); + equal(error.message, 'bad $nin value'); + done(); + }); }); - it("notContainedIn queries", function(done) { + it('notContainedIn queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.notContainedIn('number', [3,5,7,9,11]); - query.find().then(function(results) { - equal(results.length, 6); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.notContainedIn('number', [3, 5, 7, 9, 11]); + query.find().then(function(results) { + equal(results.length, 6); + done(); }); + }); }); - - it("objectId containedIn queries", function(done) { + it('objectId containedIn queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function(list) { - const query = new Parse.Query(BoxedNumber); - query.containedIn('objectId', - [list[2].id, list[3].id, list[0].id, - "NONSENSE"]); - query.ascending('number'); - query.find().then(function(results) { - if (results.length != 3) { - fail('expected 3 results'); - } else { - equal(results[0].get('number'), 0); - equal(results[1].get('number'), 2); - equal(results[2].get('number'), 3); - } - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function(list) { + const query = new Parse.Query(BoxedNumber); + query.containedIn('objectId', [ + list[2].id, + list[3].id, + list[0].id, + 'NONSENSE', + ]); + query.ascending('number'); + query.find().then(function(results) { + if (results.length != 3) { + fail('expected 3 results'); + } else { + equal(results[0].get('number'), 0); + equal(results[1].get('number'), 2); + equal(results[2].get('number'), 3); + } + done(); }); + }); }); - it("objectId equalTo queries", function(done) { + it('objectId equalTo queries', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function(list) { - const query = new Parse.Query(BoxedNumber); - query.equalTo('objectId', list[4].id); - query.find().then(function(results) { - if (results.length != 1) { - fail('expected 1 result') - done(); - } else { - equal(results[0].get('number'), 4); - } + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function(list) { + const query = new Parse.Query(BoxedNumber); + query.equalTo('objectId', list[4].id); + query.find().then(function(results) { + if (results.length != 1) { + fail('expected 1 result'); done(); - }); + } else { + equal(results[0].get('number'), 4); + } + done(); }); + }); }); - it("find no elements", function(done) { + it('find no elements', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.equalTo('number', 17); - query.find().then(function(results) { - equal(results.length, 0); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.equalTo('number', 17); + query.find().then(function(results) { + equal(results.length, 0); + done(); }); + }); }); - it("find with error", function(done) { + it('find with error', function(done) { const query = new Parse.Query(BoxedNumber); query.equalTo('$foo', 'bar'); - query.find() + query + .find() .then(done.fail) .catch(error => expect(error.code).toBe(Parse.Error.INVALID_KEY_NAME)) .finally(done); }); - it("get", function(done) { - Parse.Object.saveAll([new TestObject({foo: 'bar'})]).then(function(items) { + it('get', function(done) { + Parse.Object.saveAll([new TestObject({ foo: 'bar' })]).then(function( + items + ) { ok(items[0]); const objectId = items[0].id; const query = new Parse.Query(TestObject); @@ -1282,30 +1381,37 @@ describe('Parse.Query testing', () => { }); }); - it("get undefined", function(done) { - Parse.Object.saveAll([new TestObject({foo: 'bar'})]).then(function(items) { + it('get undefined', function(done) { + Parse.Object.saveAll([new TestObject({ foo: 'bar' })]).then(function( + items + ) { ok(items[0]); const query = new Parse.Query(TestObject); query.get(undefined).then(fail, done); }); }); - it("get error", function(done) { - Parse.Object.saveAll([new TestObject({foo: 'bar'})]).then(function(items) { + it('get error', function(done) { + Parse.Object.saveAll([new TestObject({ foo: 'bar' })]).then(function( + items + ) { ok(items[0]); const query = new Parse.Query(TestObject); - query.get("InvalidObjectID").then(function() { - ok(false, "The get should have failed."); - done(); - }, function(error) { - equal(error.code, Parse.Error.OBJECT_NOT_FOUND); - done(); - }); + query.get('InvalidObjectID').then( + function() { + ok(false, 'The get should have failed.'); + done(); + }, + function(error) { + equal(error.code, Parse.Error.OBJECT_NOT_FOUND); + done(); + } + ); }); }); - it("first", function(done) { - Parse.Object.saveAll([new TestObject({foo: 'bar'})]).then(function() { + it('first', function(done) { + Parse.Object.saveAll([new TestObject({ foo: 'bar' })]).then(function() { const query = new Parse.Query(TestObject); query.equalTo('foo', 'bar'); query.first().then(function(result) { @@ -1315,8 +1421,8 @@ describe('Parse.Query testing', () => { }); }); - it("first no result", function(done) { - Parse.Object.saveAll([new TestObject({foo: 'bar'})]).then(function() { + it('first no result', function(done) { + Parse.Object.saveAll([new TestObject({ foo: 'bar' })]).then(function() { const query = new Parse.Query(TestObject); query.equalTo('foo', 'baz'); query.first().then(function(result) { @@ -1326,9 +1432,11 @@ describe('Parse.Query testing', () => { }); }); - it("first with two results", function(done) { - Parse.Object.saveAll([new TestObject({foo: 'bar'}), - new TestObject({foo: 'bar'})]).then(function() { + it('first with two results', function(done) { + Parse.Object.saveAll([ + new TestObject({ foo: 'bar' }), + new TestObject({ foo: 'bar' }), + ]).then(function() { const query = new Parse.Query(TestObject); query.equalTo('foo', 'bar'); query.first().then(function(result) { @@ -1338,35 +1446,38 @@ describe('Parse.Query testing', () => { }); }); - it("first with error", function(done) { + it('first with error', function(done) { const query = new Parse.Query(BoxedNumber); query.equalTo('$foo', 'bar'); - query.first() + query + .first() .then(done.fail) .catch(e => expect(e.code).toBe(Parse.Error.INVALID_KEY_NAME)) .finally(done); }); const Container = Parse.Object.extend({ - className: "Container" + className: 'Container', }); - it("notEqualTo object", function(done) { + it('notEqualTo object', function(done) { const item1 = new TestObject(); const item2 = new TestObject(); - const container1 = new Container({item: item1}); - const container2 = new Container({item: item2}); - Parse.Object.saveAll([item1, item2, container1, container2]).then(function() { - const query = new Parse.Query(Container); - query.notEqualTo('item', item1); - query.find().then(function(results) { - equal(results.length, 1); - done(); - }); - }); + const container1 = new Container({ item: item1 }); + const container2 = new Container({ item: item2 }); + Parse.Object.saveAll([item1, item2, container1, container2]).then( + function() { + const query = new Parse.Query(Container); + query.notEqualTo('item', item1); + query.find().then(function(results) { + equal(results.length, 1); + done(); + }); + } + ); }); - it("skip", function(done) { + it('skip', function(done) { Parse.Object.saveAll([new TestObject(), new TestObject()]).then(function() { const query = new Parse.Query(TestObject); query.skip(1); @@ -1399,99 +1510,105 @@ describe('Parse.Query testing', () => { }); }); - it("count", function(done) { + it('count', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll( - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber)) - .then(function() { - const query = new Parse.Query(BoxedNumber); - query.greaterThan("number", 1); - query.count().then(function(count) { - equal(count, 8); - done(); - }); + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(makeBoxedNumber) + ).then(function() { + const query = new Parse.Query(BoxedNumber); + query.greaterThan('number', 1); + query.count().then(function(count) { + equal(count, 8); + done(); }); + }); }); - it("order by ascending number", function(done) { + it('order by ascending number', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll([3, 1, 2].map(makeBoxedNumber)).then(function() { const query = new Parse.Query(BoxedNumber); - query.ascending("number"); + query.ascending('number'); query.find().then(function(results) { equal(results.length, 3); - equal(results[0].get("number"), 1); - equal(results[1].get("number"), 2); - equal(results[2].get("number"), 3); + equal(results[0].get('number'), 1); + equal(results[1].get('number'), 2); + equal(results[2].get('number'), 3); done(); }); }); }); - it("order by descending number", function(done) { + it('order by descending number', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; Parse.Object.saveAll([3, 1, 2].map(makeBoxedNumber)).then(function() { const query = new Parse.Query(BoxedNumber); - query.descending("number"); + query.descending('number'); query.find().then(function(results) { equal(results.length, 3); - equal(results[0].get("number"), 3); - equal(results[1].get("number"), 2); - equal(results[2].get("number"), 1); + equal(results[0].get('number'), 3); + equal(results[1].get('number'), 2); + equal(results[2].get('number'), 1); done(); }); }); }); - it('can order on an object string field', function (done) { + it('can order on an object string field', function(done) { const testSet = [ - { sortField: { value: "Z" } }, - { sortField: { value: "A" } }, - { sortField: { value: "M" } }, + { sortField: { value: 'Z' } }, + { sortField: { value: 'A' } }, + { sortField: { value: 'M' } }, ]; const objects = testSet.map(e => new Parse.Object('Test', e)); Parse.Object.saveAll(objects) - .then(() => new Parse.Query('Test').addDescending('sortField.value').first()) - .then((result) => { - expect(result.get('sortField').value).toBe("Z"); - return new Parse.Query('Test').addAscending('sortField.value').first() + .then(() => + new Parse.Query('Test').addDescending('sortField.value').first() + ) + .then(result => { + expect(result.get('sortField').value).toBe('Z'); + return new Parse.Query('Test').addAscending('sortField.value').first(); }) - .then((result) => { - expect(result.get('sortField').value).toBe("A"); + .then(result => { + expect(result.get('sortField').value).toBe('A'); done(); }) .catch(done.fail); }); - it('can order on an object string field (level 2)', function (done) { + it('can order on an object string field (level 2)', function(done) { const testSet = [ - { sortField: { value: { field: "Z" } } }, - { sortField: { value: { field: "A" } } }, - { sortField: { value: { field: "M" } } }, + { sortField: { value: { field: 'Z' } } }, + { sortField: { value: { field: 'A' } } }, + { sortField: { value: { field: 'M' } } }, ]; const objects = testSet.map(e => new Parse.Object('Test', e)); Parse.Object.saveAll(objects) - .then(() => new Parse.Query('Test').addDescending('sortField.value.field').first()) - .then((result) => { - expect(result.get('sortField').value.field).toBe("Z"); - return new Parse.Query('Test').addAscending('sortField.value.field').first() + .then(() => + new Parse.Query('Test').addDescending('sortField.value.field').first() + ) + .then(result => { + expect(result.get('sortField').value.field).toBe('Z'); + return new Parse.Query('Test') + .addAscending('sortField.value.field') + .first(); }) - .then((result) => { - expect(result.get('sortField').value.field).toBe("A"); + .then(result => { + expect(result.get('sortField').value.field).toBe('A'); done(); }) .catch(done.fail); }); - it('can order on an object number field', function (done) { + it('can order on an object number field', function(done) { const testSet = [ { sortField: { value: 10 } }, { sortField: { value: 1 } }, @@ -1500,19 +1617,21 @@ describe('Parse.Query testing', () => { const objects = testSet.map(e => new Parse.Object('Test', e)); Parse.Object.saveAll(objects) - .then(() => new Parse.Query('Test').addDescending('sortField.value').first()) - .then((result) => { + .then(() => + new Parse.Query('Test').addDescending('sortField.value').first() + ) + .then(result => { expect(result.get('sortField').value).toBe(10); - return new Parse.Query('Test').addAscending('sortField.value').first() + return new Parse.Query('Test').addAscending('sortField.value').first(); }) - .then((result) => { + .then(result => { expect(result.get('sortField').value).toBe(1); done(); }) .catch(done.fail); }); - it('can order on an object number field (level 2)', function (done) { + it('can order on an object number field (level 2)', function(done) { const testSet = [ { sortField: { value: { field: 10 } } }, { sortField: { value: { field: 1 } } }, @@ -1521,45 +1640,47 @@ describe('Parse.Query testing', () => { const objects = testSet.map(e => new Parse.Object('Test', e)); Parse.Object.saveAll(objects) - .then(() => new Parse.Query('Test').addDescending('sortField.value.field').first()) - .then((result) => { + .then(() => + new Parse.Query('Test').addDescending('sortField.value.field').first() + ) + .then(result => { expect(result.get('sortField').value.field).toBe(10); - return new Parse.Query('Test').addAscending('sortField.value.field').first() + return new Parse.Query('Test') + .addAscending('sortField.value.field') + .first(); }) - .then((result) => { + .then(result => { expect(result.get('sortField').value.field).toBe(1); done(); }) .catch(done.fail); }); - it("order by ascending number then descending string", function(done) { - const strings = ["a", "b", "c", "d"]; + it('order by ascending number then descending string', function(done) { + const strings = ['a', 'b', 'c', 'd']; const makeBoxedNumber = function(num, i) { return new BoxedNumber({ number: num, string: strings[i] }); }; - Parse.Object.saveAll( - [3, 1, 3, 2].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.ascending("number").addDescending("string"); - query.find().then(function(results) { - equal(results.length, 4); - equal(results[0].get("number"), 1); - equal(results[0].get("string"), "b"); - equal(results[1].get("number"), 2); - equal(results[1].get("string"), "d"); - equal(results[2].get("number"), 3); - equal(results[2].get("string"), "c"); - equal(results[3].get("number"), 3); - equal(results[3].get("string"), "a"); - done(); - }); + Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber)).then(function() { + const query = new Parse.Query(BoxedNumber); + query.ascending('number').addDescending('string'); + query.find().then(function(results) { + equal(results.length, 4); + equal(results[0].get('number'), 1); + equal(results[0].get('string'), 'b'); + equal(results[1].get('number'), 2); + equal(results[1].get('string'), 'd'); + equal(results[2].get('number'), 3); + equal(results[2].get('string'), 'c'); + equal(results[3].get('number'), 3); + equal(results[3].get('string'), 'a'); + done(); }); + }); }); - it("order by descending number then ascending string", function(done) { - const strings = ["a", "b", "c", "d"]; + it('order by descending number then ascending string', function(done) { + const strings = ['a', 'b', 'c', 'd']; const makeBoxedNumber = function(num, i) { return new BoxedNumber({ number: num, string: strings[i] }); }; @@ -1568,123 +1689,125 @@ describe('Parse.Query testing', () => { Parse.Object.saveAll(objects) .then(() => { const query = new Parse.Query(BoxedNumber); - query.descending("number").addAscending("string"); + query.descending('number').addAscending('string'); return query.find(); - }).then((results) => { + }) + .then( + results => { + equal(results.length, 4); + equal(results[0].get('number'), 3); + equal(results[0].get('string'), 'a'); + equal(results[1].get('number'), 3); + equal(results[1].get('string'), 'c'); + equal(results[2].get('number'), 2); + equal(results[2].get('string'), 'd'); + equal(results[3].get('number'), 1); + equal(results[3].get('string'), 'b'); + done(); + }, + err => { + jfail(err); + done(); + } + ); + }); + + it('order by descending number and string', function(done) { + const strings = ['a', 'b', 'c', 'd']; + const makeBoxedNumber = function(num, i) { + return new BoxedNumber({ number: num, string: strings[i] }); + }; + Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber)).then(function() { + const query = new Parse.Query(BoxedNumber); + query.descending('number,string'); + query.find().then(function(results) { equal(results.length, 4); - equal(results[0].get("number"), 3); - equal(results[0].get("string"), "a"); - equal(results[1].get("number"), 3); - equal(results[1].get("string"), "c"); - equal(results[2].get("number"), 2); - equal(results[2].get("string"), "d"); - equal(results[3].get("number"), 1); - equal(results[3].get("string"), "b"); - done(); - }, (err) => { - jfail(err); + equal(results[0].get('number'), 3); + equal(results[0].get('string'), 'c'); + equal(results[1].get('number'), 3); + equal(results[1].get('string'), 'a'); + equal(results[2].get('number'), 2); + equal(results[2].get('string'), 'd'); + equal(results[3].get('number'), 1); + equal(results[3].get('string'), 'b'); done(); }); + }); }); - it("order by descending number and string", function(done) { - const strings = ["a", "b", "c", "d"]; + it('order by descending number and string, with space', function(done) { + const strings = ['a', 'b', 'c', 'd']; const makeBoxedNumber = function(num, i) { return new BoxedNumber({ number: num, string: strings[i] }); }; Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber)).then( function() { const query = new Parse.Query(BoxedNumber); - query.descending("number,string"); + query.descending('number, string'); query.find().then(function(results) { equal(results.length, 4); - equal(results[0].get("number"), 3); - equal(results[0].get("string"), "c"); - equal(results[1].get("number"), 3); - equal(results[1].get("string"), "a"); - equal(results[2].get("number"), 2); - equal(results[2].get("string"), "d"); - equal(results[3].get("number"), 1); - equal(results[3].get("string"), "b"); - done(); - }); - }); - }); - - it("order by descending number and string, with space", function(done) { - const strings = ["a", "b", "c", "d"]; - const makeBoxedNumber = function (num, i) { - return new BoxedNumber({number: num, string: strings[i]}); - }; - Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber)).then( - function () { - const query = new Parse.Query(BoxedNumber); - query.descending("number, string"); - query.find().then(function (results) { - equal(results.length, 4); - equal(results[0].get("number"), 3); - equal(results[0].get("string"), "c"); - equal(results[1].get("number"), 3); - equal(results[1].get("string"), "a"); - equal(results[2].get("number"), 2); - equal(results[2].get("string"), "d"); - equal(results[3].get("number"), 1); - equal(results[3].get("string"), "b"); + equal(results[0].get('number'), 3); + equal(results[0].get('string'), 'c'); + equal(results[1].get('number'), 3); + equal(results[1].get('string'), 'a'); + equal(results[2].get('number'), 2); + equal(results[2].get('string'), 'd'); + equal(results[3].get('number'), 1); + equal(results[3].get('string'), 'b'); done(); }); }, - (err) => { + err => { jfail(err); done(); - }); + } + ); }); - it("order by descending number and string, with array arg", function(done) { - const strings = ["a", "b", "c", "d"]; + it('order by descending number and string, with array arg', function(done) { + const strings = ['a', 'b', 'c', 'd']; const makeBoxedNumber = function(num, i) { return new BoxedNumber({ number: num, string: strings[i] }); }; - Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.descending(["number", "string"]); - query.find().then(function(results) { - equal(results.length, 4); - equal(results[0].get("number"), 3); - equal(results[0].get("string"), "c"); - equal(results[1].get("number"), 3); - equal(results[1].get("string"), "a"); - equal(results[2].get("number"), 2); - equal(results[2].get("string"), "d"); - equal(results[3].get("number"), 1); - equal(results[3].get("string"), "b"); - done(); - }); + Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber)).then(function() { + const query = new Parse.Query(BoxedNumber); + query.descending(['number', 'string']); + query.find().then(function(results) { + equal(results.length, 4); + equal(results[0].get('number'), 3); + equal(results[0].get('string'), 'c'); + equal(results[1].get('number'), 3); + equal(results[1].get('string'), 'a'); + equal(results[2].get('number'), 2); + equal(results[2].get('string'), 'd'); + equal(results[3].get('number'), 1); + equal(results[3].get('string'), 'b'); + done(); }); + }); }); - it("order by descending number and string, with multiple args", function(done) { - const strings = ["a", "b", "c", "d"]; + it('order by descending number and string, with multiple args', function(done) { + const strings = ['a', 'b', 'c', 'd']; const makeBoxedNumber = function(num, i) { return new BoxedNumber({ number: num, string: strings[i] }); }; - Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber)).then( - function() { - const query = new Parse.Query(BoxedNumber); - query.descending("number", "string"); - query.find().then(function(results) { - equal(results.length, 4); - equal(results[0].get("number"), 3); - equal(results[0].get("string"), "c"); - equal(results[1].get("number"), 3); - equal(results[1].get("string"), "a"); - equal(results[2].get("number"), 2); - equal(results[2].get("string"), "d"); - equal(results[3].get("number"), 1); - equal(results[3].get("string"), "b"); - done(); - }); + Parse.Object.saveAll([3, 1, 3, 2].map(makeBoxedNumber)).then(function() { + const query = new Parse.Query(BoxedNumber); + query.descending('number', 'string'); + query.find().then(function(results) { + equal(results.length, 4); + equal(results[0].get('number'), 3); + equal(results[0].get('string'), 'c'); + equal(results[1].get('number'), 3); + equal(results[1].get('string'), 'a'); + equal(results[2].get('number'), 2); + equal(results[2].get('string'), 'd'); + equal(results[3].get('number'), 1); + equal(results[3].get('string'), 'b'); + done(); }); + }); }); it("can't order by password", function(done) { @@ -1693,111 +1816,130 @@ describe('Parse.Query testing', () => { }; Parse.Object.saveAll([3, 1, 2].map(makeBoxedNumber)).then(function() { const query = new Parse.Query(BoxedNumber); - query.ascending("_password"); - query.find() + query.ascending('_password'); + query + .find() .then(done.fail) .catch(e => expect(e.code).toBe(Parse.Error.INVALID_KEY_NAME)) .finally(done); }); }); - it("order by _created_at", function(done) { + it('order by _created_at', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; const numbers = [3, 1, 2].map(makeBoxedNumber); - numbers[0].save().then(() => { - return numbers[1].save(); - }).then(() => { - return numbers[2].save(); - }).then(function() { - const query = new Parse.Query(BoxedNumber); - query.ascending("_created_at"); - query.find().then(function(results) { - equal(results.length, 3); - equal(results[0].get("number"), 3); - equal(results[1].get("number"), 1); - equal(results[2].get("number"), 2); - done(); - }, done.fail); - }); + numbers[0] + .save() + .then(() => { + return numbers[1].save(); + }) + .then(() => { + return numbers[2].save(); + }) + .then(function() { + const query = new Parse.Query(BoxedNumber); + query.ascending('_created_at'); + query.find().then(function(results) { + equal(results.length, 3); + equal(results[0].get('number'), 3); + equal(results[1].get('number'), 1); + equal(results[2].get('number'), 2); + done(); + }, done.fail); + }); }); - it("order by createdAt", function(done) { + it('order by createdAt', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; const numbers = [3, 1, 2].map(makeBoxedNumber); - numbers[0].save().then(() => { - return numbers[1].save(); - }).then(() => { - return numbers[2].save(); - }).then(function() { - const query = new Parse.Query(BoxedNumber); - query.descending("createdAt"); - query.find().then(function(results) { - equal(results.length, 3); - equal(results[0].get("number"), 2); - equal(results[1].get("number"), 1); - equal(results[2].get("number"), 3); - done(); + numbers[0] + .save() + .then(() => { + return numbers[1].save(); + }) + .then(() => { + return numbers[2].save(); + }) + .then(function() { + const query = new Parse.Query(BoxedNumber); + query.descending('createdAt'); + query.find().then(function(results) { + equal(results.length, 3); + equal(results[0].get('number'), 2); + equal(results[1].get('number'), 1); + equal(results[2].get('number'), 3); + done(); + }); }); - }); }); - it("order by _updated_at", function(done) { + it('order by _updated_at', function(done) { const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; const numbers = [3, 1, 2].map(makeBoxedNumber); - numbers[0].save().then(() => { - return numbers[1].save(); - }).then(() => { - return numbers[2].save(); - }).then(function() { - numbers[1].set("number", 4); - numbers[1].save().then(function() { - const query = new Parse.Query(BoxedNumber); - query.ascending("_updated_at"); - query.find().then(function(results) { - equal(results.length, 3); - equal(results[0].get("number"), 3); - equal(results[1].get("number"), 2); - equal(results[2].get("number"), 4); - done(); + numbers[0] + .save() + .then(() => { + return numbers[1].save(); + }) + .then(() => { + return numbers[2].save(); + }) + .then(function() { + numbers[1].set('number', 4); + numbers[1].save().then(function() { + const query = new Parse.Query(BoxedNumber); + query.ascending('_updated_at'); + query.find().then(function(results) { + equal(results.length, 3); + equal(results[0].get('number'), 3); + equal(results[1].get('number'), 2); + equal(results[2].get('number'), 4); + done(); + }); }); }); - }); }); - it("order by updatedAt", function(done) { - const makeBoxedNumber = function(i) { return new BoxedNumber({ number: i }); }; + it('order by updatedAt', function(done) { + const makeBoxedNumber = function(i) { + return new BoxedNumber({ number: i }); + }; const numbers = [3, 1, 2].map(makeBoxedNumber); - numbers[0].save().then(() => { - return numbers[1].save(); - }).then(() => { - return numbers[2].save(); - }).then(function() { - numbers[1].set("number", 4); - numbers[1].save().then(function() { - const query = new Parse.Query(BoxedNumber); - query.descending("_updated_at"); - query.find().then(function(results) { - equal(results.length, 3); - equal(results[0].get("number"), 4); - equal(results[1].get("number"), 2); - equal(results[2].get("number"), 3); - done(); + numbers[0] + .save() + .then(() => { + return numbers[1].save(); + }) + .then(() => { + return numbers[2].save(); + }) + .then(function() { + numbers[1].set('number', 4); + numbers[1].save().then(function() { + const query = new Parse.Query(BoxedNumber); + query.descending('_updated_at'); + query.find().then(function(results) { + equal(results.length, 3); + equal(results[0].get('number'), 4); + equal(results[1].get('number'), 2); + equal(results[2].get('number'), 3); + done(); + }); }); }); - }); }); // Returns a promise function makeTimeObject(start, i) { const time = new Date(); time.setSeconds(start.getSeconds() + i); - const item = new TestObject({name: "item" + i, time: time}); + const item = new TestObject({ name: 'item' + i, time: time }); return item.save(); } @@ -1805,34 +1947,37 @@ describe('Parse.Query testing', () => { function makeThreeTimeObjects() { const start = new Date(); let one, two, three; - return makeTimeObject(start, 1).then((o1) => { - one = o1; - return makeTimeObject(start, 2); - }).then((o2) => { - two = o2; - return makeTimeObject(start, 3); - }).then((o3) => { - three = o3; - return [one, two, three]; - }); + return makeTimeObject(start, 1) + .then(o1 => { + one = o1; + return makeTimeObject(start, 2); + }) + .then(o2 => { + two = o2; + return makeTimeObject(start, 3); + }) + .then(o3 => { + three = o3; + return [one, two, three]; + }); } - it("time equality", function(done) { + it('time equality', function(done) { makeThreeTimeObjects().then(function(list) { const query = new Parse.Query(TestObject); - query.equalTo("time", list[1].get("time")); + query.equalTo('time', list[1].get('time')); query.find().then(function(results) { equal(results.length, 1); - equal(results[0].get("name"), "item2"); + equal(results[0].get('name'), 'item2'); done(); }); }); }); - it("time lessThan", function(done) { + it('time lessThan', function(done) { makeThreeTimeObjects().then(function(list) { const query = new Parse.Query(TestObject); - query.lessThan("time", list[2].get("time")); + query.lessThan('time', list[2].get('time')); query.find().then(function(results) { equal(results.length, 2); done(); @@ -1841,10 +1986,10 @@ describe('Parse.Query testing', () => { }); // This test requires Date objects to be consistently stored as a Date. - it("time createdAt", function(done) { + it('time createdAt', function(done) { makeThreeTimeObjects().then(function(list) { const query = new Parse.Query(TestObject); - query.greaterThanOrEqualTo("createdAt", list[0].createdAt); + query.greaterThanOrEqualTo('createdAt', list[0].createdAt); query.find().then(function(results) { equal(results.length, 3); done(); @@ -1852,14 +1997,14 @@ describe('Parse.Query testing', () => { }); }); - it("matches string", function(done) { + it('matches string', function(done) { const thing1 = new TestObject(); - thing1.set("myString", "football"); + thing1.set('myString', 'football'); const thing2 = new TestObject(); - thing2.set("myString", "soccer"); + thing2.set('myString', 'soccer'); Parse.Object.saveAll([thing1, thing2]).then(function() { const query = new Parse.Query(TestObject); - query.matches("myString", "^fo*\\wb[^o]l+$"); + query.matches('myString', '^fo*\\wb[^o]l+$'); query.find().then(function(results) { equal(results.length, 1); done(); @@ -1867,14 +2012,14 @@ describe('Parse.Query testing', () => { }); }); - it("matches regex", function(done) { + it('matches regex', function(done) { const thing1 = new TestObject(); - thing1.set("myString", "football"); + thing1.set('myString', 'football'); const thing2 = new TestObject(); - thing2.set("myString", "soccer"); + thing2.set('myString', 'soccer'); Parse.Object.saveAll([thing1, thing2]).then(function() { const query = new Parse.Query(TestObject); - query.matches("myString", /^fo*\wb[^o]l+$/); + query.matches('myString', /^fo*\wb[^o]l+$/); query.find().then(function(results) { equal(results.length, 1); done(); @@ -1882,52 +2027,57 @@ describe('Parse.Query testing', () => { }); }); - it("case insensitive regex success", function(done) { + it('case insensitive regex success', function(done) { const thing = new TestObject(); - thing.set("myString", "football"); + thing.set('myString', 'football'); Parse.Object.saveAll([thing]).then(function() { const query = new Parse.Query(TestObject); - query.matches("myString", "FootBall", "i"); + query.matches('myString', 'FootBall', 'i'); query.find().then(done); }); }); - it("regexes with invalid options fail", function(done) { + it('regexes with invalid options fail', function(done) { const query = new Parse.Query(TestObject); - query.matches("myString", "FootBall", "some invalid option"); - query.find() + query.matches('myString', 'FootBall', 'some invalid option'); + query + .find() .then(done.fail) .catch(e => expect(e.code).toBe(Parse.Error.INVALID_QUERY)) .finally(done); }); - it("Use a regex that requires all modifiers", function(done) { + it('Use a regex that requires all modifiers', function(done) { const thing = new TestObject(); - thing.set("myString", "PArSe\nCom"); + thing.set('myString', 'PArSe\nCom'); Parse.Object.saveAll([thing]).then(function() { const query = new Parse.Query(TestObject); query.matches( - "myString", + 'myString', "parse # First fragment. We'll write this in one case but match " + - "insensitively\n.com # Second fragment. This can be separated by any " + - "character, including newline", - "mixs"); - query.find().then(function(results) { - equal(results.length, 1); - done(); - }, function(err) { - jfail(err); - done(); - }); + 'insensitively\n.com # Second fragment. This can be separated by any ' + + 'character, including newline', + 'mixs' + ); + query.find().then( + function(results) { + equal(results.length, 1); + done(); + }, + function(err) { + jfail(err); + done(); + } + ); }); }); - it("Regular expression constructor includes modifiers inline", function(done) { + it('Regular expression constructor includes modifiers inline', function(done) { const thing = new TestObject(); - thing.set("myString", "\n\nbuffer\n\nparse.COM"); + thing.set('myString', '\n\nbuffer\n\nparse.COM'); Parse.Object.saveAll([thing]).then(function() { const query = new Parse.Query(TestObject); - query.matches("myString", /parse\.com/mi); + query.matches('myString', /parse\.com/im); query.find().then(function(results) { equal(results.length, 1); done(); @@ -1935,16 +2085,19 @@ describe('Parse.Query testing', () => { }); }); - const someAscii = "\\E' !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTU" + + const someAscii = + "\\E' !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTU" + "VWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'"; - it("contains", function(done) { - Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}), - new TestObject({myString: "start" + someAscii}), - new TestObject({myString: someAscii + "end"}), - new TestObject({myString: someAscii})]).then(function() { + it('contains', function(done) { + Parse.Object.saveAll([ + new TestObject({ myString: 'zax' + someAscii + 'qub' }), + new TestObject({ myString: 'start' + someAscii }), + new TestObject({ myString: someAscii + 'end' }), + new TestObject({ myString: someAscii }), + ]).then(function() { const query = new Parse.Query(TestObject); - query.contains("myString", someAscii); + query.contains('myString', someAscii); query.find().then(function(results) { equal(results.length, 4); done(); @@ -1952,30 +2105,34 @@ describe('Parse.Query testing', () => { }); }); - it('nested contains', (done) => { + it('nested contains', done => { const sender1 = { group: ['A', 'B'] }; const sender2 = { group: ['A', 'C'] }; const sender3 = { group: ['B', 'C'] }; const obj1 = new TestObject({ sender: sender1 }); const obj2 = new TestObject({ sender: sender2 }); const obj3 = new TestObject({ sender: sender3 }); - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const query = new Parse.Query(TestObject); - query.contains('sender.group', 'A'); - return query.find(); - }).then((results) => { - equal(results.length, 2); - done(); - }, done.fail); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const query = new Parse.Query(TestObject); + query.contains('sender.group', 'A'); + return query.find(); + }) + .then(results => { + equal(results.length, 2); + done(); + }, done.fail); }); - it("startsWith", function(done) { - Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}), - new TestObject({myString: "start" + someAscii}), - new TestObject({myString: someAscii + "end"}), - new TestObject({myString: someAscii})]).then(function() { + it('startsWith', function(done) { + Parse.Object.saveAll([ + new TestObject({ myString: 'zax' + someAscii + 'qub' }), + new TestObject({ myString: 'start' + someAscii }), + new TestObject({ myString: someAscii + 'end' }), + new TestObject({ myString: someAscii }), + ]).then(function() { const query = new Parse.Query(TestObject); - query.startsWith("myString", someAscii); + query.startsWith('myString', someAscii); query.find().then(function(results) { equal(results.length, 2); done(); @@ -1983,13 +2140,15 @@ describe('Parse.Query testing', () => { }); }); - it("endsWith", function(done) { - Parse.Object.saveAll([new TestObject({myString: "zax" + someAscii + "qub"}), - new TestObject({myString: "start" + someAscii}), - new TestObject({myString: someAscii + "end"}), - new TestObject({myString: someAscii})]).then(function() { + it('endsWith', function(done) { + Parse.Object.saveAll([ + new TestObject({ myString: 'zax' + someAscii + 'qub' }), + new TestObject({ myString: 'start' + someAscii }), + new TestObject({ myString: someAscii + 'end' }), + new TestObject({ myString: someAscii }), + ]).then(function() { const query = new Parse.Query(TestObject); - query.endsWith("myString", someAscii); + query.endsWith('myString', someAscii); query.find().then(function(results) { equal(results.length, 2); done(); @@ -1997,7 +2156,7 @@ describe('Parse.Query testing', () => { }); }); - it("exists", function(done) { + it('exists', function(done) { const objects = []; for (const i of [0, 1, 2, 3, 4, 5, 6, 7, 8]) { const item = new TestObject(); @@ -2010,18 +2169,18 @@ describe('Parse.Query testing', () => { } Parse.Object.saveAll(objects).then(function() { const query = new Parse.Query(TestObject); - query.exists("x"); + query.exists('x'); query.find().then(function(results) { equal(results.length, 5); for (const result of results) { - ok(result.get("x")); + ok(result.get('x')); } done(); }); }); }); - it("doesNotExist", function(done) { + it('doesNotExist', function(done) { const objects = []; for (const i of [0, 1, 2, 3, 4, 5, 6, 7, 8]) { const item = new TestObject(); @@ -2034,18 +2193,18 @@ describe('Parse.Query testing', () => { } Parse.Object.saveAll(objects).then(function() { const query = new Parse.Query(TestObject); - query.doesNotExist("x"); + query.doesNotExist('x'); query.find().then(function(results) { equal(results.length, 4); for (const result of results) { - ok(result.get("y")); + ok(result.get('y')); } done(); }); }); }); - it("exists relation", function(done) { + it('exists relation', function(done) { const objects = []; for (const i of [0, 1, 2, 3, 4, 5, 6, 7, 8]) { const container = new Container(); @@ -2061,18 +2220,18 @@ describe('Parse.Query testing', () => { } Parse.Object.saveAll(objects).then(function() { const query = new Parse.Query(Container); - query.exists("x"); + query.exists('x'); query.find().then(function(results) { equal(results.length, 5); for (const result of results) { - ok(result.get("x")); + ok(result.get('x')); } done(); }); }); }); - it("doesNotExist relation", function(done) { + it('doesNotExist relation', function(done) { const objects = []; for (const i of [0, 1, 2, 3, 4, 5, 6, 7]) { const container = new Container(); @@ -2088,11 +2247,11 @@ describe('Parse.Query testing', () => { } Parse.Object.saveAll(objects).then(function() { const query = new Parse.Query(Container); - query.doesNotExist("x"); + query.doesNotExist('x'); query.find().then(function(results) { equal(results.length, 4); for (const result of results) { - ok(result.get("y")); + ok(result.get('y')); } done(); }); @@ -2102,8 +2261,8 @@ describe('Parse.Query testing', () => { it("don't include by default", function(done) { const child = new TestObject(); const parent = new Container(); - child.set("foo", "bar"); - parent.set("child", child); + child.set('foo', 'bar'); + parent.set('child', child); Parse.Object.saveAll([child, parent]).then(function() { child._clearServerData(); const query = new Parse.Query(Container); @@ -2111,86 +2270,92 @@ describe('Parse.Query testing', () => { equal(results.length, 1); const parentAgain = results[0]; const goodURL = Parse.serverURL; - Parse.serverURL = "YAAAAAAAAARRRRRGGGGGGGGG"; - const childAgain = parentAgain.get("child"); + Parse.serverURL = 'YAAAAAAAAARRRRRGGGGGGGGG'; + const childAgain = parentAgain.get('child'); ok(childAgain); - equal(childAgain.get("foo"), undefined); + equal(childAgain.get('foo'), undefined); Parse.serverURL = goodURL; done(); }); }); }); - it("include relation", function(done) { + it('include relation', function(done) { const child = new TestObject(); const parent = new Container(); - child.set("foo", "bar"); - parent.set("child", child); + child.set('foo', 'bar'); + parent.set('child', child); Parse.Object.saveAll([child, parent]).then(function() { const query = new Parse.Query(Container); - query.include("child"); + query.include('child'); query.find().then(function(results) { equal(results.length, 1); const parentAgain = results[0]; const goodURL = Parse.serverURL; - Parse.serverURL = "YAAAAAAAAARRRRRGGGGGGGGG"; - const childAgain = parentAgain.get("child"); + Parse.serverURL = 'YAAAAAAAAARRRRRGGGGGGGGG'; + const childAgain = parentAgain.get('child'); ok(childAgain); - equal(childAgain.get("foo"), "bar"); + equal(childAgain.get('foo'), 'bar'); Parse.serverURL = goodURL; done(); }); }); }); - it("include relation array", function(done) { + it('include relation array', function(done) { const child = new TestObject(); const parent = new Container(); - child.set("foo", "bar"); - parent.set("child", child); + child.set('foo', 'bar'); + parent.set('child', child); Parse.Object.saveAll([child, parent]).then(function() { const query = new Parse.Query(Container); - query.include(["child"]); + query.include(['child']); query.find().then(function(results) { equal(results.length, 1); const parentAgain = results[0]; const goodURL = Parse.serverURL; - Parse.serverURL = "YAAAAAAAAARRRRRGGGGGGGGG"; - const childAgain = parentAgain.get("child"); + Parse.serverURL = 'YAAAAAAAAARRRRRGGGGGGGGG'; + const childAgain = parentAgain.get('child'); ok(childAgain); - equal(childAgain.get("foo"), "bar"); + equal(childAgain.get('foo'), 'bar'); Parse.serverURL = goodURL; done(); }); }); }); - it("nested include", function(done) { - const Child = Parse.Object.extend("Child"); - const Parent = Parse.Object.extend("Parent"); - const Grandparent = Parse.Object.extend("Grandparent"); + it('nested include', function(done) { + const Child = Parse.Object.extend('Child'); + const Parent = Parse.Object.extend('Parent'); + const Grandparent = Parse.Object.extend('Grandparent'); const objects = []; for (let i = 0; i < 5; ++i) { const grandparent = new Grandparent({ - z:i, + z: i, parent: new Parent({ - y:i, + y: i, child: new Child({ - x:i - }) - }) + x: i, + }), + }), }); objects.push(grandparent); } Parse.Object.saveAll(objects).then(function() { const query = new Parse.Query(Grandparent); - query.include(["parent.child"]); + query.include(['parent.child']); query.find().then(function(results) { equal(results.length, 5); for (const object of results) { - equal(object.get("z"), object.get("parent").get("y")); - equal(object.get("z"), object.get("parent").get("child").get("x")); + equal(object.get('z'), object.get('parent').get('y')); + equal( + object.get('z'), + object + .get('parent') + .get('child') + .get('x') + ); } done(); }); @@ -2198,23 +2363,23 @@ describe('Parse.Query testing', () => { }); it("include doesn't make dirty wrong", function(done) { - const Parent = Parse.Object.extend("ParentObject"); - const Child = Parse.Object.extend("ChildObject"); + const Parent = Parse.Object.extend('ParentObject'); + const Child = Parse.Object.extend('ChildObject'); const parent = new Parent(); const child = new Child(); - child.set("foo", "bar"); - parent.set("child", child); + child.set('foo', 'bar'); + parent.set('child', child); Parse.Object.saveAll([child, parent]).then(function() { const query = new Parse.Query(Parent); - query.include("child"); + query.include('child'); query.find().then(function(results) { equal(results.length, 1); const parentAgain = results[0]; - const childAgain = parentAgain.get("child"); + const childAgain = parentAgain.get('child'); equal(childAgain.id, child.id); equal(parentAgain.id, parent.id); - equal(childAgain.get("foo"), "bar"); + equal(childAgain.get('foo'), 'bar'); equal(false, parentAgain.dirty()); equal(false, childAgain.dirty()); done(); @@ -2222,215 +2387,244 @@ describe('Parse.Query testing', () => { }); }); - it('properly includes array', (done) => { + it('properly includes array', done => { const objects = []; let total = 0; - while(objects.length != 5) { + while (objects.length != 5) { const object = new Parse.Object('AnObject'); object.set('key', objects.length); total += objects.length; objects.push(object); } - Parse.Object.saveAll(objects).then(() => { - const object = new Parse.Object("AContainer"); - object.set('objects', objects); - return object.save(); - }).then(() => { - const query = new Parse.Query('AContainer'); - query.include('objects'); - return query.find() - }).then((results) => { - expect(results.length).toBe(1); - const res = results[0]; - const 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) => { + Parse.Object.saveAll(objects) + .then(() => { + const object = new Parse.Object('AContainer'); + object.set('objects', objects); + return object.save(); + }) + .then(() => { + const query = new Parse.Query('AContainer'); + query.include('objects'); + return query.find(); + }) + .then( + results => { + expect(results.length).toBe(1); + const res = results[0]; + const 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 => { const objects = []; let total = 0; - while(objects.length != 5) { + while (objects.length != 5) { const object = new Parse.Object('AnObject'); object.set('key', objects.length); total += objects.length; objects.push(object); } - while(objects.length != 10) { + while (objects.length != 10) { const object = new Parse.Object('AnotherObject'); object.set('key', objects.length); total += objects.length; objects.push(object); } - Parse.Object.saveAll(objects).then(() => { - const object = new Parse.Object("AContainer"); - object.set('objects', objects); - return object.save(); - }).then(() => { - const query = new Parse.Query('AContainer'); - query.include('objects'); - return query.find() - }).then((results) => { - expect(results.length).toBe(1); - const res = results[0]; - const objects = res.get('objects'); - expect(objects.length).toBe(10); - objects.forEach((object) => { - total -= object.get('key'); - }); - expect(total).toBe(0); - done() - }, (e) => { - fail('should not fail'); - fail(JSON.stringify(e)); - done(); - }) - }); - - it('properly nested array of mixed objects with bad ids', (done) => { + Parse.Object.saveAll(objects) + .then(() => { + const object = new Parse.Object('AContainer'); + object.set('objects', objects); + return object.save(); + }) + .then(() => { + const query = new Parse.Query('AContainer'); + query.include('objects'); + return query.find(); + }) + .then( + results => { + expect(results.length).toBe(1); + const res = results[0]; + const objects = res.get('objects'); + expect(objects.length).toBe(10); + objects.forEach(object => { + total -= object.get('key'); + }); + expect(total).toBe(0); + done(); + }, + e => { + fail('should not fail'); + fail(JSON.stringify(e)); + done(); + } + ); + }); + + it('properly nested array of mixed objects with bad ids', done => { const objects = []; let total = 0; - while(objects.length != 5) { + while (objects.length != 5) { const object = new Parse.Object('AnObject'); object.set('key', objects.length); objects.push(object); } - while(objects.length != 10) { + while (objects.length != 10) { const object = new Parse.Object('AnotherObject'); object.set('key', objects.length); objects.push(object); } - Parse.Object.saveAll(objects).then(() => { - const object = new Parse.Object("AContainer"); - for (let i = 0; i < objects.length; i++) { - if (i % 2 == 0) { - objects[i].id = 'randomThing' - } else { - total += objects[i].get('key'); + Parse.Object.saveAll(objects) + .then(() => { + const object = new Parse.Object('AContainer'); + for (let 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(() => { + const query = new Parse.Query('AContainer'); + query.include('objects'); + return query.find(); + }) + .then( + results => { + expect(results.length).toBe(1); + const res = results[0]; + const objects = res.get('objects'); + expect(objects.length).toBe(5); + objects.forEach(object => { + total -= object.get('key'); + }); + expect(total).toBe(0); + done(); + }, + err => { + jfail(err); + fail('should not fail'); + done(); } - } - object.set('objects', objects); - return object.save(); - }).then(() => { - const query = new Parse.Query('AContainer'); - query.include('objects'); - return query.find() - }).then((results) => { - expect(results.length).toBe(1); - const res = results[0]; - const objects = res.get('objects'); - expect(objects.length).toBe(5); - objects.forEach((object) => { - total -= object.get('key'); - }); - expect(total).toBe(0); - done() - }, (err) => { - jfail(err); - fail('should not fail'); - done(); - }) - }); - - it('properly fetches nested pointers', (done) => { + ); + }); + + it('properly fetches nested pointers', done => { const color = new Parse.Object('Color'); - color.set('hex','#133733'); + color.set('hex', '#133733'); const circle = new Parse.Object('Circle'); circle.set('radius', 1337); - Parse.Object.saveAll([color, circle]).then(() => { - circle.set('color', color); - const badCircle = new Parse.Object('Circle'); - badCircle.id = 'badId'; - const complexFigure = new Parse.Object('ComplexFigure'); - complexFigure.set('consistsOf', [circle, badCircle]); - return complexFigure.save(); - }).then(() => { - const q = new Parse.Query('ComplexFigure'); - q.include('consistsOf.color'); - return q.find() - }).then((results) => { - expect(results.length).toBe(1); - const figure = results[0]; - expect(figure.get('consistsOf').length).toBe(1); - expect(figure.get('consistsOf')[0].get('color').get('hex')).toBe('#133733'); - done(); - }, () => { - fail('should not fail'); - done(); - }) - - }); - - it("result object creation uses current extension", function(done) { - const ParentObject = Parse.Object.extend({ className: "ParentObject" }); + Parse.Object.saveAll([color, circle]) + .then(() => { + circle.set('color', color); + const badCircle = new Parse.Object('Circle'); + badCircle.id = 'badId'; + const complexFigure = new Parse.Object('ComplexFigure'); + complexFigure.set('consistsOf', [circle, badCircle]); + return complexFigure.save(); + }) + .then(() => { + const q = new Parse.Query('ComplexFigure'); + q.include('consistsOf.color'); + return q.find(); + }) + .then( + results => { + expect(results.length).toBe(1); + const figure = results[0]; + expect(figure.get('consistsOf').length).toBe(1); + expect( + figure + .get('consistsOf')[0] + .get('color') + .get('hex') + ).toBe('#133733'); + done(); + }, + () => { + fail('should not fail'); + done(); + } + ); + }); + + it('result object creation uses current extension', function(done) { + const ParentObject = Parse.Object.extend({ className: 'ParentObject' }); // Add a foo() method to ChildObject. - let ChildObject = Parse.Object.extend("ChildObject", { + let ChildObject = Parse.Object.extend('ChildObject', { foo: function() { - return "foo"; - } + return 'foo'; + }, }); const parent = new ParentObject(); const child = new ChildObject(); - parent.set("child", child); + parent.set('child', child); Parse.Object.saveAll([child, parent]).then(function() { // Add a bar() method to ChildObject. - ChildObject = Parse.Object.extend("ChildObject", { + ChildObject = Parse.Object.extend('ChildObject', { bar: function() { - return "bar"; - } + return 'bar'; + }, }); const query = new Parse.Query(ParentObject); - query.include("child"); + query.include('child'); query.find().then(function(results) { equal(results.length, 1); const parentAgain = results[0]; - const childAgain = parentAgain.get("child"); - equal(childAgain.foo(), "foo"); - equal(childAgain.bar(), "bar"); + const childAgain = parentAgain.get('child'); + equal(childAgain.foo(), 'foo'); + equal(childAgain.bar(), 'bar'); done(); }); }); }); - it("matches query", function(done) { - const ParentObject = Parse.Object.extend("ParentObject"); - const ChildObject = Parse.Object.extend("ChildObject"); + it('matches query', function(done) { + const ParentObject = Parse.Object.extend('ParentObject'); + const ChildObject = Parse.Object.extend('ChildObject'); const objects = []; - for (let i = 0; i < 10; ++i) { + for (let i = 0; i < 10; ++i) { objects.push( new ParentObject({ - child: new ChildObject({x: i}), - x: 10 + i - })); + child: new ChildObject({ x: i }), + x: 10 + i, + }) + ); } Parse.Object.saveAll(objects).then(function() { const subQuery = new Parse.Query(ChildObject); - subQuery.greaterThan("x", 5); + subQuery.greaterThan('x', 5); const query = new Parse.Query(ParentObject); - query.matchesQuery("child", subQuery); + query.matchesQuery('child', subQuery); query.find().then(function(results) { equal(results.length, 4); for (const object of results) { - ok(object.get("x") > 15); + ok(object.get('x') > 15); } const query = new Parse.Query(ParentObject); - query.doesNotMatchQuery("child", subQuery); - query.find().then(function (results) { + query.doesNotMatchQuery('child', subQuery); + query.find().then(function(results) { equal(results.length, 6); for (const object of results) { - ok(object.get("x") >= 10); - ok(object.get("x") <= 15); + ok(object.get('x') >= 10); + ok(object.get('x') <= 15); done(); } }); @@ -2438,22 +2632,22 @@ describe('Parse.Query testing', () => { }); }); - it("select query", function(done) { - const RestaurantObject = Parse.Object.extend("Restaurant"); - const PersonObject = Parse.Object.extend("Person"); + it('select query', function(done) { + const RestaurantObject = Parse.Object.extend('Restaurant'); + const PersonObject = Parse.Object.extend('Person'); const objects = [ - new RestaurantObject({ ratings: 5, location: "Djibouti" }), - new RestaurantObject({ ratings: 3, location: "Ouagadougou" }), - new PersonObject({ name: "Bob", hometown: "Djibouti" }), - new PersonObject({ name: "Tom", hometown: "Ouagadougou" }), - new PersonObject({ name: "Billy", hometown: "Detroit" }) + new RestaurantObject({ ratings: 5, location: 'Djibouti' }), + new RestaurantObject({ ratings: 3, location: 'Ouagadougou' }), + new PersonObject({ name: 'Bob', hometown: 'Djibouti' }), + new PersonObject({ name: 'Tom', hometown: 'Ouagadougou' }), + new PersonObject({ name: 'Billy', hometown: 'Detroit' }), ]; Parse.Object.saveAll(objects).then(function() { const query = new Parse.Query(RestaurantObject); - query.greaterThan("ratings", 4); + query.greaterThan('ratings', 4); const mainQuery = new Parse.Query(PersonObject); - mainQuery.matchesKeyInQuery("hometown", "location", query); + mainQuery.matchesKeyInQuery('hometown', 'location', query); mainQuery.find().then(function(results) { equal(results.length, 1); equal(results[0].get('name'), 'Bob'); @@ -2462,38 +2656,43 @@ describe('Parse.Query testing', () => { }); }); - it('$select inside $or', (done) => { + it('$select inside $or', done => { const Restaurant = Parse.Object.extend('Restaurant'); const Person = Parse.Object.extend('Person'); const objects = [ - new Restaurant({ ratings: 5, location: "Djibouti" }), - new Restaurant({ ratings: 3, location: "Ouagadougou" }), - new Person({ name: "Bob", hometown: "Djibouti" }), - new Person({ name: "Tom", hometown: "Ouagadougou" }), - new Person({ name: "Billy", hometown: "Detroit" }) + new Restaurant({ ratings: 5, location: 'Djibouti' }), + new Restaurant({ ratings: 3, location: 'Ouagadougou' }), + new Person({ name: 'Bob', hometown: 'Djibouti' }), + new Person({ name: 'Tom', hometown: 'Ouagadougou' }), + new Person({ name: 'Billy', hometown: 'Detroit' }), ]; - Parse.Object.saveAll(objects).then(() => { - const subquery = new Parse.Query(Restaurant); - subquery.greaterThan('ratings', 4); - const query1 = new Parse.Query(Person); - query1.matchesKeyInQuery('hometown', 'location', subquery); - const query2 = new Parse.Query(Person); - query2.equalTo('name', 'Tom'); - const query = Parse.Query.or(query1, query2); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(2); - done(); - }, (error) => { - jfail(error); - done(); - }); + Parse.Object.saveAll(objects) + .then(() => { + const subquery = new Parse.Query(Restaurant); + subquery.greaterThan('ratings', 4); + const query1 = new Parse.Query(Person); + query1.matchesKeyInQuery('hometown', 'location', subquery); + const query2 = new Parse.Query(Person); + query2.equalTo('name', 'Tom'); + const query = Parse.Query.or(query1, query2); + return query.find(); + }) + .then( + results => { + expect(results.length).toEqual(2); + done(); + }, + error => { + jfail(error); + done(); + } + ); }); - it('$nor valid query', (done) => { - const objects = Array.from(Array(10).keys()).map((rating) => { - return new TestObject({ 'rating': rating }); + it('$nor valid query', done => { + const objects = Array.from(Array(10).keys()).map(rating => { + return new TestObject({ rating: rating }); }); const highValue = 5; @@ -2502,68 +2701,82 @@ describe('Parse.Query testing', () => { body: { where: { $nor: [ - { rating : { $gt : highValue } }, - { rating : { $lte : lowValue } }, - ] + { rating: { $gt: highValue } }, + { rating: { $lte: lowValue } }, + ], }, - } + }, }); - Parse.Object.saveAll(objects).then(() => { - return rp.get(Parse.serverURL + "/classes/TestObject", options); - }).then((results) => { - expect(results.results.length).toBe(highValue - lowValue); - expect(results.results.every(res => res.rating > lowValue && res.rating <= highValue)).toBe(true); - done(); - }); + Parse.Object.saveAll(objects) + .then(() => { + return rp.get(Parse.serverURL + '/classes/TestObject', options); + }) + .then(results => { + expect(results.results.length).toBe(highValue - lowValue); + expect( + results.results.every( + res => res.rating > lowValue && res.rating <= highValue + ) + ).toBe(true); + done(); + }); }); - it('$nor invalid query - empty array', (done) => { + it('$nor invalid query - empty array', done => { const options = Object.assign({}, masterKeyOptions, { body: { where: { $nor: [] }, - } + }, }); const obj = new TestObject(); - obj.save().then(() => { - return rp.get(Parse.serverURL + "/classes/TestObject", options); - }).then(done.fail).catch((error) => { - equal(error.error.code, Parse.Error.INVALID_QUERY); - done(); - }); + obj + .save() + .then(() => { + return rp.get(Parse.serverURL + '/classes/TestObject', options); + }) + .then(done.fail) + .catch(error => { + equal(error.error.code, Parse.Error.INVALID_QUERY); + done(); + }); }); - it('$nor invalid query - wrong type', (done) => { + it('$nor invalid query - wrong type', done => { const options = Object.assign({}, masterKeyOptions, { body: { where: { $nor: 1337 }, - } + }, }); const obj = new TestObject(); - obj.save().then(() => { - return rp.get(Parse.serverURL + "/classes/TestObject", options); - }).then(done.fail).catch((error) => { - equal(error.error.code, Parse.Error.INVALID_QUERY); - done(); - }); + obj + .save() + .then(() => { + return rp.get(Parse.serverURL + '/classes/TestObject', options); + }) + .then(done.fail) + .catch(error => { + equal(error.error.code, Parse.Error.INVALID_QUERY); + done(); + }); }); - it("dontSelect query", function(done) { - const RestaurantObject = Parse.Object.extend("Restaurant"); - const PersonObject = Parse.Object.extend("Person"); + it('dontSelect query', function(done) { + const RestaurantObject = Parse.Object.extend('Restaurant'); + const PersonObject = Parse.Object.extend('Person'); const objects = [ - new RestaurantObject({ ratings: 5, location: "Djibouti" }), - new RestaurantObject({ ratings: 3, location: "Ouagadougou" }), - new PersonObject({ name: "Bob", hometown: "Djibouti" }), - new PersonObject({ name: "Tom", hometown: "Ouagadougou" }), - new PersonObject({ name: "Billy", hometown: "Djibouti" }) + new RestaurantObject({ ratings: 5, location: 'Djibouti' }), + new RestaurantObject({ ratings: 3, location: 'Ouagadougou' }), + new PersonObject({ name: 'Bob', hometown: 'Djibouti' }), + new PersonObject({ name: 'Tom', hometown: 'Ouagadougou' }), + new PersonObject({ name: 'Billy', hometown: 'Djibouti' }), ]; Parse.Object.saveAll(objects).then(function() { const query = new Parse.Query(RestaurantObject); - query.greaterThan("ratings", 4); + query.greaterThan('ratings', 4); const mainQuery = new Parse.Query(PersonObject); - mainQuery.doesNotMatchKeyInQuery("hometown", "location", query); + mainQuery.doesNotMatchKeyInQuery('hometown', 'location', query); mainQuery.find().then(function(results) { equal(results.length, 1); equal(results[0].get('name'), 'Tom'); @@ -2572,21 +2785,21 @@ describe('Parse.Query testing', () => { }); }); - it("dontSelect query without conditions", function(done) { - const RestaurantObject = Parse.Object.extend("Restaurant"); - const PersonObject = Parse.Object.extend("Person"); + it('dontSelect query without conditions', function(done) { + const RestaurantObject = Parse.Object.extend('Restaurant'); + const PersonObject = Parse.Object.extend('Person'); const objects = [ - new RestaurantObject({ location: "Djibouti" }), - new RestaurantObject({ location: "Ouagadougou" }), - new PersonObject({ name: "Bob", hometown: "Djibouti" }), - new PersonObject({ name: "Tom", hometown: "Yoloblahblahblah" }), - new PersonObject({ name: "Billy", hometown: "Ouagadougou" }) + new RestaurantObject({ location: 'Djibouti' }), + new RestaurantObject({ location: 'Ouagadougou' }), + new PersonObject({ name: 'Bob', hometown: 'Djibouti' }), + new PersonObject({ name: 'Tom', hometown: 'Yoloblahblahblah' }), + new PersonObject({ name: 'Billy', hometown: 'Ouagadougou' }), ]; Parse.Object.saveAll(objects).then(function() { const query = new Parse.Query(RestaurantObject); const mainQuery = new Parse.Query(PersonObject); - mainQuery.doesNotMatchKeyInQuery("hometown", "location", query); + mainQuery.doesNotMatchKeyInQuery('hometown', 'location', query); mainQuery.find().then(results => { equal(results.length, 1); equal(results[0].get('name'), 'Tom'); @@ -2595,64 +2808,75 @@ describe('Parse.Query testing', () => { }); }); - it("equalTo on same column as $dontSelect should not break $dontSelect functionality (#3678)", function(done) { - const AuthorObject = Parse.Object.extend("Author"); - const BlockedObject = Parse.Object.extend("Blocked"); - const PostObject = Parse.Object.extend("Post"); + it('equalTo on same column as $dontSelect should not break $dontSelect functionality (#3678)', function(done) { + const AuthorObject = Parse.Object.extend('Author'); + const BlockedObject = Parse.Object.extend('Blocked'); + const PostObject = Parse.Object.extend('Post'); let postAuthor = null; let requestUser = null; - return new AuthorObject({ name: "Julius"}).save().then((user) => { - postAuthor = user; - return new AuthorObject({ name: "Bob"}).save(); - }).then((user) => { - requestUser = user; - const objects = [ - new PostObject({ author: postAuthor, title: "Lorem ipsum" }), - new PostObject({ author: requestUser, title: "Kafka" }), - new PostObject({ author: requestUser, title: "Brown fox" }), - new BlockedObject({ blockedBy: postAuthor, blockedUser: requestUser}) - ]; - return Parse.Object.saveAll(objects); - }).then(() => { - const banListQuery = new Parse.Query(BlockedObject); - banListQuery.equalTo("blockedUser", requestUser); - - return new Parse.Query(PostObject) - .equalTo("author", postAuthor) - .doesNotMatchKeyInQuery("author", "blockedBy", banListQuery) - .find() - .then((r) => { - expect(r.length).toEqual(0); - done(); - }, done.fail); - }) + return new AuthorObject({ name: 'Julius' }) + .save() + .then(user => { + postAuthor = user; + return new AuthorObject({ name: 'Bob' }).save(); + }) + .then(user => { + requestUser = user; + const objects = [ + new PostObject({ author: postAuthor, title: 'Lorem ipsum' }), + new PostObject({ author: requestUser, title: 'Kafka' }), + new PostObject({ author: requestUser, title: 'Brown fox' }), + new BlockedObject({ + blockedBy: postAuthor, + blockedUser: requestUser, + }), + ]; + return Parse.Object.saveAll(objects); + }) + .then(() => { + const banListQuery = new Parse.Query(BlockedObject); + banListQuery.equalTo('blockedUser', requestUser); + + return new Parse.Query(PostObject) + .equalTo('author', postAuthor) + .doesNotMatchKeyInQuery('author', 'blockedBy', banListQuery) + .find() + .then(r => { + expect(r.length).toEqual(0); + done(); + }, done.fail); + }); }); - it("multiple dontSelect query", function(done) { - const RestaurantObject = Parse.Object.extend("Restaurant"); - const PersonObject = Parse.Object.extend("Person"); + it('multiple dontSelect query', function(done) { + const RestaurantObject = Parse.Object.extend('Restaurant'); + const PersonObject = Parse.Object.extend('Person'); const objects = [ - new RestaurantObject({ ratings: 7, location: "Djibouti2" }), - new RestaurantObject({ ratings: 5, location: "Djibouti" }), - new RestaurantObject({ ratings: 3, location: "Ouagadougou" }), - new PersonObject({ name: "Bob2", hometown: "Djibouti2" }), - new PersonObject({ name: "Bob", hometown: "Djibouti" }), - new PersonObject({ name: "Tom", hometown: "Ouagadougou" }), + new RestaurantObject({ ratings: 7, location: 'Djibouti2' }), + new RestaurantObject({ ratings: 5, location: 'Djibouti' }), + new RestaurantObject({ ratings: 3, location: 'Ouagadougou' }), + new PersonObject({ name: 'Bob2', hometown: 'Djibouti2' }), + new PersonObject({ name: 'Bob', hometown: 'Djibouti' }), + new PersonObject({ name: 'Tom', hometown: 'Ouagadougou' }), ]; Parse.Object.saveAll(objects).then(function() { const query = new Parse.Query(RestaurantObject); - query.greaterThan("ratings", 6); + query.greaterThan('ratings', 6); const query2 = new Parse.Query(RestaurantObject); - query2.lessThan("ratings", 4); + query2.lessThan('ratings', 4); const subQuery = new Parse.Query(PersonObject); - subQuery.matchesKeyInQuery("hometown", "location", query); + subQuery.matchesKeyInQuery('hometown', 'location', query); const subQuery2 = new Parse.Query(PersonObject); - subQuery2.matchesKeyInQuery("hometown", "location", query2); + subQuery2.matchesKeyInQuery('hometown', 'location', query2); const mainQuery = new Parse.Query(PersonObject); - mainQuery.doesNotMatchKeyInQuery("objectId", "objectId", Parse.Query.or(subQuery, subQuery2)); + mainQuery.doesNotMatchKeyInQuery( + 'objectId', + 'objectId', + Parse.Query.or(subQuery, subQuery2) + ); mainQuery.find().then(function(results) { equal(results.length, 1); equal(results[0].get('name'), 'Bob'); @@ -2661,48 +2885,55 @@ describe('Parse.Query testing', () => { }); }); - it("object with length", function(done) { - const TestObject = Parse.Object.extend("TestObject"); + it('object with length', function(done) { + const TestObject = Parse.Object.extend('TestObject'); const obj = new TestObject(); - obj.set("length", 5); - equal(obj.get("length"), 5); - obj.save().then(function() { - const query = new Parse.Query(TestObject); - query.find().then(function(results) { - equal(results.length, 1); - equal(results[0].get("length"), 5); - done(); - }, function(error) { + obj.set('length', 5); + equal(obj.get('length'), 5); + obj.save().then( + function() { + const query = new Parse.Query(TestObject); + query.find().then( + function(results) { + equal(results.length, 1); + equal(results[0].get('length'), 5); + done(); + }, + function(error) { + ok(false, error.message); + done(); + } + ); + }, + function(error) { ok(false, error.message); done(); - }); - }, function(error) { - ok(false, error.message); - done(); - }); + } + ); }); - it("include user", function(done) { - Parse.User.signUp("bob", "password", { age: 21 }) - .then(function(user) { - const TestObject = Parse.Object.extend("TestObject"); - const obj = new TestObject(); - obj.save({ - owner: user - }).then(function(obj) { + it('include user', function(done) { + Parse.User.signUp('bob', 'password', { age: 21 }).then(function(user) { + const TestObject = Parse.Object.extend('TestObject'); + const obj = new TestObject(); + obj + .save({ + owner: user, + }) + .then(function(obj) { const query = new Parse.Query(TestObject); - query.include("owner"); + query.include('owner'); query.get(obj.id).then(function(objAgain) { equal(objAgain.id, obj.id); - ok(objAgain.get("owner") instanceof Parse.User); - equal(objAgain.get("owner").get("age"), 21); + ok(objAgain.get('owner') instanceof Parse.User); + equal(objAgain.get('owner').get('age'), 21); done(); }, done.fail); }, done.fail); - }, done.fail); + }, done.fail); }); - it("or queries", function(done) { + it('or queries', function(done) { const objects = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(x) { const object = new Parse.Object('BoxedNumber'); object.set('x', x); @@ -2725,7 +2956,7 @@ describe('Parse.Query testing', () => { }); // This relies on matchesQuery aka the $inQuery operator - it("or complex queries", function(done) { + it('or complex queries', function(done) { const objects = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(x) { const child = new Parse.Object('Child'); child.set('x', x); @@ -2750,73 +2981,78 @@ describe('Parse.Query testing', () => { }); }); - it("async methods", function(done) { + it('async methods', function(done) { const saves = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map(function(x) { - const obj = new Parse.Object("TestObject"); - obj.set("x", x + 1); + const obj = new Parse.Object('TestObject'); + obj.set('x', x + 1); return obj.save(); }); - Promise.all(saves).then(function() { - const query = new Parse.Query("TestObject"); - query.ascending("x"); - return query.first(); - - }).then(function(obj) { - equal(obj.get("x"), 1); - const query = new Parse.Query("TestObject"); - query.descending("x"); - return query.find(); - - }).then(function(results) { - equal(results.length, 10); - const query = new Parse.Query("TestObject"); - return query.get(results[0].id); - - }).then(function(obj1) { - equal(obj1.get("x"), 10); - const query = new Parse.Query("TestObject"); - return query.count(); - - }).then(function(count) { - equal(count, 10); - - }).then(function() { - done(); - - }); + Promise.all(saves) + .then(function() { + const query = new Parse.Query('TestObject'); + query.ascending('x'); + return query.first(); + }) + .then(function(obj) { + equal(obj.get('x'), 1); + const query = new Parse.Query('TestObject'); + query.descending('x'); + return query.find(); + }) + .then(function(results) { + equal(results.length, 10); + const query = new Parse.Query('TestObject'); + return query.get(results[0].id); + }) + .then(function(obj1) { + equal(obj1.get('x'), 10); + const query = new Parse.Query('TestObject'); + return query.count(); + }) + .then(function(count) { + equal(count, 10); + }) + .then(function() { + done(); + }); }); - it("query.each", function(done) { + it('query.each', function(done) { const TOTAL = 50; const COUNT = 25; const items = range(TOTAL).map(function(x) { const obj = new TestObject(); - obj.set("x", x); + obj.set('x', x); return obj; }); Parse.Object.saveAll(items).then(function() { const query = new Parse.Query(TestObject); - query.lessThan("x", COUNT); + query.lessThan('x', COUNT); const seen = []; - query.each(function(obj) { - seen[obj.get("x")] = (seen[obj.get("x")] || 0) + 1; - }, { - batchSize: 10, - }).then(function() { - equal(seen.length, COUNT); - for (let i = 0; i < COUNT; i++) { - equal(seen[i], 1, "Should have seen object number " + i); - } - done(); - }, done.fail); + query + .each( + function(obj) { + seen[obj.get('x')] = (seen[obj.get('x')] || 0) + 1; + }, + { + batchSize: 10, + } + ) + .then(function() { + equal(seen.length, COUNT); + for (let i = 0; i < COUNT; i++) { + equal(seen[i], 1, 'Should have seen object number ' + i); + } + done(); + }, done.fail); }); }); - it("query.each async", function(done) { + it('query.each async', function(done) { const TOTAL = 50; const COUNT = 25; @@ -2824,92 +3060,104 @@ describe('Parse.Query testing', () => { const items = range(TOTAL).map(function(x) { const obj = new TestObject(); - obj.set("x", x); + obj.set('x', x); return obj; }); const seen = []; - Parse.Object.saveAll(items).then(function() { - const query = new Parse.Query(TestObject); - query.lessThan("x", COUNT); - return query.each(function(obj) { - return new Promise((resolve) => { - process.nextTick(function() { - seen[obj.get("x")] = (seen[obj.get("x")] || 0) + 1; - resolve(); - }); - }); - }, { - batchSize: 10 + Parse.Object.saveAll(items) + .then(function() { + const query = new Parse.Query(TestObject); + query.lessThan('x', COUNT); + return query.each( + function(obj) { + return new Promise(resolve => { + process.nextTick(function() { + seen[obj.get('x')] = (seen[obj.get('x')] || 0) + 1; + resolve(); + }); + }); + }, + { + batchSize: 10, + } + ); + }) + .then(function() { + equal(seen.length, COUNT); + for (let i = 0; i < COUNT; i++) { + equal(seen[i], 1, 'Should have seen object number ' + i); + } + done(); }); - - }).then(function() { - equal(seen.length, COUNT); - for (let i = 0; i < COUNT; i++) { - equal(seen[i], 1, "Should have seen object number " + i); - } - done(); - }); }); - it("query.each fails with order", function(done) { + it('query.each fails with order', function(done) { const TOTAL = 50; const COUNT = 25; const items = range(TOTAL).map(function(x) { const obj = new TestObject(); - obj.set("x", x); + obj.set('x', x); return obj; }); const seen = []; - Parse.Object.saveAll(items).then(function() { - const query = new Parse.Query(TestObject); - query.lessThan("x", COUNT); - query.ascending("x"); - return query.each(function(obj) { - seen[obj.get("x")] = (seen[obj.get("x")] || 0) + 1; - }); - - }).then(function() { - ok(false, "This should have failed."); - done(); - }, function() { - done(); - }); + Parse.Object.saveAll(items) + .then(function() { + const query = new Parse.Query(TestObject); + query.lessThan('x', COUNT); + query.ascending('x'); + return query.each(function(obj) { + seen[obj.get('x')] = (seen[obj.get('x')] || 0) + 1; + }); + }) + .then( + function() { + ok(false, 'This should have failed.'); + done(); + }, + function() { + done(); + } + ); }); - it("query.each fails with skip", function(done) { + it('query.each fails with skip', function(done) { const TOTAL = 50; const COUNT = 25; const items = range(TOTAL).map(function(x) { const obj = new TestObject(); - obj.set("x", x); + obj.set('x', x); return obj; }); const seen = []; - Parse.Object.saveAll(items).then(function() { - const query = new Parse.Query(TestObject); - query.lessThan("x", COUNT); - query.skip(5); - return query.each(function(obj) { - seen[obj.get("x")] = (seen[obj.get("x")] || 0) + 1; - }); - - }).then(function() { - ok(false, "This should have failed."); - done(); - }, function() { - done(); - }); + Parse.Object.saveAll(items) + .then(function() { + const query = new Parse.Query(TestObject); + query.lessThan('x', COUNT); + query.skip(5); + return query.each(function(obj) { + seen[obj.get('x')] = (seen[obj.get('x')] || 0) + 1; + }); + }) + .then( + function() { + ok(false, 'This should have failed.'); + done(); + }, + function() { + done(); + } + ); }); - it("query.each fails with limit", function(done) { + it('query.each fails with limit', function(done) { const TOTAL = 50; const COUNT = 25; @@ -2917,86 +3165,113 @@ describe('Parse.Query testing', () => { const items = range(TOTAL).map(function(x) { const obj = new TestObject(); - obj.set("x", x); + obj.set('x', x); return obj; }); const seen = []; - Parse.Object.saveAll(items).then(function() { - const query = new Parse.Query(TestObject); - query.lessThan("x", COUNT); - query.limit(5); - return query.each(function(obj) { - seen[obj.get("x")] = (seen[obj.get("x")] || 0) + 1; - }); - - }).then(function() { - ok(false, "This should have failed."); - done(); - }, function() { - done(); - }); + Parse.Object.saveAll(items) + .then(function() { + const query = new Parse.Query(TestObject); + query.lessThan('x', COUNT); + query.limit(5); + return query.each(function(obj) { + seen[obj.get('x')] = (seen[obj.get('x')] || 0) + 1; + }); + }) + .then( + function() { + ok(false, 'This should have failed.'); + done(); + }, + function() { + done(); + } + ); }); - it("select keys query", function(done) { + it('select keys query', function(done) { const obj = new TestObject({ foo: 'baz', bar: 1 }); - obj.save().then(function () { - obj._clearServerData(); - const query = new Parse.Query(TestObject); - query.select('foo'); - return query.first(); - }).then(function(result) { - ok(result.id, "expected object id to be set"); - ok(result.createdAt, "expected object createdAt to be set"); - ok(result.updatedAt, "expected object updatedAt to be set"); - ok(!result.dirty(), "expected result not to be dirty"); - strictEqual(result.get('foo'), 'baz'); - strictEqual(result.get('bar'), undefined, - "expected 'bar' field to be unset"); - return result.fetch(); - }).then(function(result) { - strictEqual(result.get('foo'), 'baz'); - strictEqual(result.get('bar'), 1); - }).then(function() { - obj._clearServerData(); - const query = new Parse.Query(TestObject); - query.select([]); - return query.first(); - }).then(function(result) { - ok(result.id, "expected object id to be set"); - ok(!result.dirty(), "expected result not to be dirty"); - strictEqual(result.get('foo'), undefined, - "expected 'foo' field to be unset"); - strictEqual(result.get('bar'), undefined, - "expected 'bar' field to be unset"); - }).then(function() { - obj._clearServerData(); - const query = new Parse.Query(TestObject); - query.select(['foo','bar']); - return query.first(); - }).then(function(result) { - ok(result.id, "expected object id to be set"); - ok(!result.dirty(), "expected result not to be dirty"); - strictEqual(result.get('foo'), 'baz'); - strictEqual(result.get('bar'), 1); - }).then(function() { - obj._clearServerData(); - const query = new Parse.Query(TestObject); - query.select('foo', 'bar'); - return query.first(); - }).then(function(result) { - ok(result.id, "expected object id to be set"); - ok(!result.dirty(), "expected result not to be dirty"); - strictEqual(result.get('foo'), 'baz'); - strictEqual(result.get('bar'), 1); - }).then(function() { - done(); - }, function (err) { - ok(false, "other error: " + JSON.stringify(err)); - done(); - }); + obj + .save() + .then(function() { + obj._clearServerData(); + const query = new Parse.Query(TestObject); + query.select('foo'); + return query.first(); + }) + .then(function(result) { + ok(result.id, 'expected object id to be set'); + ok(result.createdAt, 'expected object createdAt to be set'); + ok(result.updatedAt, 'expected object updatedAt to be set'); + ok(!result.dirty(), 'expected result not to be dirty'); + strictEqual(result.get('foo'), 'baz'); + strictEqual( + result.get('bar'), + undefined, + "expected 'bar' field to be unset" + ); + return result.fetch(); + }) + .then(function(result) { + strictEqual(result.get('foo'), 'baz'); + strictEqual(result.get('bar'), 1); + }) + .then(function() { + obj._clearServerData(); + const query = new Parse.Query(TestObject); + query.select([]); + return query.first(); + }) + .then(function(result) { + ok(result.id, 'expected object id to be set'); + ok(!result.dirty(), 'expected result not to be dirty'); + strictEqual( + result.get('foo'), + undefined, + "expected 'foo' field to be unset" + ); + strictEqual( + result.get('bar'), + undefined, + "expected 'bar' field to be unset" + ); + }) + .then(function() { + obj._clearServerData(); + const query = new Parse.Query(TestObject); + query.select(['foo', 'bar']); + return query.first(); + }) + .then(function(result) { + ok(result.id, 'expected object id to be set'); + ok(!result.dirty(), 'expected result not to be dirty'); + strictEqual(result.get('foo'), 'baz'); + strictEqual(result.get('bar'), 1); + }) + .then(function() { + obj._clearServerData(); + const query = new Parse.Query(TestObject); + query.select('foo', 'bar'); + return query.first(); + }) + .then(function(result) { + ok(result.id, 'expected object id to be set'); + ok(!result.dirty(), 'expected result not to be dirty'); + strictEqual(result.get('foo'), 'baz'); + strictEqual(result.get('bar'), 1); + }) + .then( + function() { + done(); + }, + function(err) { + ok(false, 'other error: ' + JSON.stringify(err)); + done(); + } + ); }); it('select keys with each query', function(done) { @@ -3006,29 +3281,37 @@ describe('Parse.Query testing', () => { obj._clearServerData(); const query = new Parse.Query(TestObject); query.select('foo'); - query.each(function(result) { - ok(result.id, 'expected object id to be set'); - ok(result.createdAt, 'expected object createdAt to be set'); - ok(result.updatedAt, 'expected object updatedAt to be set'); - ok(!result.dirty(), 'expected result not to be dirty'); - strictEqual(result.get('foo'), 'baz'); - strictEqual(result.get('bar'), undefined, - 'expected "bar" field to be unset'); - }).then(function() { - done(); - }, function(err) { - jfail(err); - done(); - }); + query + .each(function(result) { + ok(result.id, 'expected object id to be set'); + ok(result.createdAt, 'expected object createdAt to be set'); + ok(result.updatedAt, 'expected object updatedAt to be set'); + ok(!result.dirty(), 'expected result not to be dirty'); + strictEqual(result.get('foo'), 'baz'); + strictEqual( + result.get('bar'), + undefined, + 'expected "bar" field to be unset' + ); + }) + .then( + function() { + done(); + }, + function(err) { + jfail(err); + done(); + } + ); }); }); - it('notEqual with array of pointers', (done) => { + it('notEqual with array of pointers', done => { const children = []; const parents = []; const promises = []; - for (let i = 0; i < 2; i++) { - const proc = (iter) => { + for (let i = 0; i < 2; i++) { + const proc = iter => { const child = new Parse.Object('Child'); children.push(child); const parent = new Parse.Object('Parent'); @@ -3042,345 +3325,456 @@ describe('Parse.Query testing', () => { }; proc(i); } - Promise.all(promises).then(() => { - const query = new Parse.Query('Parent'); - query.notEqualTo('child', children[0]); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(1); - expect(results[0].id).toEqual(parents[1].id); - done(); - }).catch((error) => { console.log(error); }); + Promise.all(promises) + .then(() => { + const query = new Parse.Query('Parent'); + query.notEqualTo('child', children[0]); + return query.find(); + }) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].id).toEqual(parents[1].id); + done(); + }) + .catch(error => { + console.log(error); + }); }); // PG don't support creating a null column - it_exclude_dbs(['postgres'])('querying for null value', (done) => { + it_exclude_dbs(['postgres'])('querying for null value', done => { const obj = new Parse.Object('TestObject'); obj.set('aNull', null); - obj.save().then(() => { - const query = new Parse.Query('TestObject'); - query.equalTo('aNull', null); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(1); - expect(results[0].get('aNull')).toEqual(null); - done(); - }) - }); - - it('query within dictionary', (done) => { + obj + .save() + .then(() => { + const query = new Parse.Query('TestObject'); + query.equalTo('aNull', null); + return query.find(); + }) + .then(results => { + expect(results.length).toEqual(1); + expect(results[0].get('aNull')).toEqual(null); + done(); + }); + }); + + it('query within dictionary', done => { const promises = []; - for (let i = 0; i < 2; i++) { - const proc = (iter) => { + for (let i = 0; i < 2; i++) { + const proc = iter => { const obj = new Parse.Object('TestObject'); obj.set('aDict', { x: iter + 1, y: iter + 2 }); promises.push(obj.save()); }; proc(i); } - Promise.all(promises).then(() => { - const query = new Parse.Query('TestObject'); - query.equalTo('aDict.x', 1); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(1); - done(); - }, (error) => { - console.log(error); - }); + Promise.all(promises) + .then(() => { + const query = new Parse.Query('TestObject'); + query.equalTo('aDict.x', 1); + return query.find(); + }) + .then( + results => { + expect(results.length).toEqual(1); + done(); + }, + error => { + console.log(error); + } + ); }); it('supports include on the wrong key type (#2262)', function(done) { const childObject = new Parse.Object('TestChildObject'); childObject.set('hello', 'world'); - childObject.save().then(() => { - const obj = new Parse.Object('TestObject'); - obj.set('foo', 'bar'); - obj.set('child', childObject); - return obj.save(); - }).then(() => { - const q = new Parse.Query('TestObject'); - q.include('child'); - q.include('child.parent'); - q.include('createdAt'); - q.include('createdAt.createdAt'); - return q.find(); - }).then((objs) => { - expect(objs.length).toBe(1); - expect(objs[0].get('child').get('hello')).toEqual('world'); - expect(objs[0].createdAt instanceof Date).toBe(true); - done(); - }, () => { - fail('should not fail'); - done(); - }); - }); - - it('query match on array with single object', (done) => { - const target = {__type: 'Pointer', className: 'TestObject', objectId: 'abc123'}; + childObject + .save() + .then(() => { + const obj = new Parse.Object('TestObject'); + obj.set('foo', 'bar'); + obj.set('child', childObject); + return obj.save(); + }) + .then(() => { + const q = new Parse.Query('TestObject'); + q.include('child'); + q.include('child.parent'); + q.include('createdAt'); + q.include('createdAt.createdAt'); + return q.find(); + }) + .then( + objs => { + expect(objs.length).toBe(1); + expect(objs[0].get('child').get('hello')).toEqual('world'); + expect(objs[0].createdAt instanceof Date).toBe(true); + done(); + }, + () => { + fail('should not fail'); + done(); + } + ); + }); + + it('query match on array with single object', done => { + const target = { + __type: 'Pointer', + className: 'TestObject', + objectId: 'abc123', + }; const obj = new Parse.Object('TestObject'); obj.set('someObjs', [target]); - obj.save().then(() => { - const query = new Parse.Query('TestObject'); - query.equalTo('someObjs', target); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(1); - done(); - }, (error) => { - console.log(error); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('TestObject'); + query.equalTo('someObjs', target); + return query.find(); + }) + .then( + results => { + expect(results.length).toEqual(1); + done(); + }, + error => { + console.log(error); + } + ); }); - it('query match on array with multiple objects', (done) => { - const target1 = {__type: 'Pointer', className: 'TestObject', objectId: 'abc'}; - const target2 = {__type: 'Pointer', className: 'TestObject', objectId: '123'}; + it('query match on array with multiple objects', done => { + const target1 = { + __type: 'Pointer', + className: 'TestObject', + objectId: 'abc', + }; + const target2 = { + __type: 'Pointer', + className: 'TestObject', + objectId: '123', + }; const obj = new Parse.Object('TestObject'); obj.set('someObjs', [target1, target2]); - obj.save().then(() => { - const query = new Parse.Query('TestObject'); - query.equalTo('someObjs', target1); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(1); - done(); - }, (error) => { - console.log(error); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('TestObject'); + query.equalTo('someObjs', target1); + return query.find(); + }) + .then( + results => { + expect(results.length).toEqual(1); + done(); + }, + error => { + console.log(error); + } + ); }); - it('query should not match on array when searching for null', (done) => { - const target = {__type: 'Pointer', className: 'TestObject', objectId: '123'}; + it('query should not match on array when searching for null', done => { + const target = { + __type: 'Pointer', + className: 'TestObject', + objectId: '123', + }; const obj = new Parse.Object('TestObject'); obj.set('someKey', 'someValue'); obj.set('someObjs', [target]); - obj.save().then(() => { - const query = new Parse.Query('TestObject'); - query.equalTo('someKey', 'someValue'); - query.equalTo('someObjs', null); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(0); - done(); - }, (error) => { - console.log(error); - }); + obj + .save() + .then(() => { + const query = new Parse.Query('TestObject'); + query.equalTo('someKey', 'someValue'); + query.equalTo('someObjs', null); + return query.find(); + }) + .then( + results => { + expect(results.length).toEqual(0); + done(); + }, + error => { + console.log(error); + } + ); }); // #371 - it('should properly interpret a query v1', (done) => { - const query = new Parse.Query("C1"); - const auxQuery = new Parse.Query("C1"); - query.matchesKeyInQuery("A1", "A2", auxQuery); - query.include("A3"); - query.include("A2"); - query.find().then(() => { - done(); - }, (err) => { - jfail(err); - fail("should not failt"); - done(); - }) - }); - - it('should properly interpret a query v2', (done) => { - const user = new Parse.User(); - user.set("username", "foo"); - user.set("password", "bar"); - return user.save().then((user) => { - const objIdQuery = new Parse.Query("_User").equalTo("objectId", user.id); - const blockedUserQuery = user.relation("blockedUsers").query(); - - const aResponseQuery = new Parse.Query("MatchRelationshipActivityResponse"); - aResponseQuery.equalTo("userA", user); - aResponseQuery.equalTo("userAResponse", 1); + it('should properly interpret a query v1', done => { + const query = new Parse.Query('C1'); + const auxQuery = new Parse.Query('C1'); + query.matchesKeyInQuery('A1', 'A2', auxQuery); + query.include('A3'); + query.include('A2'); + query.find().then( + () => { + done(); + }, + err => { + jfail(err); + fail('should not failt'); + done(); + } + ); + }); - const bResponseQuery = new Parse.Query("MatchRelationshipActivityResponse"); - bResponseQuery.equalTo("userB", user); - bResponseQuery.equalTo("userBResponse", 1); + it('should properly interpret a query v2', done => { + const user = new Parse.User(); + user.set('username', 'foo'); + user.set('password', 'bar'); + return user + .save() + .then(user => { + const objIdQuery = new Parse.Query('_User').equalTo( + 'objectId', + user.id + ); + const blockedUserQuery = user.relation('blockedUsers').query(); - const matchOr = Parse.Query.or(aResponseQuery, bResponseQuery); - const matchRelationshipA = new Parse.Query("_User"); - matchRelationshipA.matchesKeyInQuery("objectId", "userAObjectId", matchOr); - const matchRelationshipB = new Parse.Query("_User"); - matchRelationshipB.matchesKeyInQuery("objectId", "userBObjectId", matchOr); + const aResponseQuery = new Parse.Query( + 'MatchRelationshipActivityResponse' + ); + aResponseQuery.equalTo('userA', user); + aResponseQuery.equalTo('userAResponse', 1); + const bResponseQuery = new Parse.Query( + 'MatchRelationshipActivityResponse' + ); + bResponseQuery.equalTo('userB', user); + bResponseQuery.equalTo('userBResponse', 1); + + const matchOr = Parse.Query.or(aResponseQuery, bResponseQuery); + const matchRelationshipA = new Parse.Query('_User'); + matchRelationshipA.matchesKeyInQuery( + 'objectId', + 'userAObjectId', + matchOr + ); + const matchRelationshipB = new Parse.Query('_User'); + matchRelationshipB.matchesKeyInQuery( + 'objectId', + 'userBObjectId', + matchOr + ); - const orQuery = Parse.Query.or(objIdQuery, blockedUserQuery, matchRelationshipA, matchRelationshipB); - const query = new Parse.Query("_User"); - query.doesNotMatchQuery("objectId", orQuery); - return query.find(); - }).then(() => { - done(); - }, (err) => { - jfail(err); - fail("should not fail"); - done(); - }); + const orQuery = Parse.Query.or( + objIdQuery, + blockedUserQuery, + matchRelationshipA, + matchRelationshipB + ); + const query = new Parse.Query('_User'); + query.doesNotMatchQuery('objectId', orQuery); + return query.find(); + }) + .then( + () => { + done(); + }, + err => { + jfail(err); + fail('should not fail'); + done(); + } + ); }); - it("should match a key in an array (#3195)", function(done) { - const AuthorObject = Parse.Object.extend("Author"); - const GroupObject = Parse.Object.extend("Group"); - const PostObject = Parse.Object.extend("Post"); + it('should match a key in an array (#3195)', function(done) { + const AuthorObject = Parse.Object.extend('Author'); + const GroupObject = Parse.Object.extend('Group'); + const PostObject = Parse.Object.extend('Post'); - return new AuthorObject().save().then((user) => { - const post = new PostObject({ - author: user - }); + return new AuthorObject() + .save() + .then(user => { + const post = new PostObject({ + author: user, + }); - const group = new GroupObject({ - members: [user], - }); + const group = new GroupObject({ + members: [user], + }); - return Promise.all([post.save(), group.save()]); - }).then((results) => { - const p = results[0]; - return new Parse.Query(PostObject) - .matchesKeyInQuery("author", "members", new Parse.Query(GroupObject)) - .find() - .then((r) => { - expect(r.length).toEqual(1); - if (r.length > 0) { - expect(r[0].id).toEqual(p.id); - } - done(); - }, done.fail); - }); + return Promise.all([post.save(), group.save()]); + }) + .then(results => { + const p = results[0]; + return new Parse.Query(PostObject) + .matchesKeyInQuery('author', 'members', new Parse.Query(GroupObject)) + .find() + .then(r => { + expect(r.length).toEqual(1); + if (r.length > 0) { + expect(r[0].id).toEqual(p.id); + } + done(); + }, done.fail); + }); }); - it('should find objects with array of pointers', (done) => { + it('should find objects with array of pointers', done => { const objects = []; - while(objects.length != 5) { + while (objects.length != 5) { const object = new Parse.Object('ContainedObject'); object.set('index', objects.length); objects.push(object); } - Parse.Object.saveAll(objects).then((objects) => { - const container = new Parse.Object('Container'); - const pointers = objects.map((obj) => { - return { - __type: 'Pointer', - className: 'ContainedObject', - objectId: obj.id + Parse.Object.saveAll(objects) + .then(objects => { + const container = new Parse.Object('Container'); + const pointers = objects.map(obj => { + return { + __type: 'Pointer', + className: 'ContainedObject', + objectId: obj.id, + }; + }); + container.set('objects', pointers); + const container2 = new Parse.Object('Container'); + container2.set('objects', pointers.slice(2, 3)); + return Parse.Object.saveAll([container, container2]); + }) + .then(() => { + const inQuery = new Parse.Query('ContainedObject'); + inQuery.greaterThanOrEqualTo('index', 1); + const query = new Parse.Query('Container'); + query.matchesQuery('objects', inQuery); + return query.find(); + }) + .then(results => { + if (results) { + expect(results.length).toBe(2); } + done(); }) - container.set('objects', pointers); - const container2 = new Parse.Object('Container'); - container2.set('objects', pointers.slice(2, 3)); - return Parse.Object.saveAll([container, container2]); - }).then(() => { - const inQuery = new Parse.Query('ContainedObject'); - inQuery.greaterThanOrEqualTo('index', 1); - const query = new Parse.Query('Container'); - query.matchesQuery('objects', inQuery); - return query.find(); - }).then((results) => { - if (results) { - expect(results.length).toBe(2); - } - done(); - }).catch((err) => { - jfail(err); - fail('should not fail'); - done(); - }) - }) + .catch(err => { + jfail(err); + fail('should not fail'); + done(); + }); + }); it('query with two OR subqueries (regression test #1259)', done => { const relatedObject = new Parse.Object('Class2'); - relatedObject.save().then(relatedObject => { - const anObject = new Parse.Object('Class1'); - const relation = anObject.relation('relation'); - relation.add(relatedObject); - return anObject.save(); - }).then(anObject => { - const q1 = anObject.relation('relation').query(); - q1.doesNotExist('nonExistantKey1'); - const q2 = anObject.relation('relation').query(); - q2.doesNotExist('nonExistantKey2'); - Parse.Query.or(q1, q2).find().then(results => { - expect(results.length).toEqual(1); - if (results.length == 1) { - expect(results[0].objectId).toEqual(q1.objectId); - } - done(); + relatedObject + .save() + .then(relatedObject => { + const anObject = new Parse.Object('Class1'); + const relation = anObject.relation('relation'); + relation.add(relatedObject); + return anObject.save(); + }) + .then(anObject => { + const q1 = anObject.relation('relation').query(); + q1.doesNotExist('nonExistantKey1'); + const q2 = anObject.relation('relation').query(); + q2.doesNotExist('nonExistantKey2'); + Parse.Query.or(q1, q2) + .find() + .then(results => { + expect(results.length).toEqual(1); + if (results.length == 1) { + expect(results[0].objectId).toEqual(q1.objectId); + } + done(); + }); }); - }); }); it('objectId containedIn with multiple large array', done => { const obj = new Parse.Object('MyClass'); - obj.save().then(obj => { - const longListOfStrings = []; - for (let i = 0; i < 130; i++) { - longListOfStrings.push(i.toString()); - } - longListOfStrings.push(obj.id); - const q = new Parse.Query('MyClass'); - q.containedIn('objectId', longListOfStrings); - q.containedIn('objectId', longListOfStrings); - return q.find(); - }).then(results => { - expect(results.length).toEqual(1); - done(); - }); + obj + .save() + .then(obj => { + const longListOfStrings = []; + for (let i = 0; i < 130; i++) { + longListOfStrings.push(i.toString()); + } + longListOfStrings.push(obj.id); + const 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('containedIn with pointers should work with string array', done => { const obj = new Parse.Object('MyClass'); const child = new Parse.Object('Child'); - child.save().then(() => { - obj.set('child', child); - return obj.save(); - }).then(() => { - const objs = []; - for(let i = 0; i < 10; i++) { - objs.push(new Parse.Object('MyClass')); - } - return Parse.Object.saveAll(objs); - }).then(() => { - const query = new Parse.Query('MyClass'); - query.containedIn('child', [child.id]); - return query.find(); - }).then((results) => { - expect(results.length).toBe(1); - }).then(done).catch(done.fail); + child + .save() + .then(() => { + obj.set('child', child); + return obj.save(); + }) + .then(() => { + const objs = []; + for (let i = 0; i < 10; i++) { + objs.push(new Parse.Object('MyClass')); + } + return Parse.Object.saveAll(objs); + }) + .then(() => { + const query = new Parse.Query('MyClass'); + query.containedIn('child', [child.id]); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(1); + }) + .then(done) + .catch(done.fail); }); it('containedIn with pointers should work with string array, with many objects', done => { const objs = []; const children = []; - for(let i = 0; i < 10; i++) { + for (let i = 0; i < 10; i++) { const obj = new Parse.Object('MyClass'); const child = new Parse.Object('Child'); objs.push(obj); children.push(child); } - Parse.Object.saveAll(children).then(() => { - return Parse.Object.saveAll(objs.map((obj, i) => { - obj.set('child', children[i]); - return obj; - })); - }).then(() => { - const query = new Parse.Query('MyClass'); - const subset = children.slice(0, 5).map((child) => { - return child.id; - }); - query.containedIn('child', subset); - return query.find(); - }).then((results) => { - expect(results.length).toBe(5); - }).then(done).catch(done.fail); - }); - - it('include for specific object', function(done){ + Parse.Object.saveAll(children) + .then(() => { + return Parse.Object.saveAll( + objs.map((obj, i) => { + obj.set('child', children[i]); + return obj; + }) + ); + }) + .then(() => { + const query = new Parse.Query('MyClass'); + const subset = children.slice(0, 5).map(child => { + return child.id; + }); + query.containedIn('child', subset); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(5); + }) + .then(done) + .catch(done.fail); + }); + + it('include for specific object', function(done) { const child = new Parse.Object('Child'); const parent = new Parse.Object('Parent'); child.set('foo', 'bar'); parent.set('child', child); - Parse.Object.saveAll([child, parent]).then(function(response){ + Parse.Object.saveAll([child, parent]).then(function(response) { const savedParent = response[1]; const parentQuery = new Parse.Query('Parent'); parentQuery.include('child'); @@ -3397,10 +3791,10 @@ describe('Parse.Query testing', () => { const Foobar = new Parse.Object('Foobar'); Foobar.set('foo', 'bar'); Foobar.set('fizz', 'buzz'); - Foobar.save().then(function(savedFoobar){ + Foobar.save().then(function(savedFoobar) { const foobarQuery = new Parse.Query('Foobar'); foobarQuery.select('fizz'); - foobarQuery.get(savedFoobar.id).then(function(foobarObj){ + foobarQuery.get(savedFoobar.id).then(function(foobarObj) { equal(foobarObj.get('fizz'), 'buzz'); equal(foobarObj.get('foo'), undefined); done(); @@ -3413,27 +3807,29 @@ describe('Parse.Query testing', () => { const BarBaz = new Parse.Object('Barbaz'); BarBaz.set('key', 'value'); BarBaz.set('otherKey', 'value'); - BarBaz.save().then(() => { - Foobar.set('foo', 'bar'); - Foobar.set('fizz', 'buzz'); - Foobar.set('barBaz', BarBaz); - return Foobar.save(); - }).then(function(savedFoobar){ - const foobarQuery = new Parse.Query('Foobar'); - foobarQuery.include('barBaz'); - foobarQuery.select(['fizz', 'barBaz.key']); - foobarQuery.get(savedFoobar.id).then(function(foobarObj) { - equal(foobarObj.get('fizz'), 'buzz'); - equal(foobarObj.get('foo'), undefined); - if (foobarObj.has('barBaz')) { - equal(foobarObj.get('barBaz').get('key'), 'value'); - equal(foobarObj.get('barBaz').get('otherKey'), undefined); - } else { - fail('barBaz should be set'); - } - done(); + BarBaz.save() + .then(() => { + Foobar.set('foo', 'bar'); + Foobar.set('fizz', 'buzz'); + Foobar.set('barBaz', BarBaz); + return Foobar.save(); + }) + .then(function(savedFoobar) { + const foobarQuery = new Parse.Query('Foobar'); + foobarQuery.include('barBaz'); + foobarQuery.select(['fizz', 'barBaz.key']); + foobarQuery.get(savedFoobar.id).then(function(foobarObj) { + equal(foobarObj.get('fizz'), 'buzz'); + equal(foobarObj.get('foo'), undefined); + if (foobarObj.has('barBaz')) { + equal(foobarObj.get('barBaz').get('key'), 'value'); + equal(foobarObj.get('barBaz').get('otherKey'), undefined); + } else { + fail('barBaz should be set'); + } + done(); + }); }); - }); }); it('select nested keys 2 level (issue #1567)', function(done) { @@ -3443,35 +3839,50 @@ describe('Parse.Query testing', () => { Bazoo.set('some', 'thing'); Bazoo.set('otherSome', 'value'); - Bazoo.save().then(() => { - BarBaz.set('key', 'value'); - BarBaz.set('otherKey', 'value'); - BarBaz.set('bazoo', Bazoo); - return BarBaz.save(); - }).then(() => { - Foobar.set('foo', 'bar'); - Foobar.set('fizz', 'buzz'); - Foobar.set('barBaz', BarBaz); - return Foobar.save(); - }).then(function(savedFoobar){ - const foobarQuery = new Parse.Query('Foobar'); - foobarQuery.include('barBaz'); - foobarQuery.include('barBaz.bazoo'); - foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']); - foobarQuery.get(savedFoobar.id).then(function(foobarObj) { - equal(foobarObj.get('fizz'), 'buzz'); - equal(foobarObj.get('foo'), undefined); - if (foobarObj.has('barBaz')) { - equal(foobarObj.get('barBaz').get('key'), 'value'); - equal(foobarObj.get('barBaz').get('otherKey'), undefined); - equal(foobarObj.get('barBaz').get('bazoo').get('some'), 'thing'); - equal(foobarObj.get('barBaz').get('bazoo').get('otherSome'), undefined); - } else { - fail('barBaz should be set'); - } - done(); + Bazoo.save() + .then(() => { + BarBaz.set('key', 'value'); + BarBaz.set('otherKey', 'value'); + BarBaz.set('bazoo', Bazoo); + return BarBaz.save(); + }) + .then(() => { + Foobar.set('foo', 'bar'); + Foobar.set('fizz', 'buzz'); + Foobar.set('barBaz', BarBaz); + return Foobar.save(); + }) + .then(function(savedFoobar) { + const foobarQuery = new Parse.Query('Foobar'); + foobarQuery.include('barBaz'); + foobarQuery.include('barBaz.bazoo'); + foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']); + foobarQuery.get(savedFoobar.id).then(function(foobarObj) { + equal(foobarObj.get('fizz'), 'buzz'); + equal(foobarObj.get('foo'), undefined); + if (foobarObj.has('barBaz')) { + equal(foobarObj.get('barBaz').get('key'), 'value'); + equal(foobarObj.get('barBaz').get('otherKey'), undefined); + equal( + foobarObj + .get('barBaz') + .get('bazoo') + .get('some'), + 'thing' + ); + equal( + foobarObj + .get('barBaz') + .get('bazoo') + .get('otherSome'), + undefined + ); + } else { + fail('barBaz should be set'); + } + done(); + }); }); - }); }); it('include with *', async () => { @@ -3484,9 +3895,9 @@ describe('Parse.Query testing', () => { body: { where: { objectId: parent.id }, include: '*', - } + }, }); - const resp = await rp.get(Parse.serverURL + "/classes/Container", options); + const resp = await rp.get(Parse.serverURL + '/classes/Container', options); const result = resp.results[0]; equal(result.child1.foo, 'bar'); equal(result.child2.foo, 'baz'); @@ -3506,9 +3917,9 @@ describe('Parse.Query testing', () => { body: { where: { objectId: parent.id }, include: 'child2,*', - } + }, }); - const resp = await rp.get(Parse.serverURL + "/classes/Container", options); + const resp = await rp.get(Parse.serverURL + '/classes/Container', options); const result = resp.results[0]; equal(result.child1.foo, 'bar'); equal(result.child2.foo, 'baz'); @@ -3518,32 +3929,34 @@ describe('Parse.Query testing', () => { equal(result.child3.name, 'mo'); }); - it('includeAll', (done) => { + it('includeAll', done => { const child1 = new TestObject({ foo: 'bar', name: 'ac' }); const child2 = new TestObject({ foo: 'baz', name: 'flo' }); const child3 = new TestObject({ foo: 'bad', name: 'mo' }); const parent = new Container({ child1, child2, child3 }); - Parse.Object.saveAll([parent, child1, child2, child3]).then(() => { - const options = Object.assign({}, masterKeyOptions, { - body: { - where: { objectId: parent.id }, - includeAll: true, - } + Parse.Object.saveAll([parent, child1, child2, child3]) + .then(() => { + const options = Object.assign({}, masterKeyOptions, { + body: { + where: { objectId: parent.id }, + includeAll: true, + }, + }); + return rp.get(Parse.serverURL + '/classes/Container', options); + }) + .then(resp => { + const result = resp.results[0]; + equal(result.child1.foo, 'bar'); + equal(result.child2.foo, 'baz'); + equal(result.child3.foo, 'bad'); + equal(result.child1.name, 'ac'); + equal(result.child2.name, 'flo'); + equal(result.child3.name, 'mo'); + done(); }); - return rp.get(Parse.serverURL + "/classes/Container", options); - }).then((resp) => { - const result = resp.results[0]; - equal(result.child1.foo, 'bar'); - equal(result.child2.foo, 'baz'); - equal(result.child3.foo, 'bad'); - equal(result.child1.name, 'ac'); - equal(result.child2.name, 'flo'); - equal(result.child3.name, 'mo'); - done(); - }); }); - it('select nested keys 2 level includeAll', (done) => { + it('select nested keys 2 level includeAll', done => { const Foobar = new Parse.Object('Foobar'); const BarBaz = new Parse.Object('Barbaz'); const Bazoo = new Parse.Object('Bazoo'); @@ -3551,40 +3964,45 @@ describe('Parse.Query testing', () => { Bazoo.set('some', 'thing'); Bazoo.set('otherSome', 'value'); - Bazoo.save().then(() => { - BarBaz.set('key', 'value'); - BarBaz.set('otherKey', 'value'); - BarBaz.set('bazoo', Bazoo); - return BarBaz.save(); - }).then(() => { - Tang.set('clan', 'wu'); - return Tang.save(); - }).then(() => { - Foobar.set('foo', 'bar'); - Foobar.set('fizz', 'buzz'); - Foobar.set('barBaz', BarBaz); - Foobar.set('group', Tang); - return Foobar.save(); - }).then((savedFoobar) => { - const options = Object.assign({}, masterKeyOptions, { - body: { - where: { objectId: savedFoobar.id }, - includeAll: true, - keys: 'fizz,barBaz.key,barBaz.bazoo.some', - } + Bazoo.save() + .then(() => { + BarBaz.set('key', 'value'); + BarBaz.set('otherKey', 'value'); + BarBaz.set('bazoo', Bazoo); + return BarBaz.save(); + }) + .then(() => { + Tang.set('clan', 'wu'); + return Tang.save(); + }) + .then(() => { + Foobar.set('foo', 'bar'); + Foobar.set('fizz', 'buzz'); + Foobar.set('barBaz', BarBaz); + Foobar.set('group', Tang); + return Foobar.save(); + }) + .then(savedFoobar => { + const options = Object.assign({}, masterKeyOptions, { + body: { + where: { objectId: savedFoobar.id }, + includeAll: true, + keys: 'fizz,barBaz.key,barBaz.bazoo.some', + }, + }); + return rp.get(Parse.serverURL + '/classes/Foobar', options); + }) + .then(resp => { + const result = resp.results[0]; + equal(result.group.clan, 'wu'); + equal(result.foo, undefined); + equal(result.fizz, 'buzz'); + equal(result.barBaz.key, 'value'); + equal(result.barBaz.otherKey, undefined); + equal(result.barBaz.bazoo.some, 'thing'); + equal(result.barBaz.bazoo.otherSome, undefined); + done(); }); - return rp.get(Parse.serverURL + "/classes/Foobar", options); - }).then((resp) => { - const result = resp.results[0]; - equal(result.group.clan, 'wu'); - equal(result.foo, undefined); - equal(result.fizz, 'buzz'); - equal(result.barBaz.key, 'value'); - equal(result.barBaz.otherKey, undefined); - equal(result.barBaz.bazoo.some, 'thing'); - equal(result.barBaz.bazoo.otherSome, undefined); - done(); - }) }); it('select nested keys 2 level without include (issue #3185)', function(done) { @@ -3594,97 +4012,119 @@ describe('Parse.Query testing', () => { Bazoo.set('some', 'thing'); Bazoo.set('otherSome', 'value'); - Bazoo.save().then(() => { - BarBaz.set('key', 'value'); - BarBaz.set('otherKey', 'value'); - BarBaz.set('bazoo', Bazoo); - return BarBaz.save(); - }).then(() => { - Foobar.set('foo', 'bar'); - Foobar.set('fizz', 'buzz'); - Foobar.set('barBaz', BarBaz); - return Foobar.save(); - }).then(function(savedFoobar){ - const foobarQuery = new Parse.Query('Foobar'); - foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']); - return foobarQuery.get(savedFoobar.id); - }).then((foobarObj) => { - equal(foobarObj.get('fizz'), 'buzz'); - equal(foobarObj.get('foo'), undefined); - if (foobarObj.has('barBaz')) { - equal(foobarObj.get('barBaz').get('key'), 'value'); - equal(foobarObj.get('barBaz').get('otherKey'), undefined); - if (foobarObj.get('barBaz').has('bazoo')) { - equal(foobarObj.get('barBaz').get('bazoo').get('some'), 'thing'); - equal(foobarObj.get('barBaz').get('bazoo').get('otherSome'), undefined); + Bazoo.save() + .then(() => { + BarBaz.set('key', 'value'); + BarBaz.set('otherKey', 'value'); + BarBaz.set('bazoo', Bazoo); + return BarBaz.save(); + }) + .then(() => { + Foobar.set('foo', 'bar'); + Foobar.set('fizz', 'buzz'); + Foobar.set('barBaz', BarBaz); + return Foobar.save(); + }) + .then(function(savedFoobar) { + const foobarQuery = new Parse.Query('Foobar'); + foobarQuery.select(['fizz', 'barBaz.key', 'barBaz.bazoo.some']); + return foobarQuery.get(savedFoobar.id); + }) + .then(foobarObj => { + equal(foobarObj.get('fizz'), 'buzz'); + equal(foobarObj.get('foo'), undefined); + if (foobarObj.has('barBaz')) { + equal(foobarObj.get('barBaz').get('key'), 'value'); + equal(foobarObj.get('barBaz').get('otherKey'), undefined); + if (foobarObj.get('barBaz').has('bazoo')) { + equal( + foobarObj + .get('barBaz') + .get('bazoo') + .get('some'), + 'thing' + ); + equal( + foobarObj + .get('barBaz') + .get('bazoo') + .get('otherSome'), + undefined + ); + } else { + fail('bazoo should be set'); + } } else { - fail('bazoo should be set'); + fail('barBaz should be set'); } - } else { - fail('barBaz should be set'); - } - done(); - }) + done(); + }); }); it('properly handles nested ors', function(done) { const objects = []; - while(objects.length != 4) { + while (objects.length != 4) { const obj = new Parse.Object('Object'); obj.set('x', objects.length); - objects.push(obj) + objects.push(obj); } - Parse.Object.saveAll(objects).then(() => { - const q0 = new Parse.Query('Object'); - q0.equalTo('x', 0); - const q1 = new Parse.Query('Object'); - q1.equalTo('x', 1); - const q2 = new Parse.Query('Object'); - q2.equalTo('x', 2); - const or01 = Parse.Query.or(q0,q1); - return Parse.Query.or(or01, q2).find(); - }).then((results) => { - expect(results.length).toBe(3); - done(); - }).catch((error) => { - fail('should not fail'); - jfail(error); - done(); - }) + Parse.Object.saveAll(objects) + .then(() => { + const q0 = new Parse.Query('Object'); + q0.equalTo('x', 0); + const q1 = new Parse.Query('Object'); + q1.equalTo('x', 1); + const q2 = new Parse.Query('Object'); + q2.equalTo('x', 2); + const or01 = Parse.Query.or(q0, q1); + return Parse.Query.or(or01, q2).find(); + }) + .then(results => { + expect(results.length).toBe(3); + done(); + }) + .catch(error => { + fail('should not fail'); + jfail(error); + done(); + }); }); it('should not depend on parameter order #3169', function(done) { - const score1 = new Parse.Object('Score', {scoreId: '1'}); - const score2 = new Parse.Object('Score', {scoreId: '2'}); - const game1 = new Parse.Object('Game', {gameId: '1'}); - const game2 = new Parse.Object('Game', {gameId: '2'}); - Parse.Object.saveAll([score1, score2, game1, game2]).then(() => { - game1.set('score', [score1]); - game2.set('score', [score2]); - return Parse.Object.saveAll([game1, game2]); - }).then(() => { - const where = { - score: { - objectId: score1.id, - className: 'Score', - __type: 'Pointer', - } - } - return require('request-promise').post({ - url: Parse.serverURL + "/classes/Game", - json: { where, "_method": "GET" }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }).then((response) => { - expect(response.results.length).toBe(1); - done(); - }, done.fail); + const score1 = new Parse.Object('Score', { scoreId: '1' }); + const score2 = new Parse.Object('Score', { scoreId: '2' }); + const game1 = new Parse.Object('Game', { gameId: '1' }); + const game2 = new Parse.Object('Game', { gameId: '2' }); + Parse.Object.saveAll([score1, score2, game1, game2]) + .then(() => { + game1.set('score', [score1]); + game2.set('score', [score2]); + return Parse.Object.saveAll([game1, game2]); + }) + .then(() => { + const where = { + score: { + objectId: score1.id, + className: 'Score', + __type: 'Pointer', + }, + }; + return require('request-promise').post({ + url: Parse.serverURL + '/classes/Game', + json: { where, _method: 'GET' }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + }, + }); + }) + .then(response => { + expect(response.results.length).toBe(1); + done(); + }, done.fail); }); - it('should not interfere with has when using select on field with undefined value #3999', (done) => { + it('should not interfere with has when using select on field with undefined value #3999', done => { const obj1 = new Parse.Object('TestObject'); const obj2 = new Parse.Object('OtherObject'); obj2.set('otherField', 1); @@ -3692,17 +4132,24 @@ describe('Parse.Query testing', () => { obj1.set('shouldBe', true); const obj3 = new Parse.Object('TestObject'); obj3.set('shouldBe', false); - Parse.Object.saveAll([obj1, obj3]).then(() => { - const query = new Parse.Query('TestObject'); - query.include('testPointerField'); - query.select(['testPointerField', 'testPointerField.otherField', 'shouldBe']); - return query.find(); - }).then(results => { - results.forEach(result => { - equal(result.has('testPointerField'), result.get('shouldBe')); - }); - done(); - }).catch(done.fail); + Parse.Object.saveAll([obj1, obj3]) + .then(() => { + const query = new Parse.Query('TestObject'); + query.include('testPointerField'); + query.select([ + 'testPointerField', + 'testPointerField.otherField', + 'shouldBe', + ]); + return query.find(); + }) + .then(results => { + results.forEach(result => { + equal(result.has('testPointerField'), result.get('shouldBe')); + }); + done(); + }) + .catch(done.fail); }); it_only_db('mongo')('should handle relative times correctly', function(done) { @@ -3722,7 +4169,7 @@ describe('Parse.Query testing', () => { q.greaterThan('ttl', { $relativeTime: 'in 1 day' }); return q.find({ useMasterKey: true }); }) - .then((results) => { + .then(results => { expect(results.length).toBe(1); }) .then(() => { @@ -3730,7 +4177,7 @@ describe('Parse.Query testing', () => { q.greaterThan('ttl', { $relativeTime: '1 day ago' }); return q.find({ useMasterKey: true }); }) - .then((results) => { + .then(results => { expect(results.length).toBe(1); }) .then(() => { @@ -3738,7 +4185,7 @@ describe('Parse.Query testing', () => { q.lessThan('ttl', { $relativeTime: '5 days ago' }); return q.find({ useMasterKey: true }); }) - .then((results) => { + .then(results => { expect(results.length).toBe(0); }) .then(() => { @@ -3746,7 +4193,7 @@ describe('Parse.Query testing', () => { q.greaterThan('ttl', { $relativeTime: '3 days ago' }); return q.find({ useMasterKey: true }); }) - .then((results) => { + .then(results => { expect(results.length).toBe(2); }) .then(() => { @@ -3754,7 +4201,7 @@ describe('Parse.Query testing', () => { q.greaterThan('ttl', { $relativeTime: 'now' }); return q.find({ useMasterKey: true }); }) - .then((results) => { + .then(results => { expect(results.length).toBe(1); }) .then(() => { @@ -3763,7 +4210,7 @@ describe('Parse.Query testing', () => { q.lessThan('ttl', { $relativeTime: 'in 1 day' }); return q.find({ useMasterKey: true }); }) - .then((results) => { + .then(results => { expect(results.length).toBe(0); }) .then(() => { @@ -3771,7 +4218,7 @@ describe('Parse.Query testing', () => { q.greaterThan('ttl', { $relativeTime: '1 year 3 weeks ago' }); return q.find({ useMasterKey: true }); }) - .then((results) => { + .then(results => { expect(results.length).toBe(2); }) .then(done, done.fail); @@ -3785,32 +4232,37 @@ describe('Parse.Query testing', () => { const q = new Parse.Query('MyCustomObject'); q.greaterThan('ttl', { $relativeTime: '-12 bananas ago' }); - obj1.save({ useMasterKey: true }) + obj1 + .save({ useMasterKey: true }) .then(() => q.find({ useMasterKey: true })) .then(done.fail, done); }); - it_only_db('mongo')('should error when using $relativeTime on non-Date field', function(done) { - const obj1 = new Parse.Object('MyCustomObject', { - name: 'obj1', - nonDateField: 'abcd', - ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now - }); + it_only_db('mongo')( + 'should error when using $relativeTime on non-Date field', + function(done) { + const obj1 = new Parse.Object('MyCustomObject', { + name: 'obj1', + nonDateField: 'abcd', + ttl: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now + }); - const q = new Parse.Query('MyCustomObject'); - q.greaterThan('nonDateField', { $relativeTime: '1 day ago' }); - obj1.save({ useMasterKey: true }) - .then(() => q.find({ useMasterKey: true })) - .then(done.fail, done); - }); + const q = new Parse.Query('MyCustomObject'); + q.greaterThan('nonDateField', { $relativeTime: '1 day ago' }); + obj1 + .save({ useMasterKey: true }) + .then(() => q.find({ useMasterKey: true })) + .then(done.fail, done); + } + ); it('should match complex structure with dot notation when using matchesKeyInQuery', function(done) { const group1 = new Parse.Object('Group', { - name: 'Group #1' + name: 'Group #1', }); const group2 = new Parse.Object('Group', { - name: 'Group #2' + name: 'Group #2', }); Parse.Object.saveAll([group1, group2]) @@ -3818,13 +4270,13 @@ describe('Parse.Query testing', () => { const role1 = new Parse.Object('Role', { name: 'Role #1', type: 'x', - belongsTo: group1 + belongsTo: group1, }); const role2 = new Parse.Object('Role', { name: 'Role #2', type: 'y', - belongsTo: group1 + belongsTo: group1, }); return Parse.Object.saveAll([role1, role2]); @@ -3834,23 +4286,27 @@ describe('Parse.Query testing', () => { rolesOfTypeX.equalTo('type', 'x'); const groupsWithRoleX = new Parse.Query('Group'); - groupsWithRoleX.matchesKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX); + groupsWithRoleX.matchesKeyInQuery( + 'objectId', + 'belongsTo.objectId', + rolesOfTypeX + ); groupsWithRoleX.find().then(function(results) { equal(results.length, 1); equal(results[0].get('name'), group1.get('name')); done(); }); - }) + }); }); it('should match complex structure with dot notation when using doesNotMatchKeyInQuery', function(done) { const group1 = new Parse.Object('Group', { - name: 'Group #1' + name: 'Group #1', }); const group2 = new Parse.Object('Group', { - name: 'Group #2' + name: 'Group #2', }); Parse.Object.saveAll([group1, group2]) @@ -3858,13 +4314,13 @@ describe('Parse.Query testing', () => { const role1 = new Parse.Object('Role', { name: 'Role #1', type: 'x', - belongsTo: group1 + belongsTo: group1, }); const role2 = new Parse.Object('Role', { name: 'Role #2', type: 'y', - belongsTo: group1 + belongsTo: group1, }); return Parse.Object.saveAll([role1, role2]); @@ -3874,69 +4330,70 @@ describe('Parse.Query testing', () => { rolesOfTypeX.equalTo('type', 'x'); const groupsWithRoleX = new Parse.Query('Group'); - groupsWithRoleX.doesNotMatchKeyInQuery('objectId', 'belongsTo.objectId', rolesOfTypeX); + groupsWithRoleX.doesNotMatchKeyInQuery( + 'objectId', + 'belongsTo.objectId', + rolesOfTypeX + ); groupsWithRoleX.find().then(function(results) { equal(results.length, 1); equal(results[0].get('name'), group2.get('name')); done(); }); - }) + }); }); - it('withJSON supports geoWithin.centerSphere', (done) => { + it('withJSON supports geoWithin.centerSphere', done => { const inbound = new Parse.GeoPoint(1.5, 1.5); const onbound = new Parse.GeoPoint(10, 10); const outbound = new Parse.GeoPoint(20, 20); - const obj1 = new Parse.Object('TestObject', {location: inbound}); - const obj2 = new Parse.Object('TestObject', {location: onbound}); - const obj3 = new Parse.Object('TestObject', {location: outbound}); + const obj1 = new Parse.Object('TestObject', { location: inbound }); + const obj2 = new Parse.Object('TestObject', { location: onbound }); + const obj3 = new Parse.Object('TestObject', { location: outbound }); const center = new Parse.GeoPoint(0, 0); const distanceInKilometers = 1569 + 1; // 1569km is the approximate distance between {0, 0} and {10, 10}. - Parse.Object.saveAll([obj1, obj2, obj3]).then(() => { - const q = new Parse.Query(TestObject); - const jsonQ = q.toJSON(); - jsonQ.where.location = { - '$geoWithin': { - '$centerSphere': [ - center, - distanceInKilometers / 6371.0 - ] - } - }; - q.withJSON(jsonQ); - return q.find(); - }).then(results => { - equal(results.length, 2); - const q = new Parse.Query(TestObject); - const jsonQ = q.toJSON(); - jsonQ.where.location = { - '$geoWithin': { - '$centerSphere': [ - [0, 0], - distanceInKilometers / 6371.0 - ] - } - }; - q.withJSON(jsonQ); - return q.find(); - }).then(results => { - equal(results.length, 2); - done(); - }).catch(error => { - fail(error); - done(); - }); + Parse.Object.saveAll([obj1, obj2, obj3]) + .then(() => { + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + $geoWithin: { + $centerSphere: [center, distanceInKilometers / 6371.0], + }, + }; + q.withJSON(jsonQ); + return q.find(); + }) + .then(results => { + equal(results.length, 2); + const q = new Parse.Query(TestObject); + const jsonQ = q.toJSON(); + jsonQ.where.location = { + $geoWithin: { + $centerSphere: [[0, 0], distanceInKilometers / 6371.0], + }, + }; + q.withJSON(jsonQ); + return q.find(); + }) + .then(results => { + equal(results.length, 2); + done(); + }) + .catch(error => { + fail(error); + done(); + }); }); - it('withJSON with geoWithin.centerSphere fails without parameters', (done) => { + it('withJSON with geoWithin.centerSphere fails without parameters', done => { const q = new Parse.Query(TestObject); const jsonQ = q.toJSON(); jsonQ.where.location = { - '$geoWithin': { - '$centerSphere': [ - ] - } + $geoWithin: { + $centerSphere: [], + }, }; q.withJSON(jsonQ); q.find() @@ -3945,16 +4402,13 @@ describe('Parse.Query testing', () => { .finally(done); }); - it('withJSON with geoWithin.centerSphere fails with invalid distance', (done) => { + it('withJSON with geoWithin.centerSphere fails with invalid distance', done => { const q = new Parse.Query(TestObject); const jsonQ = q.toJSON(); jsonQ.where.location = { - '$geoWithin': { - '$centerSphere': [ - [0, 0], - 'invalid_distance' - ] - } + $geoWithin: { + $centerSphere: [[0, 0], 'invalid_distance'], + }, }; q.withJSON(jsonQ); q.find() @@ -3963,33 +4417,31 @@ describe('Parse.Query testing', () => { .finally(() => done()); }); - it('withJSON with geoWithin.centerSphere fails with invalid coordinate', (done) => { + it('withJSON with geoWithin.centerSphere fails with invalid coordinate', done => { const q = new Parse.Query(TestObject); const jsonQ = q.toJSON(); jsonQ.where.location = { - '$geoWithin': { - '$centerSphere': [ - [-190,-190], - 1 - ] - } + $geoWithin: { + $centerSphere: [[-190, -190], 1], + }, }; q.withJSON(jsonQ); - q.find().then(done.fail).catch(done); + q.find() + .then(done.fail) + .catch(done); }); - it('withJSON with geoWithin.centerSphere fails with invalid geo point', (done) => { + it('withJSON with geoWithin.centerSphere fails with invalid geo point', done => { const q = new Parse.Query(TestObject); const jsonQ = q.toJSON(); jsonQ.where.location = { - '$geoWithin': { - '$centerSphere': [ - {'longitude': 0, 'dummytude': 0}, - 1 - ] - } + $geoWithin: { + $centerSphere: [{ longitude: 0, dummytude: 0 }, 1], + }, }; q.withJSON(jsonQ); - q.find().then(done.fail).catch(done); + q.find() + .then(done.fail) + .catch(done); }); }); diff --git a/spec/ParseRelation.spec.js b/spec/ParseRelation.spec.js index 4cd6716099..bd6771458c 100644 --- a/spec/ParseRelation.spec.js +++ b/spec/ParseRelation.spec.js @@ -2,212 +2,222 @@ // This is a port of the test suite: // hungry/js/test/parse_relation_test.js -const ChildObject = Parse.Object.extend({className: "ChildObject"}); -const ParentObject = Parse.Object.extend({className: "ParentObject"}); +const ChildObject = Parse.Object.extend({ className: 'ChildObject' }); +const ParentObject = Parse.Object.extend({ className: 'ParentObject' }); describe('Parse.Relation testing', () => { - it("simple add and remove relation", (done) => { + it('simple add and remove relation', done => { const child = new ChildObject(); - child.set("x", 2); + child.set('x', 2); const parent = new ParentObject(); - parent.set("x", 4); - const relation = parent.relation("child"); - - child.save().then(() => { - relation.add(child); - return parent.save(); - }, (e) => { - fail(e); - }).then(() => { - return relation.query().find(); - }).then((list) => { - equal(list.length, 1, - "Should have gotten one element back"); - equal(list[0].id, child.id, - "Should have gotten the right value"); - ok(!parent.dirty("child"), - "The relation should not be dirty"); - - relation.remove(child); - return parent.save(); - }).then(() => { - return relation.query().find(); - }).then((list) => { - equal(list.length, 0, - "Delete should have worked"); - ok(!parent.dirty("child"), - "The relation should not be dirty"); - done(); - }); + parent.set('x', 4); + const relation = parent.relation('child'); + + child + .save() + .then( + () => { + relation.add(child); + return parent.save(); + }, + e => { + fail(e); + } + ) + .then(() => { + return relation.query().find(); + }) + .then(list => { + equal(list.length, 1, 'Should have gotten one element back'); + equal(list[0].id, child.id, 'Should have gotten the right value'); + ok(!parent.dirty('child'), 'The relation should not be dirty'); + + relation.remove(child); + return parent.save(); + }) + .then(() => { + return relation.query().find(); + }) + .then(list => { + equal(list.length, 0, 'Delete should have worked'); + ok(!parent.dirty('child'), 'The relation should not be dirty'); + done(); + }); }); - it("query relation without schema", async () => { - const ChildObject = Parse.Object.extend("ChildObject"); + it('query relation without schema', async () => { + const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x:i})); + childObjects.push(new ChildObject({ x: i })); } await Parse.Object.saveAll(childObjects); - const ParentObject = Parse.Object.extend("ParentObject"); + const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); - parent.set("x", 4); - let relation = parent.relation("child"); + parent.set('x', 4); + let relation = parent.relation('child'); relation.add(childObjects[0]); await parent.save(); const parentAgain = new ParentObject(); parentAgain.id = parent.id; - relation = parentAgain.relation("child"); + relation = parentAgain.relation('child'); const list = await relation.query().find(); - equal(list.length, 1, - "Should have gotten one element back"); - equal(list[0].id, childObjects[0].id, - "Should have gotten the right value"); + equal(list.length, 1, 'Should have gotten one element back'); + equal(list[0].id, childObjects[0].id, 'Should have gotten the right value'); }); - it("relations are constructed right from query", async () => { - - const ChildObject = Parse.Object.extend("ChildObject"); + it('relations are constructed right from query', async () => { + const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x: i})); + childObjects.push(new ChildObject({ x: i })); } await Parse.Object.saveAll(childObjects); - const ParentObject = Parse.Object.extend("ParentObject"); + const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); - parent.set("x", 4); - const relation = parent.relation("child"); + parent.set('x', 4); + const relation = parent.relation('child'); relation.add(childObjects[0]); await parent.save(); const query = new Parse.Query(ParentObject); const object = await query.get(parent.id); - const relationAgain = object.relation("child"); + const relationAgain = object.relation('child'); const list = await relationAgain.query().find(); - equal(list.length, 1, - "Should have gotten one element back"); - equal(list[0].id, childObjects[0].id, - "Should have gotten the right value"); - ok(!parent.dirty("child"), - "The relation should not be dirty"); + equal(list.length, 1, 'Should have gotten one element back'); + equal(list[0].id, childObjects[0].id, 'Should have gotten the right value'); + ok(!parent.dirty('child'), 'The relation should not be dirty'); }); - it("compound add and remove relation", (done) => { - const ChildObject = Parse.Object.extend("ChildObject"); + it('compound add and remove relation', done => { + const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x: i})); + childObjects.push(new ChildObject({ x: i })); } let parent; let relation; - Parse.Object.saveAll(childObjects).then(function() { - const ParentObject = Parse.Object.extend('ParentObject'); - parent = new ParentObject(); - parent.set('x', 4); - relation = parent.relation('child'); - relation.add(childObjects[0]); - relation.add(childObjects[1]); - relation.remove(childObjects[0]); - relation.add(childObjects[2]); - return parent.save(); - }).then(function() { - return relation.query().find(); - }).then(function(list) { - equal(list.length, 2, 'Should have gotten two elements back'); - ok(!parent.dirty('child'), 'The relation should not be dirty'); - relation.remove(childObjects[1]); - relation.remove(childObjects[2]); - relation.add(childObjects[1]); - relation.add(childObjects[0]); - return parent.save(); - }).then(function() { - return relation.query().find(); - }).then(function(list) { - equal(list.length, 2, 'Deletes and then adds should have worked'); - ok(!parent.dirty('child'), 'The relation should not be dirty'); - done(); - }, function(err) { - ok(false, err.message); - done(); - }); + Parse.Object.saveAll(childObjects) + .then(function() { + const ParentObject = Parse.Object.extend('ParentObject'); + parent = new ParentObject(); + parent.set('x', 4); + relation = parent.relation('child'); + relation.add(childObjects[0]); + relation.add(childObjects[1]); + relation.remove(childObjects[0]); + relation.add(childObjects[2]); + return parent.save(); + }) + .then(function() { + return relation.query().find(); + }) + .then(function(list) { + equal(list.length, 2, 'Should have gotten two elements back'); + ok(!parent.dirty('child'), 'The relation should not be dirty'); + relation.remove(childObjects[1]); + relation.remove(childObjects[2]); + relation.add(childObjects[1]); + relation.add(childObjects[0]); + return parent.save(); + }) + .then(function() { + return relation.query().find(); + }) + .then( + function(list) { + equal(list.length, 2, 'Deletes and then adds should have worked'); + ok(!parent.dirty('child'), 'The relation should not be dirty'); + done(); + }, + function(err) { + ok(false, err.message); + done(); + } + ); }); - it("related at ordering optimizations", (done) => { - const ChildObject = Parse.Object.extend("ChildObject"); + it('related at ordering optimizations', done => { + const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x: i})); + childObjects.push(new ChildObject({ x: i })); } let parent; let relation; - Parse.Object.saveAll(childObjects).then(function() { - const ParentObject = Parse.Object.extend('ParentObject'); - parent = new ParentObject(); - parent.set('x', 4); - relation = parent.relation('child'); - relation.add(childObjects); - return parent.save(); - }).then(function() { - const query = relation.query(); - query.descending('createdAt'); - query.skip(1); - query.limit(3); - return query.find(); - }).then(function(list) { - expect(list.length).toBe(3); - }).then(done, done.fail); + Parse.Object.saveAll(childObjects) + .then(function() { + const ParentObject = Parse.Object.extend('ParentObject'); + parent = new ParentObject(); + parent.set('x', 4); + relation = parent.relation('child'); + relation.add(childObjects); + return parent.save(); + }) + .then(function() { + const query = relation.query(); + query.descending('createdAt'); + query.skip(1); + query.limit(3); + return query.find(); + }) + .then(function(list) { + expect(list.length).toBe(3); + }) + .then(done, done.fail); }); - it_exclude_dbs(['postgres'])("queries with relations", async () => { - - const ChildObject = Parse.Object.extend("ChildObject"); + it_exclude_dbs(['postgres'])('queries with relations', async () => { + const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x: i})); + childObjects.push(new ChildObject({ x: i })); } await Parse.Object.saveAll(childObjects); - const ParentObject = Parse.Object.extend("ParentObject"); + const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); - parent.set("x", 4); - const relation = parent.relation("child"); + parent.set('x', 4); + const relation = parent.relation('child'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.add(childObjects[2]); await parent.save(); const query = relation.query(); - query.equalTo("x", 2); + query.equalTo('x', 2); const list = await query.find(); - equal(list.length, 1, - "There should only be one element"); - ok(list[0] instanceof ChildObject, - "Should be of type ChildObject"); - equal(list[0].id, childObjects[2].id, - "We should have gotten back the right result"); + equal(list.length, 1, 'There should only be one element'); + ok(list[0] instanceof ChildObject, 'Should be of type ChildObject'); + equal( + list[0].id, + childObjects[2].id, + 'We should have gotten back the right result' + ); }); - it("queries on relation fields", async () => { - const ChildObject = Parse.Object.extend("ChildObject"); + it('queries on relation fields', async () => { + const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x: i})); + childObjects.push(new ChildObject({ x: i })); } await Parse.Object.saveAll(childObjects); - const ParentObject = Parse.Object.extend("ParentObject"); + const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); - parent.set("x", 4); - const relation = parent.relation("child"); + parent.set('x', 4); + const relation = parent.relation('child'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.add(childObjects[2]); const parent2 = new ParentObject(); - parent2.set("x", 3); - const relation2 = parent2.relation("child"); + parent2.set('x', 3); + const relation2 = parent2.relation('child'); relation2.add(childObjects[4]); relation2.add(childObjects[5]); relation2.add(childObjects[6]); @@ -219,120 +229,133 @@ describe('Parse.Relation testing', () => { const objects = []; objects.push(childObjects[4]); objects.push(childObjects[9]); - const list = await query.containedIn("child", objects).find(); - equal(list.length, 1, "There should be only one result"); - equal(list[0].id, parent2.id, - "Should have gotten back the right result"); + const list = await query.containedIn('child', objects).find(); + equal(list.length, 1, 'There should be only one result'); + equal(list[0].id, parent2.id, 'Should have gotten back the right result'); }); - it("queries on relation fields with multiple containedIn (regression test for #1271)", (done) => { - const ChildObject = Parse.Object.extend("ChildObject"); + it('queries on relation fields with multiple containedIn (regression test for #1271)', done => { + const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x: i})); + childObjects.push(new ChildObject({ x: i })); } - Parse.Object.saveAll(childObjects).then(() => { - const ParentObject = Parse.Object.extend("ParentObject"); - const parent = new ParentObject(); - parent.set("x", 4); - const parent1Children = parent.relation("child"); - parent1Children.add(childObjects[0]); - parent1Children.add(childObjects[1]); - parent1Children.add(childObjects[2]); - const parent2 = new ParentObject(); - parent2.set("x", 3); - const parent2Children = parent2.relation("child"); - parent2Children.add(childObjects[4]); - parent2Children.add(childObjects[5]); - parent2Children.add(childObjects[6]); - - const parent2OtherChildren = parent2.relation("otherChild"); - parent2OtherChildren.add(childObjects[0]); - parent2OtherChildren.add(childObjects[1]); - parent2OtherChildren.add(childObjects[2]); - - return Parse.Object.saveAll([parent, parent2]); - }).then(() => { - const 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(() => { - const 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(); - }); + Parse.Object.saveAll(childObjects) + .then(() => { + const ParentObject = Parse.Object.extend('ParentObject'); + const parent = new ParentObject(); + parent.set('x', 4); + const parent1Children = parent.relation('child'); + parent1Children.add(childObjects[0]); + parent1Children.add(childObjects[1]); + parent1Children.add(childObjects[2]); + const parent2 = new ParentObject(); + parent2.set('x', 3); + const parent2Children = parent2.relation('child'); + parent2Children.add(childObjects[4]); + parent2Children.add(childObjects[5]); + parent2Children.add(childObjects[6]); + + const parent2OtherChildren = parent2.relation('otherChild'); + parent2OtherChildren.add(childObjects[0]); + parent2OtherChildren.add(childObjects[1]); + parent2OtherChildren.add(childObjects[2]); + + return Parse.Object.saveAll([parent, parent2]); + }) + .then(() => { + const 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(() => { + const 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_exclude_dbs(['postgres'])("query on pointer and relation fields with equal", (done) => { - const ChildObject = Parse.Object.extend("ChildObject"); - const childObjects = []; - for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x: i})); - } - - Parse.Object.saveAll(childObjects).then(() => { - const ParentObject = Parse.Object.extend("ParentObject"); - const parent = new ParentObject(); - parent.set("x", 4); - const relation = parent.relation("toChilds"); - relation.add(childObjects[0]); - relation.add(childObjects[1]); - relation.add(childObjects[2]); - - const parent2 = new ParentObject(); - parent2.set("x", 3); - parent2.set("toChild", childObjects[2]); - - const parents = []; - parents.push(parent); - parents.push(parent2); - parents.push(new ParentObject()); - - return Parse.Object.saveAll(parents).then(() => { - const query = new Parse.Query(ParentObject); - query.equalTo("objectId", parent.id); - query.equalTo("toChilds", childObjects[2]); + it_exclude_dbs(['postgres'])( + 'query on pointer and relation fields with equal', + done => { + const ChildObject = Parse.Object.extend('ChildObject'); + const childObjects = []; + for (let i = 0; i < 10; i++) { + childObjects.push(new ChildObject({ x: i })); + } - return query.find().then((list) => { - equal(list.length, 1, "There should be 1 result"); + Parse.Object.saveAll(childObjects) + .then(() => { + const ParentObject = Parse.Object.extend('ParentObject'); + const parent = new ParentObject(); + parent.set('x', 4); + const relation = parent.relation('toChilds'); + relation.add(childObjects[0]); + relation.add(childObjects[1]); + relation.add(childObjects[2]); + + const parent2 = new ParentObject(); + parent2.set('x', 3); + parent2.set('toChild', childObjects[2]); + + const parents = []; + parents.push(parent); + parents.push(parent2); + parents.push(new ParentObject()); + + return Parse.Object.saveAll(parents).then(() => { + const query = new Parse.Query(ParentObject); + query.equalTo('objectId', parent.id); + query.equalTo('toChilds', childObjects[2]); + + return query.find().then(list => { + equal(list.length, 1, 'There should be 1 result'); + done(); + }); + }); + }) + .catch(err => { + jfail(err); done(); }); - }); - }).catch(err => { - jfail(err); - done(); - }); - }); + } + ); - it("query on pointer and relation fields with equal bis", (done) => { - const ChildObject = Parse.Object.extend("ChildObject"); + it('query on pointer and relation fields with equal bis', done => { + const ChildObject = Parse.Object.extend('ChildObject'); const childObjects = []; for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x: i})); + childObjects.push(new ChildObject({ x: i })); } Parse.Object.saveAll(childObjects).then(() => { - const ParentObject = Parse.Object.extend("ParentObject"); + const ParentObject = Parse.Object.extend('ParentObject'); const parent = new ParentObject(); - parent.set("x", 4); - const relation = parent.relation("toChilds"); + parent.set('x', 4); + const relation = parent.relation('toChilds'); relation.add(childObjects[0]); relation.add(childObjects[1]); relation.add(childObjects[2]); const parent2 = new ParentObject(); - parent2.set("x", 3); - parent2.relation("toChilds").add(childObjects[2]); + parent2.set('x', 3); + parent2.relation('toChilds').add(childObjects[2]); const parents = []; parents.push(parent); @@ -341,152 +364,174 @@ describe('Parse.Relation testing', () => { return Parse.Object.saveAll(parents).then(() => { const query = new Parse.Query(ParentObject); - query.equalTo("objectId", parent2.id); + query.equalTo('objectId', parent2.id); // childObjects[2] is in 2 relations // before the fix, that woul yield 2 results - query.equalTo("toChilds", childObjects[2]); + query.equalTo('toChilds', childObjects[2]); - return query.find().then((list) => { - equal(list.length, 1, "There should be 1 result"); + return query.find().then(list => { + equal(list.length, 1, 'There should be 1 result'); done(); }); }); }); }); - it_exclude_dbs(['postgres'])("or queries on pointer and relation fields", (done) => { - const ChildObject = Parse.Object.extend("ChildObject"); - const childObjects = []; - for (let i = 0; i < 10; i++) { - childObjects.push(new ChildObject({x: i})); - } - - Parse.Object.saveAll(childObjects).then(() => { - const ParentObject = Parse.Object.extend("ParentObject"); - const parent = new ParentObject(); - parent.set("x", 4); - const relation = parent.relation("toChilds"); - relation.add(childObjects[0]); - relation.add(childObjects[1]); - relation.add(childObjects[2]); - - const parent2 = new ParentObject(); - parent2.set("x", 3); - parent2.set("toChild", childObjects[2]); - - const parents = []; - parents.push(parent); - parents.push(parent2); - parents.push(new ParentObject()); + it_exclude_dbs(['postgres'])( + 'or queries on pointer and relation fields', + done => { + const ChildObject = Parse.Object.extend('ChildObject'); + const childObjects = []; + for (let i = 0; i < 10; i++) { + childObjects.push(new ChildObject({ x: i })); + } - return Parse.Object.saveAll(parents).then(() => { - const query1 = new Parse.Query(ParentObject); - query1.containedIn("toChilds", [childObjects[2]]); - const query2 = new Parse.Query(ParentObject); - query2.equalTo("toChild", childObjects[2]); - const query = Parse.Query.or(query1, query2); - return query.find().then((list) => { - const objectIds = list.map(function(item){ - return item.id; + Parse.Object.saveAll(childObjects).then(() => { + const ParentObject = Parse.Object.extend('ParentObject'); + const parent = new ParentObject(); + parent.set('x', 4); + const relation = parent.relation('toChilds'); + relation.add(childObjects[0]); + relation.add(childObjects[1]); + relation.add(childObjects[2]); + + const parent2 = new ParentObject(); + parent2.set('x', 3); + parent2.set('toChild', childObjects[2]); + + const parents = []; + parents.push(parent); + parents.push(parent2); + parents.push(new ParentObject()); + + return Parse.Object.saveAll(parents).then(() => { + const query1 = new Parse.Query(ParentObject); + query1.containedIn('toChilds', [childObjects[2]]); + const query2 = new Parse.Query(ParentObject); + query2.equalTo('toChild', childObjects[2]); + const query = Parse.Query.or(query1, query2); + return query.find().then(list => { + const objectIds = list.map(function(item) { + return item.id; + }); + expect(objectIds.indexOf(parent.id)).not.toBe(-1); + expect(objectIds.indexOf(parent2.id)).not.toBe(-1); + equal(list.length, 2, 'There should be 2 results'); + done(); }); - expect(objectIds.indexOf(parent.id)).not.toBe(-1); - expect(objectIds.indexOf(parent2.id)).not.toBe(-1); - equal(list.length, 2, "There should be 2 results"); - done(); }); }); - }); - }); - + } + ); - it("Get query on relation using un-fetched parent object", (done) => { + it('Get query on relation using un-fetched parent object', done => { // Setup data model const Wheel = Parse.Object.extend('Wheel'); const Car = Parse.Object.extend('Car'); const origWheel = new Wheel(); - origWheel.save().then(function() { - const car = new Car(); - const relation = car.relation('wheels'); - relation.add(origWheel); - return car.save(); - }).then(function(car) { - // Test starts here. - // Create an un-fetched shell car object - const unfetchedCar = new Car(); - unfetchedCar.id = car.id; - const relation = unfetchedCar.relation('wheels'); - const query = relation.query(); - - // Parent object is un-fetched, so this will call /1/classes/Car instead - // of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }. - return query.get(origWheel.id); - }).then(function(wheel) { - // Make sure this is Wheel and not Car. - strictEqual(wheel.className, 'Wheel'); - strictEqual(wheel.id, origWheel.id); - }).then(function() { - done(); - },function(err) { - ok(false, 'unexpected error: ' + JSON.stringify(err)); - done(); - }); + origWheel + .save() + .then(function() { + const car = new Car(); + const relation = car.relation('wheels'); + relation.add(origWheel); + return car.save(); + }) + .then(function(car) { + // Test starts here. + // Create an un-fetched shell car object + const unfetchedCar = new Car(); + unfetchedCar.id = car.id; + const relation = unfetchedCar.relation('wheels'); + const query = relation.query(); + + // Parent object is un-fetched, so this will call /1/classes/Car instead + // of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }. + return query.get(origWheel.id); + }) + .then(function(wheel) { + // Make sure this is Wheel and not Car. + strictEqual(wheel.className, 'Wheel'); + strictEqual(wheel.id, origWheel.id); + }) + .then( + function() { + done(); + }, + function(err) { + ok(false, 'unexpected error: ' + JSON.stringify(err)); + done(); + } + ); }); - it("Find query on relation using un-fetched parent object", (done) => { + it('Find query on relation using un-fetched parent object', done => { // Setup data model const Wheel = Parse.Object.extend('Wheel'); const Car = Parse.Object.extend('Car'); const origWheel = new Wheel(); - origWheel.save().then(function() { - const car = new Car(); - const relation = car.relation('wheels'); - relation.add(origWheel); - return car.save(); - }).then(function(car) { - // Test starts here. - // Create an un-fetched shell car object - const unfetchedCar = new Car(); - unfetchedCar.id = car.id; - const relation = unfetchedCar.relation('wheels'); - const query = relation.query(); - - // Parent object is un-fetched, so this will call /1/classes/Car instead - // of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }. - return query.find(origWheel.id); - }).then(function(results) { - // Make sure this is Wheel and not Car. - const wheel = results[0]; - strictEqual(wheel.className, 'Wheel'); - strictEqual(wheel.id, origWheel.id); - }).then(function() { - done(); - },function(err) { - ok(false, 'unexpected error: ' + JSON.stringify(err)); - done(); - }); + origWheel + .save() + .then(function() { + const car = new Car(); + const relation = car.relation('wheels'); + relation.add(origWheel); + return car.save(); + }) + .then(function(car) { + // Test starts here. + // Create an un-fetched shell car object + const unfetchedCar = new Car(); + unfetchedCar.id = car.id; + const relation = unfetchedCar.relation('wheels'); + const query = relation.query(); + + // Parent object is un-fetched, so this will call /1/classes/Car instead + // of /1/classes/Wheel and pass { "redirectClassNameForKey":"wheels" }. + return query.find(origWheel.id); + }) + .then(function(results) { + // Make sure this is Wheel and not Car. + const wheel = results[0]; + strictEqual(wheel.className, 'Wheel'); + strictEqual(wheel.id, origWheel.id); + }) + .then( + function() { + done(); + }, + function(err) { + ok(false, 'unexpected error: ' + JSON.stringify(err)); + done(); + } + ); }); - it('Find objects with a related object using equalTo', (done) => { + it('Find objects with a related object using equalTo', done => { // Setup the objects const Card = Parse.Object.extend('Card'); const House = Parse.Object.extend('House'); const card = new Card(); - card.save().then(() => { - const house = new House(); - const relation = house.relation('cards'); - relation.add(card); - return house.save(); - }).then(() => { - const query = new Parse.Query('House'); - query.equalTo('cards', card); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(1); - done(); - }); + card + .save() + .then(() => { + const house = new House(); + const relation = house.relation('cards'); + relation.add(card); + return house.save(); + }) + .then(() => { + const query = new Parse.Query('House'); + query.equalTo('cards', card); + return query.find(); + }) + .then(results => { + expect(results.length).toEqual(1); + done(); + }); }); - it('should properly get related objects with unfetched queries', (done) => { + it('should properly get related objects with unfetched queries', done => { const objects = []; const owners = []; const allObjects = []; @@ -495,7 +540,7 @@ describe('Parse.Relation testing', () => { const object = new Parse.Object('AnObject'); object.set({ index: objects.length, - even: objects.length % 2 == 0 + even: objects.length % 2 == 0, }); objects.push(object); const owner = new Parse.Object('AnOwner'); @@ -506,138 +551,156 @@ describe('Parse.Relation testing', () => { const anotherOwner = new Parse.Object('AnotherOwner'); - return Parse.Object.saveAll(allObjects.concat([anotherOwner])).then(() => { - // put all the AnObject into the anotherOwner relationKey - anotherOwner.relation('relationKey').add(objects); - // Set each object[i] into owner[i]; - owners.forEach((owner,i) => { - owner.set('key', objects[i]); - }); - return Parse.Object.saveAll(owners.concat([anotherOwner])); - }).then(() => { - // Query on the relation of another owner - const object = new Parse.Object('AnotherOwner'); - object.id = anotherOwner.id; - const relationQuery = object.relation('relationKey').query(); - // Just get the even ones - relationQuery.equalTo('even', true); - // Make the query on anOwner - const query = new Parse.Query('AnOwner'); - // where key match the relation query. - query.matchesQuery('key', relationQuery); - query.include('key'); - return query.find(); - }).then((results) => { - expect(results.length).toBe(5); - results.forEach((result) => { - expect(result.get('key').get('even')).toBe(true); - }); - return Promise.resolve(); - }).then(() => { - // Query on the relation of another owner - const object = new Parse.Object('AnotherOwner'); - object.id = anotherOwner.id; - const relationQuery = object.relation('relationKey').query(); - // Just get the even ones - relationQuery.equalTo('even', true); - // Make the query on anOwner - const query = new Parse.Query('AnOwner'); - // where key match the relation query. - query.doesNotMatchQuery('key', relationQuery); - query.include('key'); - return query.find(); - }).then((results) => { - expect(results.length).toBe(5); - results.forEach((result) => { - expect(result.get('key').get('even')).toBe(false); - }); - done(); - }, (e) => { - fail(JSON.stringify(e)); - done(); - }) + return Parse.Object.saveAll(allObjects.concat([anotherOwner])) + .then(() => { + // put all the AnObject into the anotherOwner relationKey + anotherOwner.relation('relationKey').add(objects); + // Set each object[i] into owner[i]; + owners.forEach((owner, i) => { + owner.set('key', objects[i]); + }); + return Parse.Object.saveAll(owners.concat([anotherOwner])); + }) + .then(() => { + // Query on the relation of another owner + const object = new Parse.Object('AnotherOwner'); + object.id = anotherOwner.id; + const relationQuery = object.relation('relationKey').query(); + // Just get the even ones + relationQuery.equalTo('even', true); + // Make the query on anOwner + const query = new Parse.Query('AnOwner'); + // where key match the relation query. + query.matchesQuery('key', relationQuery); + query.include('key'); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(5); + results.forEach(result => { + expect(result.get('key').get('even')).toBe(true); + }); + return Promise.resolve(); + }) + .then(() => { + // Query on the relation of another owner + const object = new Parse.Object('AnotherOwner'); + object.id = anotherOwner.id; + const relationQuery = object.relation('relationKey').query(); + // Just get the even ones + relationQuery.equalTo('even', true); + // Make the query on anOwner + const query = new Parse.Query('AnOwner'); + // where key match the relation query. + query.doesNotMatchQuery('key', relationQuery); + query.include('key'); + return query.find(); + }) + .then( + results => { + expect(results.length).toBe(5); + results.forEach(result => { + expect(result.get('key').get('even')).toBe(false); + }); + done(); + }, + e => { + fail(JSON.stringify(e)); + done(); + } + ); }); - it("select query", function(done) { - const RestaurantObject = Parse.Object.extend("Restaurant"); - const PersonObject = Parse.Object.extend("Person"); + it('select query', function(done) { + const RestaurantObject = Parse.Object.extend('Restaurant'); + const PersonObject = Parse.Object.extend('Person'); const OwnerObject = Parse.Object.extend('Owner'); const restaurants = [ - new RestaurantObject({ ratings: 5, location: "Djibouti" }), - new RestaurantObject({ ratings: 3, location: "Ouagadougou" }), + new RestaurantObject({ ratings: 5, location: 'Djibouti' }), + new RestaurantObject({ ratings: 3, location: 'Ouagadougou' }), ]; const persons = [ - new PersonObject({ name: "Bob", hometown: "Djibouti" }), - new PersonObject({ name: "Tom", hometown: "Ouagadougou" }), - new PersonObject({ name: "Billy", hometown: "Detroit" }), + new PersonObject({ name: 'Bob', hometown: 'Djibouti' }), + new PersonObject({ name: 'Tom', hometown: 'Ouagadougou' }), + new PersonObject({ name: 'Billy', hometown: 'Detroit' }), ]; - const owner = new OwnerObject({name: 'Joe'}); + const owner = new OwnerObject({ name: 'Joe' }); const allObjects = [owner].concat(restaurants).concat(persons); expect(allObjects.length).toEqual(6); - Parse.Object.saveAll([owner].concat(restaurants).concat(persons)).then(function() { - owner.relation('restaurants').add(restaurants); - return owner.save() - }).then(async () => { - const unfetchedOwner = new OwnerObject(); - unfetchedOwner.id = owner.id; - const query = unfetchedOwner.relation('restaurants').query(); - query.greaterThan("ratings", 4); - const mainQuery = new Parse.Query(PersonObject); - mainQuery.matchesKeyInQuery("hometown", "location", query); - const results = await mainQuery.find(); - equal(results.length, 1); - if (results.length > 0) { - equal(results[0].get('name'), 'Bob'); - } - done(); - }, (e) => { - fail(JSON.stringify(e)); - done(); - }); + Parse.Object.saveAll([owner].concat(restaurants).concat(persons)) + .then(function() { + owner.relation('restaurants').add(restaurants); + return owner.save(); + }) + .then( + async () => { + const unfetchedOwner = new OwnerObject(); + unfetchedOwner.id = owner.id; + const query = unfetchedOwner.relation('restaurants').query(); + query.greaterThan('ratings', 4); + const mainQuery = new Parse.Query(PersonObject); + mainQuery.matchesKeyInQuery('hometown', 'location', query); + const results = await mainQuery.find(); + equal(results.length, 1); + if (results.length > 0) { + equal(results[0].get('name'), 'Bob'); + } + done(); + }, + e => { + fail(JSON.stringify(e)); + done(); + } + ); }); - it("dontSelect query", function(done) { - const RestaurantObject = Parse.Object.extend("Restaurant"); - const PersonObject = Parse.Object.extend("Person"); + it('dontSelect query', function(done) { + const RestaurantObject = Parse.Object.extend('Restaurant'); + const PersonObject = Parse.Object.extend('Person'); const OwnerObject = Parse.Object.extend('Owner'); const restaurants = [ - new RestaurantObject({ ratings: 5, location: "Djibouti" }), - new RestaurantObject({ ratings: 3, location: "Ouagadougou" }), + new RestaurantObject({ ratings: 5, location: 'Djibouti' }), + new RestaurantObject({ ratings: 3, location: 'Ouagadougou' }), ]; const persons = [ - new PersonObject({ name: "Bob", hometown: "Djibouti" }), - new PersonObject({ name: "Tom", hometown: "Ouagadougou" }), - new PersonObject({ name: "Billy", hometown: "Detroit" }), + new PersonObject({ name: 'Bob', hometown: 'Djibouti' }), + new PersonObject({ name: 'Tom', hometown: 'Ouagadougou' }), + new PersonObject({ name: 'Billy', hometown: 'Detroit' }), ]; - const owner = new OwnerObject({name: 'Joe'}); + const owner = new OwnerObject({ name: 'Joe' }); const allObjects = [owner].concat(restaurants).concat(persons); expect(allObjects.length).toEqual(6); - Parse.Object.saveAll([owner].concat(restaurants).concat(persons)).then(function() { - owner.relation('restaurants').add(restaurants); - return owner.save() - }).then(async () => { - const unfetchedOwner = new OwnerObject(); - unfetchedOwner.id = owner.id; - const query = unfetchedOwner.relation('restaurants').query(); - query.greaterThan("ratings", 4); - const mainQuery = new Parse.Query(PersonObject); - mainQuery.doesNotMatchKeyInQuery("hometown", "location", query); - mainQuery.ascending('name'); - const results = await mainQuery.find() - equal(results.length, 2); - if (results.length > 0) { - equal(results[0].get('name'), 'Billy'); - equal(results[1].get('name'), 'Tom'); - } - done(); - }, (e) => { - fail(JSON.stringify(e)); - done(); - }); + Parse.Object.saveAll([owner].concat(restaurants).concat(persons)) + .then(function() { + owner.relation('restaurants').add(restaurants); + return owner.save(); + }) + .then( + async () => { + const unfetchedOwner = new OwnerObject(); + unfetchedOwner.id = owner.id; + const query = unfetchedOwner.relation('restaurants').query(); + query.greaterThan('ratings', 4); + const mainQuery = new Parse.Query(PersonObject); + mainQuery.doesNotMatchKeyInQuery('hometown', 'location', query); + mainQuery.ascending('name'); + const results = await mainQuery.find(); + equal(results.length, 2); + if (results.length > 0) { + equal(results[0].get('name'), 'Billy'); + equal(results[1].get('name'), 'Tom'); + } + done(); + }, + e => { + fail(JSON.stringify(e)); + done(); + } + ); }); it('relations are not bidirectional (regression test for #871)', done => { - const PersonObject = Parse.Object.extend("Person"); + const PersonObject = Parse.Object.extend('Person'); const p1 = new PersonObject(); const p2 = new PersonObject(); Parse.Object.saveAll([p1, p2]).then(results => { @@ -659,122 +722,153 @@ describe('Parse.Relation testing', () => { done(); }); }); - }) + }); }); }); it('can query roles in Cloud Code (regession test #1489)', done => { - Parse.Cloud.define('isAdmin', (request) => { + Parse.Cloud.define('isAdmin', request => { const query = new Parse.Query(Parse.Role); query.equalTo('name', 'admin'); - return query.first({ useMasterKey: true }) - .then(role => { + return query.first({ useMasterKey: true }).then( + role => { const relation = new Parse.Relation(role, 'users'); const admins = relation.query(); admins.equalTo('username', request.user.get('username')); - admins.first({ useMasterKey: true }) - .then(user => { + admins.first({ useMasterKey: true }).then( + user => { if (user) { done(); } else { fail('Should have found admin user, found nothing instead'); done(); } - }, () => { + }, + () => { fail('User not admin'); done(); - }) - }, error => { + } + ); + }, + error => { fail('Should have found admin user, errored instead'); fail(error); done(); - }); + } + ); }); const adminUser = new Parse.User(); adminUser.set('username', 'name'); adminUser.set('password', 'pass'); - adminUser.signUp() - .then(adminUser => { + adminUser.signUp().then( + adminUser => { const adminACL = new Parse.ACL(); adminACL.setPublicReadAccess(true); // Create admin role const adminRole = new Parse.Role('admin', adminACL); adminRole.getUsers().add(adminUser); - adminRole.save() - .then(() => { + adminRole.save().then( + () => { Parse.Cloud.run('isAdmin'); - }, error => { + }, + error => { fail('failed to save role'); fail(error); - done() - }); - }, error => { + done(); + } + ); + }, + error => { fail('failed to sign up'); fail(error); done(); - }); + } + ); }); it('can be saved without error', done => { const obj1 = new Parse.Object('PPAP'); - obj1.save() - .then(() => { + obj1.save().then( + () => { const newRelation = obj1.relation('aRelation'); newRelation.add(obj1); - obj1.save().then(() => { - const relation = obj1.get('aRelation'); - obj1.set('aRelation', relation); - obj1.save().then(() => { - done(); - }, error => { - fail('failed to save ParseRelation object'); + obj1.save().then( + () => { + const relation = obj1.get('aRelation'); + obj1.set('aRelation', relation); + obj1.save().then( + () => { + done(); + }, + error => { + fail('failed to save ParseRelation object'); + fail(error); + done(); + } + ); + }, + error => { + fail('failed to create relation field'); fail(error); done(); - }); - }, error => { - fail('failed to create relation field'); - fail(error); - done(); - }); - }, error => { + } + ); + }, + error => { fail('failed to save obj'); fail(error); done(); - }); + } + ); }); - it('ensures beforeFind on relation doesnt side effect', (done) => { + it('ensures beforeFind on relation doesnt side effect', done => { const parent = new Parse.Object('Parent'); const child = new Parse.Object('Child'); - child.save().then(() => { - parent.relation('children').add(child); - return parent.save(); - }).then(() => { - // We need to use a new reference otherwise the JS SDK remembers the className for a relation - // After saves or finds - const otherParent = new Parse.Object('Parent'); - otherParent.id = parent.id; - return otherParent.relation('children').query().find(); - }).then((children) => { - // Without an after find all is good, all results have been redirected with proper className - children.forEach((child) => expect(child.className).toBe('Child')); - // Setup the afterFind - Parse.Cloud.afterFind('Child', (req) => { - return Promise.resolve(req.objects.map((child) => { - child.set('afterFound', true); - return child; - })); - }); - const otherParent = new Parse.Object('Parent'); - otherParent.id = parent.id; - return otherParent.relation('children').query().find(); - }).then((children) => { - children.forEach((child) => { - expect(child.className).toBe('Child'); - expect(child.get('afterFound')).toBe(true); - }); - }).then(done).catch(done.fail); + child + .save() + .then(() => { + parent.relation('children').add(child); + return parent.save(); + }) + .then(() => { + // We need to use a new reference otherwise the JS SDK remembers the className for a relation + // After saves or finds + const otherParent = new Parse.Object('Parent'); + otherParent.id = parent.id; + return otherParent + .relation('children') + .query() + .find(); + }) + .then(children => { + // Without an after find all is good, all results have been redirected with proper className + children.forEach(child => expect(child.className).toBe('Child')); + // Setup the afterFind + Parse.Cloud.afterFind('Child', req => { + return Promise.resolve( + req.objects.map(child => { + child.set('afterFound', true); + return child; + }) + ); + }); + const otherParent = new Parse.Object('Parent'); + otherParent.id = parent.id; + return otherParent + .relation('children') + .query() + .find(); + }) + .then(children => { + children.forEach(child => { + expect(child.className).toBe('Child'); + expect(child.get('afterFound')).toBe(true); + }); + }) + .then(done) + .catch(done.fail); }); }); diff --git a/spec/ParseRole.spec.js b/spec/ParseRole.spec.js index 283f1cafaa..47af03457e 100644 --- a/spec/ParseRole.spec.js +++ b/spec/ParseRole.spec.js @@ -1,67 +1,77 @@ -"use strict"; +'use strict'; // 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. -const RestQuery = require("../lib/RestQuery"); -const Auth = require("../lib/Auth").Auth; -const Config = require("../lib/Config"); +const RestQuery = require('../lib/RestQuery'); +const Auth = require('../lib/Auth').Auth; +const Config = require('../lib/Config'); describe('Parse Role testing', () => { it('Do a bunch of basic role testing', done => { let user; let role; - createTestUser().then((x) => { - user = x; - const acl = new Parse.ACL(); - acl.setPublicReadAccess(true); - acl.setPublicWriteAccess(false); - role = new Parse.Object('_Role'); - role.set('name', 'Foos'); - role.setACL(acl); - const users = role.relation('users'); - users.add(user); - return role.save({}, { useMasterKey: true }); - }).then(() => { - const query = new Parse.Query('_Role'); - return query.find({ useMasterKey: true }); - }).then((x) => { - expect(x.length).toEqual(1); - const relation = x[0].relation('users').query(); - return relation.first({ useMasterKey: true }); - }).then((x) => { - expect(x.id).toEqual(user.id); - // Here we've got a valid role and a user assigned. - // Lets create an object only the role can read/write and test - // the different scenarios. - const obj = new Parse.Object('TestObject'); - const acl = new Parse.ACL(); - acl.setPublicReadAccess(false); - acl.setPublicWriteAccess(false); - acl.setRoleReadAccess('Foos', true); - acl.setRoleWriteAccess('Foos', true); - obj.setACL(acl); - return obj.save(); - }).then(() => { - const query = new Parse.Query('TestObject'); - return query.find({ sessionToken: user.getSessionToken() }); - }).then((x) => { - expect(x.length).toEqual(1); - const objAgain = x[0]; - objAgain.set('foo', 'bar'); - // This should succeed: - return objAgain.save({}, {sessionToken: user.getSessionToken()}); - }).then((x) => { - x.set('foo', 'baz'); - // This should fail: - return x.save({},{sessionToken: ""}); - }).then(() => { - fail('Should not have been able to save.'); - }, (e) => { - expect(e.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); - + createTestUser() + .then(x => { + user = x; + const acl = new Parse.ACL(); + acl.setPublicReadAccess(true); + acl.setPublicWriteAccess(false); + role = new Parse.Object('_Role'); + role.set('name', 'Foos'); + role.setACL(acl); + const users = role.relation('users'); + users.add(user); + return role.save({}, { useMasterKey: true }); + }) + .then(() => { + const query = new Parse.Query('_Role'); + return query.find({ useMasterKey: true }); + }) + .then(x => { + expect(x.length).toEqual(1); + const relation = x[0].relation('users').query(); + return relation.first({ useMasterKey: true }); + }) + .then(x => { + expect(x.id).toEqual(user.id); + // Here we've got a valid role and a user assigned. + // Lets create an object only the role can read/write and test + // the different scenarios. + const obj = new Parse.Object('TestObject'); + const acl = new Parse.ACL(); + acl.setPublicReadAccess(false); + acl.setPublicWriteAccess(false); + acl.setRoleReadAccess('Foos', true); + acl.setRoleWriteAccess('Foos', true); + obj.setACL(acl); + return obj.save(); + }) + .then(() => { + const query = new Parse.Query('TestObject'); + return query.find({ sessionToken: user.getSessionToken() }); + }) + .then(x => { + expect(x.length).toEqual(1); + const objAgain = x[0]; + objAgain.set('foo', 'bar'); + // This should succeed: + return objAgain.save({}, { sessionToken: user.getSessionToken() }); + }) + .then(x => { + x.set('foo', 'baz'); + // This should fail: + return x.save({}, { sessionToken: '' }); + }) + .then( + () => { + fail('Should not have been able to save.'); + }, + e => { + expect(e.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + done(); + } + ); }); const createRole = function(name, sibling, user) { @@ -76,222 +86,272 @@ describe('Parse Role testing', () => { return role.save({}, { useMasterKey: true }); }; - it("should not recursively load the same role multiple times", (done) => { - const rootRole = "RootRole"; - const roleNames = ["FooRole", "BarRole", "BazRole"]; + it('should not recursively load the same role multiple times', done => { + const rootRole = 'RootRole'; + const roleNames = ['FooRole', 'BarRole', 'BazRole']; const allRoles = [rootRole].concat(roleNames); const roleObjs = {}; const createAllRoles = function(user) { const promises = allRoles.map(function(roleName) { - return createRole(roleName, null, user) - .then(function(roleObj) { - roleObjs[roleName] = roleObj; - return roleObj; - }); + return createRole(roleName, null, user).then(function(roleObj) { + roleObjs[roleName] = roleObj; + return roleObj; + }); }); return Promise.all(promises); }; - const restExecute = spyOn(RestQuery.prototype, "execute").and.callThrough(); - - let user, - auth, - getAllRolesSpy; - createTestUser().then((newUser) => { - user = newUser; - return createAllRoles(user); - }).then ((roles) => { - const 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]); - } - }); + const restExecute = spyOn(RestQuery.prototype, 'execute').and.callThrough(); - return Parse.Object.saveAll(roles, { useMasterKey: true }); - }).then(() => { - auth = new Auth({config: Config.get("test"), isMaster: true, user: user}); - getAllRolesSpy = spyOn(auth, "_getAllRolesNamesForRoleIds").and.callThrough(); + let user, auth, getAllRolesSpy; + createTestUser() + .then(newUser => { + user = newUser; + return createAllRoles(user); + }) + .then(roles => { + const 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: Config.get('test'), + isMaster: true, + user: user, + }); + getAllRolesSpy = spyOn( + auth, + '_getAllRolesNamesForRoleIds' + ).and.callThrough(); - return auth._loadRoles(); - }).then ((roles) => { - expect(roles.length).toEqual(4); + return auth._loadRoles(); + }) + .then(roles => { + expect(roles.length).toEqual(4); - allRoles.forEach(function(name) { - expect(roles.indexOf("role:" + name)).not.toBe(-1); - }); + 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(() => { - fail("should succeed"); - done(); - }); + // 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(() => { + fail('should succeed'); + done(); + }); }); function testLoadRoles(config, done) { - const rolesNames = ["FooRole", "BarRole", "BazRole"]; + const rolesNames = ['FooRole', 'BarRole', 'BazRole']; const roleIds = {}; - createTestUser().then((user) => { - // Put the user on the 1st role - return createRole(rolesNames[0], null, user).then((aRole) => { - roleIds[aRole.get("name")] = aRole.id; - // set the 1st role as a sibling of the second - // user will should have 2 role now - return createRole(rolesNames[1], aRole, null); - }).then((anotherRole) => { - roleIds[anotherRole.get("name")] = anotherRole.id; - // set this role as a sibling of the last - // the user should now have 3 roles - return createRole(rolesNames[2], anotherRole, null); - }).then((lastRole) => { - roleIds[lastRole.get("name")] = lastRole.id; - const auth = new Auth({ config, isMaster: true, user: user }); - return auth._loadRoles(); + createTestUser() + .then(user => { + // Put the user on the 1st role + return createRole(rolesNames[0], null, user) + .then(aRole => { + roleIds[aRole.get('name')] = aRole.id; + // set the 1st role as a sibling of the second + // user will should have 2 role now + return createRole(rolesNames[1], aRole, null); + }) + .then(anotherRole => { + roleIds[anotherRole.get('name')] = anotherRole.id; + // set this role as a sibling of the last + // the user should now have 3 roles + return createRole(rolesNames[2], anotherRole, null); + }) + .then(lastRole => { + roleIds[lastRole.get('name')] = lastRole.id; + const auth = new Auth({ config, isMaster: true, user: user }); + return auth._loadRoles(); + }); }) - }).then((roles) => { - expect(roles.length).toEqual(3); - rolesNames.forEach((name) => { - expect(roles.indexOf('role:' + name)).not.toBe(-1); - }); - done(); - }, function(){ - fail("should succeed") - done(); - }); + .then( + roles => { + expect(roles.length).toEqual(3); + rolesNames.forEach(name => { + expect(roles.indexOf('role:' + name)).not.toBe(-1); + }); + done(); + }, + function() { + fail('should succeed'); + done(); + } + ); } - it("should recursively load roles", (done) => { + it('should recursively load roles', done => { testLoadRoles(Config.get('test'), done); }); - it("should recursively load roles without config", (done) => { + it('should recursively load roles without config', done => { testLoadRoles(undefined, done); }); - it("_Role object should not save without name.", (done) => { + it('_Role object should not save without name.', done => { const role = new Parse.Role(); - role.save(null,{useMasterKey:true}) - .then(() => { - fail("_Role object should not save without name."); - }, (error) => { + role.save(null, { useMasterKey: true }).then( + () => { + fail('_Role object should not save without name.'); + }, + error => { expect(error.code).toEqual(111); - role.set('name','testRole'); - role.save(null,{useMasterKey:true}) - .then(()=>{ - fail("_Role object should not save without ACL."); - }, (error2) =>{ + role.set('name', 'testRole'); + role.save(null, { useMasterKey: true }).then( + () => { + fail('_Role object should not save without ACL.'); + }, + error2 => { expect(error2.code).toEqual(111); done(); - }); - }); + } + ); + } + ); }); - it("Different _Role objects cannot have the same name.", (done) => { - const roleName = "MyRole"; + it('Different _Role objects cannot have the same name.', done => { + const roleName = 'MyRole'; let aUser; - createTestUser().then((user) => { - aUser = user; - return createRole(roleName, null, aUser); - }).then((firstRole) => { - expect(firstRole.getName()).toEqual(roleName); - return createRole(roleName, null, aUser); - }).then(() => { - fail("_Role cannot have the same name as another role"); - done(); - }, (error) => { - expect(error.code).toEqual(137); - done(); - }); + createTestUser() + .then(user => { + aUser = user; + return createRole(roleName, null, aUser); + }) + .then(firstRole => { + expect(firstRole.getName()).toEqual(roleName); + return createRole(roleName, null, aUser); + }) + .then( + () => { + fail('_Role cannot have the same name as another role'); + done(); + }, + error => { + expect(error.code).toEqual(137); + done(); + } + ); }); - it("Should properly resolve roles", (done) => { - const admin = new Parse.Role("Admin", new Parse.ACL()); - const moderator = new Parse.Role("Moderator", new Parse.ACL()); - const superModerator = new Parse.Role("SuperModerator", new Parse.ACL()); + it('Should properly resolve roles', done => { + const admin = new Parse.Role('Admin', new Parse.ACL()); + const moderator = new Parse.Role('Moderator', new Parse.ACL()); + const superModerator = new Parse.Role('SuperModerator', new Parse.ACL()); const contentManager = new Parse.Role('ContentManager', new Parse.ACL()); - const superContentManager = new Parse.Role('SuperContentManager', new Parse.ACL()); - Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true}).then(() => { - contentManager.getRoles().add([moderator, superContentManager]); - moderator.getRoles().add([admin, superModerator]); - superContentManager.getRoles().add(superModerator); - return Parse.Object.saveAll([admin, moderator, contentManager, superModerator, superContentManager], {useMasterKey: true}); - }).then(() => { - const auth = new Auth({ config: Config.get("test"), isMaster: true }); - // For each role, fetch their sibling, what they inherit - // return with result and roleId for later comparison - const promises = [admin, moderator, contentManager, superModerator].map((role) => { - return auth._getAllRolesNamesForRoleIds([role.id]).then((result) => { - return Promise.resolve({ - id: role.id, - name: role.get('name'), - roleNames: result - }); - }) - }); + const superContentManager = new Parse.Role( + 'SuperContentManager', + new Parse.ACL() + ); + Parse.Object.saveAll( + [admin, moderator, contentManager, superModerator, superContentManager], + { useMasterKey: true } + ) + .then(() => { + contentManager.getRoles().add([moderator, superContentManager]); + moderator.getRoles().add([admin, superModerator]); + superContentManager.getRoles().add(superModerator); + return Parse.Object.saveAll( + [ + admin, + moderator, + contentManager, + superModerator, + superContentManager, + ], + { useMasterKey: true } + ); + }) + .then(() => { + const auth = new Auth({ config: Config.get('test'), isMaster: true }); + // For each role, fetch their sibling, what they inherit + // return with result and roleId for later comparison + const promises = [admin, moderator, contentManager, superModerator].map( + role => { + return auth._getAllRolesNamesForRoleIds([role.id]).then(result => { + return Promise.resolve({ + id: role.id, + name: role.get('name'), + roleNames: result, + }); + }); + } + ); - return Promise.all(promises); - }).then((results) => { - results.forEach((result) => { - const id = result.id; - const roleNames = result.roleNames; - if (id == admin.id) { - 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(roleNames.length).toBe(1); - expect(roleNames.indexOf("ContentManager")).toBe(0); - } else if (id == contentManager.id) { - expect(roleNames.length).toBe(0); - } else if (id == superModerator.id) { - 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); - } + return Promise.all(promises); + }) + .then(results => { + results.forEach(result => { + const id = result.id; + const roleNames = result.roleNames; + if (id == admin.id) { + 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(roleNames.length).toBe(1); + expect(roleNames.indexOf('ContentManager')).toBe(0); + } else if (id == contentManager.id) { + expect(roleNames.length).toBe(0); + } else if (id == superModerator.id) { + 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(); + }) + .catch(() => { + done(); }); - done(); - }).catch(() => { - done(); - }) - }); - it('can create role and query empty users', (done)=> { + it('can create role and query empty users', done => { const roleACL = new Parse.ACL(); roleACL.setPublicReadAccess(true); const role = new Parse.Role('subscribers', roleACL); - role.save({}, {useMasterKey : true}) - .then(()=>{ + role.save({}, { useMasterKey: true }).then( + () => { const query = role.relation('users').query(); - query.find({useMasterKey : true}) - .then(()=>{ + query.find({ useMasterKey: true }).then( + () => { done(); - }, ()=>{ + }, + () => { fail('should not have errors'); done(); - }); - }, () => { + } + ); + }, + () => { 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('should properly handle role permissions on objects', done => { let user, user2, user3; let role, role2, role3; let obj, obj2; @@ -300,80 +360,98 @@ describe('Parse Role testing', () => { prACL.setPublicReadAccess(true); let adminACL, superACL, customerACL; - createTestUser().then((x) => { - user = x; - user2 = new Parse.User(); - return user2.save({ username: 'user2', password: 'omgbbq' }); - }).then(() => { - user3 = new Parse.User(); - return user3.save({ username: 'user3', password: 'omgbbq' }); - }).then(() => { - role = new Parse.Role('Admin', prACL); - role.getUsers().add(user); - return role.save({}, { useMasterKey: true }); - }).then(() => { - adminACL = new Parse.ACL(); - adminACL.setRoleReadAccess("Admin", true); - adminACL.setRoleWriteAccess("Admin", true); - - role2 = new Parse.Role('Super', prACL); - role2.getUsers().add(user2); - return role2.save({}, { useMasterKey: true }); - }).then(() => { - superACL = new Parse.ACL(); - superACL.setRoleReadAccess("Super", true); - superACL.setRoleWriteAccess("Super", true); - - role.getRoles().add(role2); - return role.save({}, { useMasterKey: true }); - }).then(() => { - role3 = new Parse.Role('Customer', prACL); - role3.getUsers().add(user3); - role3.getRoles().add(role); - return role3.save({}, { useMasterKey: true }); - }).then(() => { - customerACL = new Parse.ACL(); - customerACL.setRoleReadAccess("Customer", true); - customerACL.setRoleWriteAccess("Customer", true); - - const query = new Parse.Query('_Role'); - return query.find({ useMasterKey: true }); - }).then((x) => { - expect(x.length).toEqual(3); - - obj = new Parse.Object('TestObjectRoles'); - obj.set('ACL', customerACL); - return obj.save(null, { useMasterKey: true }); - }).then(() => { - // Above, the Admin role was added to the Customer role. - // An object secured by the Customer ACL should be able to be edited by the Admin user. - obj.set('changedByAdmin', true); - return obj.save(null, { sessionToken: user.getSessionToken() }); - }).then(() => { - obj2 = new Parse.Object('TestObjectRoles'); - obj2.set('ACL', adminACL); - return obj2.save(null, { useMasterKey: true }); - }, () => { - fail('Admin user should have been able to save.'); - done(); - }).then(() => { - // An object secured by the Admin ACL should not be able to be edited by a Customer role user. - obj2.set('changedByCustomer', true); - return obj2.save(null, { sessionToken: user3.getSessionToken() }); - }).then(() => { - fail('Customer user should not have been able to save.'); - done(); - }, (e) => { - if (e) { - expect(e.code).toEqual(101); - } else { - fail('should return an error'); - } - done(); - }) + createTestUser() + .then(x => { + user = x; + user2 = new Parse.User(); + return user2.save({ username: 'user2', password: 'omgbbq' }); + }) + .then(() => { + user3 = new Parse.User(); + return user3.save({ username: 'user3', password: 'omgbbq' }); + }) + .then(() => { + role = new Parse.Role('Admin', prACL); + role.getUsers().add(user); + return role.save({}, { useMasterKey: true }); + }) + .then(() => { + adminACL = new Parse.ACL(); + adminACL.setRoleReadAccess('Admin', true); + adminACL.setRoleWriteAccess('Admin', true); + + role2 = new Parse.Role('Super', prACL); + role2.getUsers().add(user2); + return role2.save({}, { useMasterKey: true }); + }) + .then(() => { + superACL = new Parse.ACL(); + superACL.setRoleReadAccess('Super', true); + superACL.setRoleWriteAccess('Super', true); + + role.getRoles().add(role2); + return role.save({}, { useMasterKey: true }); + }) + .then(() => { + role3 = new Parse.Role('Customer', prACL); + role3.getUsers().add(user3); + role3.getRoles().add(role); + return role3.save({}, { useMasterKey: true }); + }) + .then(() => { + customerACL = new Parse.ACL(); + customerACL.setRoleReadAccess('Customer', true); + customerACL.setRoleWriteAccess('Customer', true); + + const query = new Parse.Query('_Role'); + return query.find({ useMasterKey: true }); + }) + .then(x => { + expect(x.length).toEqual(3); + + obj = new Parse.Object('TestObjectRoles'); + obj.set('ACL', customerACL); + return obj.save(null, { useMasterKey: true }); + }) + .then(() => { + // Above, the Admin role was added to the Customer role. + // An object secured by the Customer ACL should be able to be edited by the Admin user. + obj.set('changedByAdmin', true); + return obj.save(null, { sessionToken: user.getSessionToken() }); + }) + .then( + () => { + obj2 = new Parse.Object('TestObjectRoles'); + obj2.set('ACL', adminACL); + return obj2.save(null, { useMasterKey: true }); + }, + () => { + fail('Admin user should have been able to save.'); + done(); + } + ) + .then(() => { + // An object secured by the Admin ACL should not be able to be edited by a Customer role user. + obj2.set('changedByCustomer', true); + return obj2.save(null, { sessionToken: user3.getSessionToken() }); + }) + .then( + () => { + fail('Customer user should not have been able to save.'); + done(); + }, + e => { + if (e) { + expect(e.code).toEqual(101); + } else { + fail('should return an error'); + } + done(); + } + ); }); - it('should add multiple users to a role and remove users', (done) => { + it('should add multiple users to a role and remove users', done => { let user, user2, user3; let role; let obj; @@ -382,157 +460,161 @@ describe('Parse Role testing', () => { prACL.setPublicReadAccess(true); prACL.setPublicWriteAccess(true); - createTestUser().then((x) => { - user = x; - user2 = new Parse.User(); - return user2.save({ username: 'user2', password: 'omgbbq' }); - }).then(() => { - user3 = new Parse.User(); - return user3.save({ username: 'user3', password: 'omgbbq' }); - }).then(() => { - role = new Parse.Role('sharedRole', prACL); - const users = role.relation('users'); - users.add(user); - users.add(user2); - users.add(user3); - return role.save({}, { useMasterKey: true }); - }).then(() => { - // query for saved role and get 3 users - const query = new Parse.Query('_Role'); - query.equalTo('name', 'sharedRole'); - return query.find({ useMasterKey: true }); - }).then((role) => { - expect(role.length).toEqual(1); - const users = role[0].relation('users').query(); - return users.find({ useMasterKey: true }); - }).then((users) => { - expect(users.length).toEqual(3); - obj = new Parse.Object('TestObjectRoles'); - obj.set('ACL', prACL); - return obj.save(null, { useMasterKey: true }); - }).then(() => { - // Above, the Admin role was added to the Customer role. - // An object secured by the Customer ACL should be able to be edited by the Admin user. - obj.set('changedByUsers', true); - return obj.save(null, { sessionToken: user.getSessionToken() }); - }).then(() => { - // query for saved role and get 3 users - const query = new Parse.Query('_Role'); - query.equalTo('name', 'sharedRole'); - return query.find({ useMasterKey: true }); - }).then((role) => { - expect(role.length).toEqual(1); - const users = role[0].relation('users'); - users.remove(user); - users.remove(user3); - return role[0].save({}, { useMasterKey: true }); - }).then((role) =>{ - const users = role.relation('users').query(); - return users.find({ useMasterKey: true }); - }).then((users) => { - expect(users.length).toEqual(1); - expect(users[0].get('username')).toEqual('user2'); - done(); - }); + createTestUser() + .then(x => { + user = x; + user2 = new Parse.User(); + return user2.save({ username: 'user2', password: 'omgbbq' }); + }) + .then(() => { + user3 = new Parse.User(); + return user3.save({ username: 'user3', password: 'omgbbq' }); + }) + .then(() => { + role = new Parse.Role('sharedRole', prACL); + const users = role.relation('users'); + users.add(user); + users.add(user2); + users.add(user3); + return role.save({}, { useMasterKey: true }); + }) + .then(() => { + // query for saved role and get 3 users + const query = new Parse.Query('_Role'); + query.equalTo('name', 'sharedRole'); + return query.find({ useMasterKey: true }); + }) + .then(role => { + expect(role.length).toEqual(1); + const users = role[0].relation('users').query(); + return users.find({ useMasterKey: true }); + }) + .then(users => { + expect(users.length).toEqual(3); + obj = new Parse.Object('TestObjectRoles'); + obj.set('ACL', prACL); + return obj.save(null, { useMasterKey: true }); + }) + .then(() => { + // Above, the Admin role was added to the Customer role. + // An object secured by the Customer ACL should be able to be edited by the Admin user. + obj.set('changedByUsers', true); + return obj.save(null, { sessionToken: user.getSessionToken() }); + }) + .then(() => { + // query for saved role and get 3 users + const query = new Parse.Query('_Role'); + query.equalTo('name', 'sharedRole'); + return query.find({ useMasterKey: true }); + }) + .then(role => { + expect(role.length).toEqual(1); + const users = role[0].relation('users'); + users.remove(user); + users.remove(user3); + return role[0].save({}, { useMasterKey: true }); + }) + .then(role => { + const users = role.relation('users').query(); + return users.find({ useMasterKey: true }); + }) + .then(users => { + expect(users.length).toEqual(1); + expect(users[0].get('username')).toEqual('user2'); + done(); + }); }); - it('should be secure (#3835)', (done) => { + it('should be secure (#3835)', done => { const acl = new Parse.ACL(); acl.getPublicReadAccess(true); const role = new Parse.Role('admin', acl); - role.save().then(() => { - const user = new Parse.User(); - return user.signUp({username: 'hello', password: 'world'}); - }).then((user) => { - role.getUsers().add(user) - return role.save(); - }).then(done.fail, () => { - const query = role.getUsers().query(); - return query.find({useMasterKey: true}); - }).then((results) => { - expect(results.length).toBe(0); - done(); - }) + role + .save() + .then(() => { + const user = new Parse.User(); + return user.signUp({ username: 'hello', password: 'world' }); + }) + .then(user => { + role.getUsers().add(user); + return role.save(); + }) + .then(done.fail, () => { + const query = role.getUsers().query(); + return query.find({ useMasterKey: true }); + }) + .then(results => { + expect(results.length).toBe(0); + done(); + }) .catch(done.fail); }); - it('should match when matching in users relation', (done) => { + it('should match when matching in users relation', done => { const user = new Parse.User(); - user - .save({ username: 'admin', password: 'admin' }) - .then((user) => { - const aCL = new Parse.ACL(); - aCL.setPublicReadAccess(true); - aCL.setPublicWriteAccess(true); - const role = new Parse.Role('admin', aCL); - const users = role.relation('users'); - users.add(user); - role - .save({}, { useMasterKey: true }) - .then(() => { - const query = new Parse.Query(Parse.Role); - query.equalTo('name', 'admin'); - query.equalTo('users', user); - query.find().then(function (roles) { - expect(roles.length).toEqual(1); - done(); - }); - }); - }); - }); - - it('should not match any entry when not matching in users relation', (done) => { - const user = new Parse.User(); - user - .save({ username: 'admin', password: 'admin' }) - .then((user) => { - const aCL = new Parse.ACL(); - aCL.setPublicReadAccess(true); - aCL.setPublicWriteAccess(true); - const role = new Parse.Role('admin', aCL); - const users = role.relation('users'); - users.add(user); - role - .save({}, { useMasterKey: true }) - .then(() => { - const otherUser = new Parse.User(); - otherUser - .save({ username: 'otherUser', password: 'otherUser' }) - .then((otherUser) => { - const query = new Parse.Query(Parse.Role); - query.equalTo('name', 'admin'); - query.equalTo('users', otherUser); - query.find().then(function(roles) { - expect(roles.length).toEqual(0); - done(); - }); - }); - }); + user.save({ username: 'admin', password: 'admin' }).then(user => { + const aCL = new Parse.ACL(); + aCL.setPublicReadAccess(true); + aCL.setPublicWriteAccess(true); + const role = new Parse.Role('admin', aCL); + const users = role.relation('users'); + users.add(user); + role.save({}, { useMasterKey: true }).then(() => { + const query = new Parse.Query(Parse.Role); + query.equalTo('name', 'admin'); + query.equalTo('users', user); + query.find().then(function(roles) { + expect(roles.length).toEqual(1); + done(); + }); }); + }); }); - it('should not match any entry when searching for null in users relation', (done) => { + it('should not match any entry when not matching in users relation', done => { const user = new Parse.User(); - user - .save({ username: 'admin', password: 'admin' }) - .then((user) => { - const aCL = new Parse.ACL(); - aCL.setPublicReadAccess(true); - aCL.setPublicWriteAccess(true); - const role = new Parse.Role('admin', aCL); - const users = role.relation('users'); - users.add(user); - role - .save({}, { useMasterKey: true }) - .then(() => { + user.save({ username: 'admin', password: 'admin' }).then(user => { + const aCL = new Parse.ACL(); + aCL.setPublicReadAccess(true); + aCL.setPublicWriteAccess(true); + const role = new Parse.Role('admin', aCL); + const users = role.relation('users'); + users.add(user); + role.save({}, { useMasterKey: true }).then(() => { + const otherUser = new Parse.User(); + otherUser + .save({ username: 'otherUser', password: 'otherUser' }) + .then(otherUser => { const query = new Parse.Query(Parse.Role); query.equalTo('name', 'admin'); - query.equalTo('users', null); - query.find().then(function (roles) { + query.equalTo('users', otherUser); + query.find().then(function(roles) { expect(roles.length).toEqual(0); done(); }); }); }); + }); + }); + + it('should not match any entry when searching for null in users relation', done => { + const user = new Parse.User(); + user.save({ username: 'admin', password: 'admin' }).then(user => { + const aCL = new Parse.ACL(); + aCL.setPublicReadAccess(true); + aCL.setPublicWriteAccess(true); + const role = new Parse.Role('admin', aCL); + const users = role.relation('users'); + users.add(user); + role.save({}, { useMasterKey: true }).then(() => { + const query = new Parse.Query(Parse.Role); + query.equalTo('name', 'admin'); + query.equalTo('users', null); + query.find().then(function(roles) { + expect(roles.length).toEqual(0); + done(); + }); + }); + }); }); }); diff --git a/spec/ParseServer.spec.js b/spec/ParseServer.spec.js index 121ba4ae94..8ecd5a6534 100644 --- a/spec/ParseServer.spec.js +++ b/spec/ParseServer.spec.js @@ -1,50 +1,53 @@ 'use strict'; /* Tests for ParseServer.js */ const express = require('express'); -const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter').default; +const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter') + .default; +const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') + .default; const ParseServer = require('../lib/ParseServer').default; describe('Server Url Checks', () => { - let server; - beforeAll((done) => { + beforeAll(done => { const app = express(); - app.get('/health', function(req, res){ + app.get('/health', function(req, res) { res.json({ - status: 'ok' + status: 'ok', }); }); server = app.listen(13376, undefined, done); }); - afterAll((done) => { + afterAll(done => { server.close(done); }); - it('validate good server url', (done) => { + it('validate good server url', done => { Parse.serverURL = 'http://localhost:13376'; ParseServer.verifyServerUrl(function(result) { - if(!result) { + if (!result) { done.fail('Did not pass valid url'); } done(); }); }); - it('mark bad server url', (done) => { + it('mark bad server url', done => { Parse.serverURL = 'notavalidurl'; ParseServer.verifyServerUrl(function(result) { - if(result) { + if (result) { done.fail('Did not mark invalid url'); } done(); }); }); - it('handleShutdown, close connection', (done) => { - const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; - const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; + it('handleShutdown, close connection', done => { + const mongoURI = + 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; + const postgresURI = + 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; let databaseAdapter; if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { databaseAdapter = new PostgresStorageAdapter({ @@ -57,12 +60,14 @@ describe('Server Url Checks', () => { collectionPrefix: 'test_', }); } - const newConfiguration = Object.assign({}, defaultConfiguration, { databaseAdapter }); + const newConfiguration = Object.assign({}, defaultConfiguration, { + databaseAdapter, + }); const parseServer = ParseServer.start(newConfiguration, () => { parseServer.handleShutdown(); - parseServer.server.close((err) => { + parseServer.server.close(err => { if (err) { - done.fail('Close Server Error') + done.fail('Close Server Error'); } reconfigureServer({}).then(() => { done(); diff --git a/spec/ParseServerRESTController.spec.js b/spec/ParseServerRESTController.spec.js index 602f1d625c..6fa128ec4c 100644 --- a/spec/ParseServerRESTController.spec.js +++ b/spec/ParseServerRESTController.spec.js @@ -1,155 +1,217 @@ -const ParseServerRESTController = require('../lib/ParseServerRESTController').ParseServerRESTController; +const ParseServerRESTController = require('../lib/ParseServerRESTController') + .ParseServerRESTController; const ParseServer = require('../lib/ParseServer').default; const Parse = require('parse/node').Parse; let RESTController; describe('ParseServerRESTController', () => { - beforeEach(() => { - RESTController = ParseServerRESTController(Parse.applicationId, ParseServer.promiseRouter({appId: Parse.applicationId})); - }) + RESTController = ParseServerRESTController( + Parse.applicationId, + ParseServer.promiseRouter({ appId: Parse.applicationId }) + ); + }); - it('should handle a get request', (done) => { - RESTController.request("GET", "/classes/MyObject").then((res) => { - expect(res.results.length).toBe(0); - done(); - }, (err) => { - console.log(err); - jfail(err); - done(); - }); + it('should handle a get request', done => { + RESTController.request('GET', '/classes/MyObject').then( + res => { + expect(res.results.length).toBe(0); + done(); + }, + err => { + console.log(err); + jfail(err); + done(); + } + ); }); - it('should handle a get request with full serverURL mount path', (done) => { - RESTController.request("GET", "/1/classes/MyObject").then((res) => { - expect(res.results.length).toBe(0); - done(); - }, (err) => { - jfail(err); - done(); - }); + it('should handle a get request with full serverURL mount path', done => { + RESTController.request('GET', '/1/classes/MyObject').then( + res => { + expect(res.results.length).toBe(0); + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - it('should handle a POST batch', (done) => { - RESTController.request("POST", "batch", { + it('should handle a POST batch', done => { + RESTController.request('POST', 'batch', { requests: [ { method: 'GET', - path: '/classes/MyObject' + path: '/classes/MyObject', }, { method: 'POST', path: '/classes/MyObject', - body: {"key": "value"} + body: { key: 'value' }, }, { method: 'GET', - path: '/classes/MyObject' - } - ] - }).then((res) => { - expect(res.length).toBe(3); - done(); - }, (err) => { - jfail(err); - done(); - }); + path: '/classes/MyObject', + }, + ], + }).then( + res => { + expect(res.length).toBe(3); + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - it('should handle a POST request', (done) => { - RESTController.request("POST", "/classes/MyObject", {"key": "value"}).then(() => { - return RESTController.request("GET", "/classes/MyObject"); - }).then((res) => { - expect(res.results.length).toBe(1); - expect(res.results[0].key).toEqual("value"); - done(); - }).catch((err) => { - console.log(err); - jfail(err); - done(); - }); + it('should handle a POST request', done => { + RESTController.request('POST', '/classes/MyObject', { key: 'value' }) + .then(() => { + return RESTController.request('GET', '/classes/MyObject'); + }) + .then(res => { + expect(res.results.length).toBe(1); + expect(res.results[0].key).toEqual('value'); + done(); + }) + .catch(err => { + console.log(err); + jfail(err); + done(); + }); }); - it('ensures sessionTokens are properly handled', (done) => { + it('ensures sessionTokens are properly handled', done => { let userId; - Parse.User.signUp('user', 'pass').then((user) => { - userId = user.id; - const sessionToken = user.getSessionToken(); - return RESTController.request("GET", "/users/me", undefined, {sessionToken}); - }).then((res) => { - // Result is in JSON format - expect(res.objectId).toEqual(userId); - done(); - }).catch((err) => { - console.log(err); - jfail(err); - done(); - }); + Parse.User.signUp('user', 'pass') + .then(user => { + userId = user.id; + const sessionToken = user.getSessionToken(); + return RESTController.request('GET', '/users/me', undefined, { + sessionToken, + }); + }) + .then(res => { + // Result is in JSON format + expect(res.objectId).toEqual(userId); + done(); + }) + .catch(err => { + console.log(err); + jfail(err); + done(); + }); }); - it('ensures masterKey is properly handled', (done) => { + it('ensures masterKey is properly handled', done => { let userId; - Parse.User.signUp('user', 'pass').then((user) => { - userId = user.id; - return Parse.User.logOut().then(() => { - return RESTController.request("GET", "/classes/_User", undefined, {useMasterKey: true}); - }); - }).then((res) => { - expect(res.results.length).toBe(1); - expect(res.results[0].objectId).toEqual(userId); - done(); - }, (err) => { - jfail(err); - done(); - }); + Parse.User.signUp('user', 'pass') + .then(user => { + userId = user.id; + return Parse.User.logOut().then(() => { + return RESTController.request('GET', '/classes/_User', undefined, { + useMasterKey: true, + }); + }); + }) + .then( + res => { + expect(res.results.length).toBe(1); + expect(res.results[0].objectId).toEqual(userId); + done(); + }, + err => { + jfail(err); + done(); + } + ); }); - it('ensures no user is created when passing an empty username', (done) => { - RESTController.request("POST", "/classes/_User", {username: "", password: "world"}).then(() => { - jfail(new Error('Success callback should not be called when passing an empty username.')); - done(); - }, (err) => { - expect(err.code).toBe(Parse.Error.USERNAME_MISSING); - expect(err.message).toBe('bad or missing username'); - done(); - }); + it('ensures no user is created when passing an empty username', done => { + RESTController.request('POST', '/classes/_User', { + username: '', + password: 'world', + }).then( + () => { + jfail( + new Error( + 'Success callback should not be called when passing an empty username.' + ) + ); + done(); + }, + err => { + expect(err.code).toBe(Parse.Error.USERNAME_MISSING); + expect(err.message).toBe('bad or missing username'); + done(); + } + ); }); - it('ensures no user is created when passing an empty password', (done) => { - RESTController.request("POST", "/classes/_User", {username: "hello", password: ""}).then(() => { - jfail(new Error('Success callback should not be called when passing an empty password.')); - done(); - }, (err) => { - expect(err.code).toBe(Parse.Error.PASSWORD_MISSING); - expect(err.message).toBe('password is required'); - done(); - }); + it('ensures no user is created when passing an empty password', done => { + RESTController.request('POST', '/classes/_User', { + username: 'hello', + password: '', + }).then( + () => { + jfail( + new Error( + 'Success callback should not be called when passing an empty password.' + ) + ); + done(); + }, + err => { + expect(err.code).toBe(Parse.Error.PASSWORD_MISSING); + expect(err.message).toBe('password is required'); + done(); + } + ); }); - it('ensures no session token is created on creating users', (done) => { - RESTController.request("POST", "/classes/_User", {username: "hello", password: "world"}).then((user) => { - expect(user.sessionToken).toBeUndefined(); - const query = new Parse.Query('_Session'); - return query.find({useMasterKey: true}); - }).then(sessions => { - expect(sessions.length).toBe(0); - done(); - }, done.fail); + it('ensures no session token is created on creating users', done => { + RESTController.request('POST', '/classes/_User', { + username: 'hello', + password: 'world', + }) + .then(user => { + expect(user.sessionToken).toBeUndefined(); + const query = new Parse.Query('_Session'); + return query.find({ useMasterKey: true }); + }) + .then(sessions => { + expect(sessions.length).toBe(0); + done(); + }, done.fail); }); - it('ensures a session token is created when passing installationId != cloud', (done) => { - RESTController.request("POST", "/classes/_User", {username: "hello", password: "world"}, {installationId: 'my-installation'}).then((user) => { - expect(user.sessionToken).not.toBeUndefined(); - const query = new Parse.Query('_Session'); - return query.find({useMasterKey: true}); - }).then(sessions => { - expect(sessions.length).toBe(1); - expect(sessions[0].get('installationId')).toBe('my-installation'); - done(); - }, (err) => { - jfail(err); - done(); - }); + it('ensures a session token is created when passing installationId != cloud', done => { + RESTController.request( + 'POST', + '/classes/_User', + { username: 'hello', password: 'world' }, + { installationId: 'my-installation' } + ) + .then(user => { + expect(user.sessionToken).not.toBeUndefined(); + const query = new Parse.Query('_Session'); + return query.find({ useMasterKey: true }); + }) + .then( + sessions => { + expect(sessions.length).toBe(1); + expect(sessions[0].get('installationId')).toBe('my-installation'); + done(); + }, + err => { + jfail(err); + done(); + } + ); }); }); diff --git a/spec/ParseSession.spec.js b/spec/ParseSession.spec.js index c69710a307..084f141e08 100644 --- a/spec/ParseSession.spec.js +++ b/spec/ParseSession.spec.js @@ -2,125 +2,137 @@ // Tests behavior of Parse Sessions // -"use strict"; +'use strict'; function setupTestUsers() { const user1 = new Parse.User(); const user2 = new Parse.User(); const user3 = new Parse.User(); - user1.set("username", "testuser_1"); - user2.set("username", "testuser_2"); - user3.set("username", "testuser_3"); + user1.set('username', 'testuser_1'); + user2.set('username', 'testuser_2'); + user3.set('username', 'testuser_3'); - user1.set("password", "password"); - user2.set("password", "password"); - user3.set("password", "password"); + user1.set('password', 'password'); + user2.set('password', 'password'); + user3.set('password', 'password'); - return user1.signUp().then(() => { - return user2.signUp(); - }).then(() => { - return user3.signUp(); - }) + return user1 + .signUp() + .then(() => { + return user2.signUp(); + }) + .then(() => { + return user3.signUp(); + }); } describe('Parse.Session', () => { - // multiple sessions with masterKey + sessionToken - it('should retain original sessionTokens with masterKey & sessionToken set', (done) => { - setupTestUsers().then((user) => { - const query = new Parse.Query(Parse.Session); - return query.find({ - useMasterKey: true, - sessionToken: user.get('sessionToken') - }); - }).then((results) => { - const foundKeys = []; - expect(results.length).toBe(3); - for(const key in results) { - const sessionToken = results[key].get('sessionToken'); - if(foundKeys[sessionToken]) { - fail('Duplicate session token present in response'); - break; + it('should retain original sessionTokens with masterKey & sessionToken set', done => { + setupTestUsers() + .then(user => { + const query = new Parse.Query(Parse.Session); + return query.find({ + useMasterKey: true, + sessionToken: user.get('sessionToken'), + }); + }) + .then(results => { + const foundKeys = []; + expect(results.length).toBe(3); + for (const key in results) { + const sessionToken = results[key].get('sessionToken'); + if (foundKeys[sessionToken]) { + fail('Duplicate session token present in response'); + break; + } + foundKeys[sessionToken] = 1; } - foundKeys[sessionToken] = 1; - } - done(); - }).catch((err) => { - fail(err); - }); + done(); + }) + .catch(err => { + fail(err); + }); }); // single session returned, with just one sessionToken - it('should retain original sessionTokens with just sessionToken set', (done) => { + it('should retain original sessionTokens with just sessionToken set', done => { let knownSessionToken; - setupTestUsers().then((user) => { - knownSessionToken = user.get('sessionToken'); - const query = new Parse.Query(Parse.Session); - return query.find({ - sessionToken: knownSessionToken + setupTestUsers() + .then(user => { + knownSessionToken = user.get('sessionToken'); + const query = new Parse.Query(Parse.Session); + return query.find({ + sessionToken: knownSessionToken, + }); + }) + .then(results => { + expect(results.length).toBe(1); + const sessionToken = results[0].get('sessionToken'); + expect(sessionToken).toBe(knownSessionToken); + done(); + }) + .catch(err => { + fail(err); }); - }).then((results) => { - expect(results.length).toBe(1); - const sessionToken = results[0].get('sessionToken'); - expect(sessionToken).toBe(knownSessionToken); - done(); - }).catch((err) => { - fail(err); - }); }); // multiple users with masterKey + sessionToken - it('token on users should retain original sessionTokens with masterKey & sessionToken set', (done) => { - setupTestUsers().then((user) => { - const query = new Parse.Query(Parse.User); - return query.find({ - useMasterKey: true, - sessionToken: user.get('sessionToken') - }); - }).then((results) => { - const foundKeys = []; - expect(results.length).toBe(3); - for(const key in results) { - const sessionToken = results[key].get('sessionToken'); - if(foundKeys[sessionToken] && sessionToken !== undefined) { - fail('Duplicate session token present in response'); - break; + it('token on users should retain original sessionTokens with masterKey & sessionToken set', done => { + setupTestUsers() + .then(user => { + const query = new Parse.Query(Parse.User); + return query.find({ + useMasterKey: true, + sessionToken: user.get('sessionToken'), + }); + }) + .then(results => { + const foundKeys = []; + expect(results.length).toBe(3); + for (const key in results) { + const sessionToken = results[key].get('sessionToken'); + if (foundKeys[sessionToken] && sessionToken !== undefined) { + fail('Duplicate session token present in response'); + break; + } + foundKeys[sessionToken] = 1; } - foundKeys[sessionToken] = 1; - } - done(); - }).catch((err) => { - fail(err); - }); + done(); + }) + .catch(err => { + fail(err); + }); }); // multiple users with just sessionToken - it('token on users should retain original sessionTokens with just sessionToken set', (done) => { + it('token on users should retain original sessionTokens with just sessionToken set', done => { let knownSessionToken; - setupTestUsers().then((user) => { - knownSessionToken = user.get('sessionToken'); - const query = new Parse.Query(Parse.User); - return query.find({ - sessionToken: knownSessionToken - }); - }).then((results) => { - const foundKeys = []; - expect(results.length).toBe(3); - for(const key in results) { - const sessionToken = results[key].get('sessionToken'); - if(foundKeys[sessionToken] && sessionToken !== undefined) { - fail('Duplicate session token present in response'); - break; + setupTestUsers() + .then(user => { + knownSessionToken = user.get('sessionToken'); + const query = new Parse.Query(Parse.User); + return query.find({ + sessionToken: knownSessionToken, + }); + }) + .then(results => { + const foundKeys = []; + expect(results.length).toBe(3); + for (const key in results) { + const sessionToken = results[key].get('sessionToken'); + if (foundKeys[sessionToken] && sessionToken !== undefined) { + fail('Duplicate session token present in response'); + break; + } + foundKeys[sessionToken] = 1; } - foundKeys[sessionToken] = 1; - } - - done(); - }).catch((err) => { - fail(err); - }); + done(); + }) + .catch(err => { + fail(err); + }); }); - }); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 0ea3949ef6..389ec520e5 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -5,9 +5,10 @@ // Tests that involve revocable sessions. // Tests that involve sending password reset emails. -"use strict"; +'use strict'; -const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; +const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter') + .default; const request = require('request'); const passwordCrypto = require('../lib/password'); const Config = require('../lib/Config'); @@ -28,24 +29,24 @@ function verifyACL(user) { } describe('Parse.User testing', () => { - it("user sign up class method", async (done) => { - const user = await Parse.User.signUp("asdf", "zxcv"); + it('user sign up class method', async done => { + const user = await Parse.User.signUp('asdf', 'zxcv'); ok(user.getSessionToken()); done(); }); - it("user sign up instance method", async () => { + it('user sign up instance method', async () => { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); + user.setPassword('asdf'); + user.setUsername('zxcv'); await user.signUp(); ok(user.getSessionToken()); }); - it("user login wrong username", async (done) => { - await Parse.User.signUp("asdf", "zxcv"); + it('user login wrong username', async done => { + await Parse.User.signUp('asdf', 'zxcv'); try { - await Parse.User.logIn("non_existent_user", "asdf3"); + await Parse.User.logIn('non_existent_user', 'asdf3'); done.fail(); } catch (e) { expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); @@ -53,10 +54,10 @@ describe('Parse.User testing', () => { } }); - it("user login wrong password", async (done) => { - await Parse.User.signUp("asdf", "zxcv"); + it('user login wrong password', async done => { + await Parse.User.signUp('asdf', 'zxcv'); try { - await Parse.User.logIn("asdf", "asdfWrong"); + await Parse.User.logIn('asdf', 'asdfWrong'); done.fail(); } catch (e) { expect(e.code).toBe(Parse.Error.OBJECT_NOT_FOUND); @@ -64,7 +65,7 @@ describe('Parse.User testing', () => { } }); - it('user login with non-string username with REST API', async (done) => { + it('user login with non-string username with REST API', async done => { await Parse.User.signUp('asdf', 'zxcv'); rp.post({ url: 'http://localhost:8378/1/login', @@ -74,20 +75,24 @@ describe('Parse.User testing', () => { }, json: { _method: 'GET', - username: {'$regex':'^asd'}, + username: { $regex: '^asd' }, password: 'zxcv', - } - }).then((res) => { - fail(`no request should succeed: ${JSON.stringify(res)}`); - done(); - }).catch((err) => { - expect(err.statusCode).toBe(404); - expect(err.message).toMatch('{"code":101,"error":"Invalid username/password."}'); - done(); - }); + }, + }) + .then(res => { + fail(`no request should succeed: ${JSON.stringify(res)}`); + done(); + }) + .catch(err => { + expect(err.statusCode).toBe(404); + expect(err.message).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); + done(); + }); }); - it('user login with non-string username with REST API (again)', async (done) => { + it('user login with non-string username with REST API (again)', async done => { await Parse.User.signUp('asdf', 'zxcv'); rp.post({ url: 'http://localhost:8378/1/login', @@ -98,19 +103,23 @@ describe('Parse.User testing', () => { json: { _method: 'GET', username: 'asdf', - password: {'$regex':'^zx'}, - } - }).then((res) => { - fail(`no request should succeed: ${JSON.stringify(res)}`); - done(); - }).catch((err) => { - expect(err.statusCode).toBe(404); - expect(err.message).toMatch('{"code":101,"error":"Invalid username/password."}'); - done(); - }); + password: { $regex: '^zx' }, + }, + }) + .then(res => { + fail(`no request should succeed: ${JSON.stringify(res)}`); + done(); + }) + .catch(err => { + expect(err.statusCode).toBe(404); + expect(err.message).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); + done(); + }); }); - it('user login using POST with REST API', async (done) => { + it('user login using POST with REST API', async done => { await Parse.User.signUp('some_user', 'some_password'); rp.post({ url: 'http://localhost:8378/1/login', @@ -121,25 +130,27 @@ describe('Parse.User testing', () => { json: { username: 'some_user', password: 'some_password', - } - }).then((res) => { - expect(res.username).toBe('some_user'); - done(); - }).catch((err) => { - fail(`no request should fail: ${JSON.stringify(err)}`); - done(); - }); + }, + }) + .then(res => { + expect(res.username).toBe('some_user'); + done(); + }) + .catch(err => { + fail(`no request should fail: ${JSON.stringify(err)}`); + done(); + }); }); - it("user login", async (done) => { - await Parse.User.signUp("asdf", "zxcv"); - const user = await Parse.User.logIn("asdf", "zxcv"); - equal(user.get("username"), "asdf"); + it('user login', async done => { + await Parse.User.signUp('asdf', 'zxcv'); + const user = await Parse.User.logIn('asdf', 'zxcv'); + equal(user.get('username'), 'asdf'); verifyACL(user); done(); }); - it('should respect ACL without locking user out', (done) => { + it('should respect ACL without locking user out', done => { const user = new Parse.User(); const ACL = new Parse.ACL(); ACL.setPublicReadAccess(false); @@ -147,48 +158,54 @@ describe('Parse.User testing', () => { user.setUsername('asdf'); user.setPassword('zxcv'); user.setACL(ACL); - user.signUp().then(() => { - return Parse.User.logIn("asdf", "zxcv"); - }).then((user) => { - equal(user.get("username"), "asdf"); - 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 - const newACL = new Parse.ACL(); - newACL.setReadAccess(user.id, false); - newACL.setWriteAccess(user.id, false); - user.setACL(newACL); - return user.save(); - }).then(() => { - 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(() => { - fail("Should not fail"); - done(); - }) + user + .signUp() + .then(() => { + 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 + const newACL = new Parse.ACL(); + newACL.setReadAccess(user.id, false); + newACL.setWriteAccess(user.id, false); + user.setACL(newACL); + return user.save(); + }) + .then(() => { + 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(() => { + fail('Should not fail'); + done(); + }); }); - it('should let masterKey lockout user', (done) => { + it('should let masterKey lockout user', done => { const user = new Parse.User(); const ACL = new Parse.ACL(); ACL.setPublicReadAccess(false); @@ -196,27 +213,37 @@ describe('Parse.User testing', () => { user.setUsername('asdf'); user.setPassword('zxcv'); user.setACL(ACL); - user.signUp().then(() => { - return Parse.User.logIn("asdf", "zxcv"); - }).then((user) => { - equal(user.get("username"), "asdf"); - // Lock the user down - const ACL = new Parse.ACL(); - user.setACL(ACL); - return user.save(null, { useMasterKey: true }); - }).then(() => { - expect(user.getACL().getPublicReadAccess()).toBe(false); - return Parse.User.logIn("asdf", "zxcv"); - }).then(done.fail).catch((err) => { - expect(err.message).toBe('Invalid username/password.'); - expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND); - done(); - }); + user + .signUp() + .then(() => { + return Parse.User.logIn('asdf', 'zxcv'); + }) + .then(user => { + equal(user.get('username'), 'asdf'); + // Lock the user down + const ACL = new Parse.ACL(); + user.setACL(ACL); + return user.save(null, { useMasterKey: true }); + }) + .then(() => { + expect(user.getACL().getPublicReadAccess()).toBe(false); + return Parse.User.logIn('asdf', 'zxcv'); + }) + .then(done.fail) + .catch(err => { + expect(err.message).toBe('Invalid username/password.'); + expect(err.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + done(); + }); }); - it_only_db('mongo')('should let legacy users without ACL login', async() => { - const databaseURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; - const adapter = new MongoStorageAdapter({ collectionPrefix: 'test_', uri: databaseURI }); + it_only_db('mongo')('should let legacy users without ACL login', async () => { + const databaseURI = + 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; + const adapter = new MongoStorageAdapter({ + collectionPrefix: 'test_', + uri: databaseURI, + }); await adapter.connect(); await adapter.database.dropDatabase(); delete adapter.connectionPromise; @@ -230,12 +257,13 @@ describe('Parse.User testing', () => { const collection = await adapter._adaptiveCollection('_User'); await collection.insertOne({ // the hashed password is 'password' hashed - "_hashed_password": "$2b$10$mJ2ca2UbCM9hlojYHZxkQe8pyEXe5YMg0nMdvP4AJBeqlTEZJ6/Uu", - "_session_token": "xxx", - "email": "xxx@a.b", - "username": "oldUser", - "emailVerified": true, - "_email_verify_token": "yyy", + _hashed_password: + '$2b$10$mJ2ca2UbCM9hlojYHZxkQe8pyEXe5YMg0nMdvP4AJBeqlTEZJ6/Uu', + _session_token: 'xxx', + email: 'xxx@a.b', + username: 'oldUser', + emailVerified: true, + _email_verify_token: 'yyy', }); // get the 2 users @@ -249,7 +277,7 @@ describe('Parse.User testing', () => { expect(newUser).not.toBeUndefined(); }); - it('should be let masterKey lock user out with authData', (done) => { + it('should be let masterKey lock user out with authData', done => { let objectId; let sessionToken; @@ -259,133 +287,168 @@ describe('Parse.User testing', () => { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-REST-API-Key': 'rest', }, - json: { key: "value", authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}} - }).then((body) => { - objectId = body.objectId; - sessionToken = body.sessionToken; - expect(sessionToken).toBeDefined(); - expect(objectId).toBeDefined(); - const user = new Parse.User(); - user.id = objectId; - const ACL = new Parse.ACL(); - user.setACL(ACL); - return user.save(null, { useMasterKey: true }); - }).then(() => { - // update the user - const options = { - url: `http://localhost:8378/1/classes/_User/`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: { key: "otherValue", authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}} - } - return rp.post(options); - }).then((res) => { - // Because the user is locked out, this should behave as creating a new user - expect(res.objectId).not.toEqual(objectId); - }).then(done) + json: { + key: 'value', + authData: { anonymous: { id: '00000000-0000-0000-0000-000000000001' } }, + }, + }) + .then(body => { + objectId = body.objectId; + sessionToken = body.sessionToken; + expect(sessionToken).toBeDefined(); + expect(objectId).toBeDefined(); + const user = new Parse.User(); + user.id = objectId; + const ACL = new Parse.ACL(); + user.setACL(ACL); + return user.save(null, { useMasterKey: true }); + }) + .then(() => { + // update the user + const options = { + url: `http://localhost:8378/1/classes/_User/`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: { + key: 'otherValue', + authData: { + anonymous: { id: '00000000-0000-0000-0000-000000000001' }, + }, + }, + }; + return rp.post(options); + }) + .then(res => { + // Because the user is locked out, this should behave as creating a new user + expect(res.objectId).not.toEqual(objectId); + }) + .then(done) .catch(done.fail); }); - it("user login with files", (done) => { - const file = new Parse.File("yolo.txt", [1,2,3], "text/plain"); - file.save().then((file) => { - return Parse.User.signUp("asdf", "zxcv", { "file" : file }); - }).then(() => { - return Parse.User.logIn("asdf", "zxcv"); - }).then((user) => { - const fileAgain = user.get('file'); - ok(fileAgain.name()); - ok(fileAgain.url()); - done(); - }).catch(err => { - jfail(err); - done(); - }); + it('user login with files', done => { + const file = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain'); + file + .save() + .then(file => { + return Parse.User.signUp('asdf', 'zxcv', { file: file }); + }) + .then(() => { + return Parse.User.logIn('asdf', 'zxcv'); + }) + .then(user => { + const fileAgain = user.get('file'); + ok(fileAgain.name()); + ok(fileAgain.url()); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); it('become sends token back', done => { let user = null; let 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 => { - jfail(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 => { + jfail(error); + done(); + } + ); }); - it("become", (done) => { + it('become', done => { let user = null; let sessionToken = null; - Promise.resolve().then(function() { - return Parse.User.signUp("Jason", "Parse", { "code": "red" }); - - }).then(function(newUser) { - equal(Parse.User.current(), newUser); - - user = newUser; - sessionToken = newUser.getSessionToken(); - ok(sessionToken); - - return Parse.User.logOut(); - }).then(() => { - ok(!Parse.User.current()); - - return Parse.User.become(sessionToken); + Promise.resolve() + .then(function() { + return Parse.User.signUp('Jason', 'Parse', { code: 'red' }); + }) + .then(function(newUser) { + equal(Parse.User.current(), newUser); - }).then(function(newUser) { - equal(Parse.User.current(), newUser); + user = newUser; + sessionToken = newUser.getSessionToken(); + ok(sessionToken); - ok(newUser); - equal(newUser.id, user.id); - equal(newUser.get("username"), "Jason"); - equal(newUser.get("code"), "red"); + return Parse.User.logOut(); + }) + .then(() => { + ok(!Parse.User.current()); - return Parse.User.logOut(); - }).then(() => { - ok(!Parse.User.current()); + return Parse.User.become(sessionToken); + }) + .then(function(newUser) { + equal(Parse.User.current(), newUser); - return Parse.User.become("somegarbage"); + ok(newUser); + equal(newUser.id, user.id); + equal(newUser.get('username'), 'Jason'); + equal(newUser.get('code'), 'red'); - }).then(function() { - // This should have failed actually. - ok(false, "Shouldn't have been able to log in with garbage session token."); - }, function(error) { - ok(error); - // Handle the error. - return Promise.resolve(); + return Parse.User.logOut(); + }) + .then(() => { + ok(!Parse.User.current()); - }).then(function() { - done(); - }, function(error) { - ok(false, error); - done(); - }); + return Parse.User.become('somegarbage'); + }) + .then( + function() { + // This should have failed actually. + ok( + false, + "Shouldn't have been able to log in with garbage session token." + ); + }, + function(error) { + ok(error); + // Handle the error. + return Promise.resolve(); + } + ) + .then( + function() { + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it("cannot save non-authed user", async (done) => { + it('cannot save non-authed user', async done => { let user = new Parse.User(); user.set({ - "password": "asdf", - "email": "asdf@example.com", - "username": "zxcv" + password: 'asdf', + email: 'asdf@example.com', + username: 'zxcv', }); let userAgain = await user.signUp(); equal(userAgain, user); @@ -393,75 +456,74 @@ describe('Parse.User testing', () => { const userNotAuthed = await query.get(user.id); user = new Parse.User(); user.set({ - "username": "hacker", - "password": "password" + username: 'hacker', + password: 'password', }); userAgain = await user.signUp(); equal(userAgain, user); - userNotAuthed.set("username", "changed"); - userNotAuthed.save().then(fail, (err) => { + userNotAuthed.set('username', 'changed'); + userNotAuthed.save().then(fail, err => { expect(err.code).toEqual(Parse.Error.SESSION_MISSING); done(); }); }); - it("cannot delete non-authed user", async (done) => { + it('cannot delete non-authed user', async done => { let user = new Parse.User(); await user.signUp({ - "password": "asdf", - "email": "asdf@example.com", - "username": "zxcv" + password: 'asdf', + email: 'asdf@example.com', + username: 'zxcv', }); const query = new Parse.Query(Parse.User); const userNotAuthed = await query.get(user.id); user = new Parse.User(); const userAgain = await user.signUp({ - "username": "hacker", - "password": "password" + username: 'hacker', + password: 'password', }); equal(userAgain, user); - userNotAuthed.set("username", "changed"); + userNotAuthed.set('username', 'changed'); try { await userNotAuthed.destroy(); done.fail(); - } catch(e) { + } catch (e) { expect(e.code).toBe(Parse.Error.SESSION_MISSING); done(); } }); - it("cannot saveAll with non-authed user", async (done) => { + it('cannot saveAll with non-authed user', async done => { let user = new Parse.User(); await user.signUp({ - "password": "asdf", - "email": "asdf@example.com", - "username": "zxcv" + password: 'asdf', + email: 'asdf@example.com', + username: 'zxcv', }); const query = new Parse.Query(Parse.User); const userNotAuthed = await query.get(user.id); user = new Parse.User(); await user.signUp({ - username: "hacker", - password: "password" + username: 'hacker', + password: 'password', }); const userNotAuthedNotChanged = await query.get(user.id); - userNotAuthed.set("username", "changed"); + userNotAuthed.set('username', 'changed'); const object = new TestObject(); await object.save({ - user: userNotAuthedNotChanged + user: userNotAuthedNotChanged, }); const item1 = new TestObject(); await item1.save({ - number: 0 + number: 0, }); - item1.set("number", 1); + item1.set('number', 1); const item2 = new TestObject(); - item2.set("number", 2); + item2.set('number', 2); try { - await Parse.Object.saveAll( - [item1, item2, userNotAuthed]); + await Parse.Object.saveAll([item1, item2, userNotAuthed]); done.fail(); - } catch(e) { + } catch (e) { expect(e.code).toBe(Parse.Error.SESSION_MISSING); done(); } @@ -471,7 +533,7 @@ describe('Parse.User testing', () => { const user = new Parse.User(); await user.signUp({ username: 'username', - password: 'password' + password: 'password', }); user.setACL(new Parse.ACL()); await user.save(); @@ -500,7 +562,10 @@ describe('Parse.User testing', () => { await user.save({ ACL: acl }, { useMasterKey: true }); // Try to update from admin... should all work fine - await user.save({ key: 'fromAdmin'}, { sessionToken: admin.getSessionToken() }); + await user.save( + { key: 'fromAdmin' }, + { sessionToken: admin.getSessionToken() } + ); await user.fetch(); expect(user.toJSON().key).toEqual('fromAdmin'); @@ -509,8 +574,8 @@ describe('Parse.User testing', () => { try { // Ensure no session token is sent await Parse.User.logOut(); - await user.save({ key: 'fromPublic'}); - } catch(e) { + await user.save({ key: 'fromPublic' }); + } catch (e) { failed = true; expect(e.code).toBe(Parse.Error.SESSION_MISSING); } @@ -521,114 +586,125 @@ describe('Parse.User testing', () => { const anyUser = new Parse.User(); await anyUser.signUp({ username: 'randomUser', - password: 'password' + password: 'password', }); try { - await user.save({ key: 'fromAnyUser'}); - } catch(e) { + await user.save({ key: 'fromAnyUser' }); + } catch (e) { failed = true; expect(e.code).toBe(Parse.Error.SESSION_MISSING); } expect({ failed }).toEqual({ failed: true }); }); - it("current user", (done) => { + it('current user', done => { const user = new Parse.User(); - user.set("password", "asdf"); - user.set("email", "asdf@example.com"); - user.set("username", "zxcv"); - user.signUp().then(() => { - const currentUser = Parse.User.current(); - equal(user.id, currentUser.id); - ok(user.getSessionToken()); - - const currentUserAgain = Parse.User.current(); - // should be the same object - equal(currentUser, currentUserAgain); - - // test logging out the current user - return Parse.User.logOut(); - }).then(() => { - equal(Parse.User.current(), null); - done(); - }); + user.set('password', 'asdf'); + user.set('email', 'asdf@example.com'); + user.set('username', 'zxcv'); + user + .signUp() + .then(() => { + const currentUser = Parse.User.current(); + equal(user.id, currentUser.id); + ok(user.getSessionToken()); + + const currentUserAgain = Parse.User.current(); + // should be the same object + equal(currentUser, currentUserAgain); + + // test logging out the current user + return Parse.User.logOut(); + }) + .then(() => { + equal(Parse.User.current(), null); + done(); + }); }); - it("user.isCurrent", (done) => { + it('user.isCurrent', done => { const user1 = new Parse.User(); const user2 = new Parse.User(); const user3 = new Parse.User(); - user1.set("username", "a"); - user2.set("username", "b"); - user3.set("username", "c"); - - user1.set("password", "password"); - user2.set("password", "password"); - user3.set("password", "password"); - - user1.signUp().then(() => { - equal(user1.isCurrent(), true); - equal(user2.isCurrent(), false); - equal(user3.isCurrent(), false); - return user2.signUp(); - }).then(() => { - equal(user1.isCurrent(), false); - equal(user2.isCurrent(), true); - equal(user3.isCurrent(), false); - return user3.signUp(); - }).then(() => { - equal(user1.isCurrent(), false); - equal(user2.isCurrent(), false); - equal(user3.isCurrent(), true); - return Parse.User.logIn("a", "password"); - }).then(() => { - equal(user1.isCurrent(), true); - equal(user2.isCurrent(), false); - equal(user3.isCurrent(), false); - return Parse.User.logIn("b", "password"); - }).then(() => { - equal(user1.isCurrent(), false); - equal(user2.isCurrent(), true); - equal(user3.isCurrent(), false); - return Parse.User.logIn("b", "password"); - }).then(() => { - equal(user1.isCurrent(), false); - equal(user2.isCurrent(), true); - equal(user3.isCurrent(), false); - return Parse.User.logOut(); - }).then(() => { - equal(user2.isCurrent(), false); - done(); - }); + user1.set('username', 'a'); + user2.set('username', 'b'); + user3.set('username', 'c'); + + user1.set('password', 'password'); + user2.set('password', 'password'); + user3.set('password', 'password'); + + user1 + .signUp() + .then(() => { + equal(user1.isCurrent(), true); + equal(user2.isCurrent(), false); + equal(user3.isCurrent(), false); + return user2.signUp(); + }) + .then(() => { + equal(user1.isCurrent(), false); + equal(user2.isCurrent(), true); + equal(user3.isCurrent(), false); + return user3.signUp(); + }) + .then(() => { + equal(user1.isCurrent(), false); + equal(user2.isCurrent(), false); + equal(user3.isCurrent(), true); + return Parse.User.logIn('a', 'password'); + }) + .then(() => { + equal(user1.isCurrent(), true); + equal(user2.isCurrent(), false); + equal(user3.isCurrent(), false); + return Parse.User.logIn('b', 'password'); + }) + .then(() => { + equal(user1.isCurrent(), false); + equal(user2.isCurrent(), true); + equal(user3.isCurrent(), false); + return Parse.User.logIn('b', 'password'); + }) + .then(() => { + equal(user1.isCurrent(), false); + equal(user2.isCurrent(), true); + equal(user3.isCurrent(), false); + return Parse.User.logOut(); + }) + .then(() => { + equal(user2.isCurrent(), false); + done(); + }); }); - it("user associations", async (done) => { + it('user associations', async done => { const child = new TestObject(); await child.save(); const user = new Parse.User(); - user.set("password", "asdf"); - user.set("email", "asdf@example.com"); - user.set("username", "zxcv"); - user.set("child", child); + user.set('password', 'asdf'); + user.set('email', 'asdf@example.com'); + user.set('username', 'zxcv'); + user.set('child', child); await user.signUp(); const object = new TestObject(); - object.set("user", user); + object.set('user', user); await object.save(); const query = new Parse.Query(TestObject); const objectAgain = await query.get(object.id); - const userAgain = objectAgain.get("user"); + const userAgain = objectAgain.get('user'); await userAgain.fetch(); equal(user.id, userAgain.id); - equal(userAgain.get("child").id, child.id); + equal(userAgain.get('child').id, child.id); done(); }); - it("user queries", async (done) => { + it('user queries', async done => { const user = new Parse.User(); - user.set("password", "asdf"); - user.set("email", "asdf@example.com"); - user.set("username", "zxcv"); + user.set('password', 'asdf'); + user.set('email', 'asdf@example.com'); + user.set('username', 'zxcv'); await user.signUp(); const query = new Parse.Query(Parse.User); const userAgain = await query.get(user.id); @@ -636,31 +712,33 @@ describe('Parse.User testing', () => { const users = await query.find(); equal(users.length, 1); equal(users[0].id, user.id); - ok(userAgain.get("email"), "asdf@example.com"); + ok(userAgain.get('email'), 'asdf@example.com'); done(); }); function signUpAll(list, optionsOrCallback) { let promise = Promise.resolve(); - list.forEach((user) => { + list.forEach(user => { promise = promise.then(function() { return user.signUp(); }); }); - promise = promise.then(function() { return list; }); + promise = promise.then(function() { + return list; + }); return promise.then(optionsOrCallback); } - it("contained in user array queries", async (done) => { + it('contained in user array queries', async done => { const USERS = 4; const MESSAGES = 5; // Make a list of users. const userList = range(USERS).map(function(i) { const user = new Parse.User(); - user.set("password", "user_num_" + i); - user.set("email", "user_num_" + i + "@example.com"); - user.set("username", "xinglblog_num_" + i); + user.set('password', 'user_num_' + i); + user.set('email', 'user_num_' + i + '@example.com'); + user.set('username', 'xinglblog_num_' + i); return user; }); @@ -673,8 +751,8 @@ describe('Parse.User testing', () => { } const messageList = range(MESSAGES).map(function(i) { const message = new TestObject(); - message.set("to", users[(i + 1) % USERS]); - message.set("from", users[i % USERS]); + message.set('to', users[(i + 1) % USERS]); + message.set('from', users[i % USERS]); return message; }); @@ -682,39 +760,39 @@ describe('Parse.User testing', () => { await Parse.Object.saveAll(messageList); // Assemble an "in" list. - const inList = [users[0], users[3], users[3]]; // Intentional dupe + const inList = [users[0], users[3], users[3]]; // Intentional dupe const query = new Parse.Query(TestObject); - query.containedIn("from", inList); + query.containedIn('from', inList); const results = await query.find(); equal(results.length, 3); done(); }); }); - it("saving a user signs them up but doesn't log them in", async (done) => { + it("saving a user signs them up but doesn't log them in", async done => { const user = new Parse.User(); await user.save({ - password: "asdf", - email: "asdf@example.com", - username: "zxcv" + password: 'asdf', + email: 'asdf@example.com', + username: 'zxcv', }); equal(Parse.User.current(), null); done(); }); - it("user updates", async (done) => { + it('user updates', async done => { const user = new Parse.User(); await user.signUp({ - password: "asdf", - email: "asdf@example.com", - username: "zxcv" + password: 'asdf', + email: 'asdf@example.com', + username: 'zxcv', }); - user.set("username", "test"); + user.set('username', 'test'); await user.save(); equal(Object.keys(user.attributes).length, 6); - ok(user.attributes["username"]); - ok(user.attributes["email"]); + ok(user.attributes['username']); + ok(user.attributes['email']); await user.destroy(); const query = new Parse.Query(Parse.User); try { @@ -727,14 +805,14 @@ describe('Parse.User testing', () => { } }); - it("count users", async (done) => { + it('count users', async done => { const james = new Parse.User(); - james.set("username", "james"); - james.set("password", "mypass"); + james.set('username', 'james'); + james.set('password', 'mypass'); await james.signUp(); const kevin = new Parse.User(); - kevin.set("username", "kevin"); - kevin.set("password", "mypass"); + kevin.set('username', 'kevin'); + kevin.set('password', 'mypass'); await kevin.signUp(); const query = new Parse.Query(Parse.User); const count = await query.count(); @@ -742,24 +820,24 @@ describe('Parse.User testing', () => { done(); }); - it("user sign up with container class", async (done) => { - await Parse.User.signUp("ilya", "mypass", { "array": ["hello"] }); + it('user sign up with container class', async done => { + await Parse.User.signUp('ilya', 'mypass', { array: ['hello'] }); done(); }); - it("user modified while saving", (done) => { + it('user modified while saving', done => { Parse.Object.disableSingleInstance(); const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "password"); + user.set('username', 'alice'); + user.set('password', 'password'); user.signUp().then(function(userAgain) { - equal(userAgain.get("username"), "bob"); - ok(userAgain.dirty("username")); + equal(userAgain.get('username'), 'bob'); + ok(userAgain.dirty('username')); const query = new Parse.Query(Parse.User); - query.get(user.id).then((freshUser) => { + query.get(user.id).then(freshUser => { console.log(freshUser.toJSON()); equal(freshUser.id, user.id); - equal(freshUser.get("username"), "alice"); + equal(freshUser.get('username'), 'alice'); Parse.Object.enableSingleInstance(); done(); }); @@ -767,228 +845,277 @@ describe('Parse.User testing', () => { // Jump a frame so the signup call is properly sent // This is due to the fact that now, we use real promises process.nextTick(() => { - ok(user.set("username", "bob")); + ok(user.set('username', 'bob')); }); }); - it("user modified while saving with unsaved child", (done) => { + it('user modified while saving with unsaved child', done => { Parse.Object.disableSingleInstance(); const user = new Parse.User(); - user.set("username", "alice"); - user.set("password", "password"); - user.set("child", new TestObject()); - user.signUp().then((userAgain) => { - equal(userAgain.get("username"), "bob"); + user.set('username', 'alice'); + user.set('password', 'password'); + user.set('child', new TestObject()); + user.signUp().then(userAgain => { + equal(userAgain.get('username'), 'bob'); // Should be dirty, but it depends on batch support. // ok(userAgain.dirty("username")); const query = new Parse.Query(Parse.User); - query.get(user.id).then((freshUser) => { + query.get(user.id).then(freshUser => { equal(freshUser.id, user.id); // Should be alice, but it depends on batch support. - equal(freshUser.get("username"), "bob"); + equal(freshUser.get('username'), 'bob'); Parse.Object.enableSingleInstance(); done(); }); }); - ok(user.set("username", "bob")); + ok(user.set('username', 'bob')); }); - it("user loaded from localStorage from signup", async (done) => { - const alice = await Parse.User.signUp("alice", "password"); - ok(alice.id, "Alice should have an objectId"); - ok(alice.getSessionToken(), "Alice should have a session token"); - equal(alice.get("password"), undefined, - "Alice should not have a password"); + it('user loaded from localStorage from signup', async done => { + const alice = await Parse.User.signUp('alice', 'password'); + ok(alice.id, 'Alice should have an objectId'); + ok(alice.getSessionToken(), 'Alice should have a session token'); + equal(alice.get('password'), undefined, 'Alice should not have a password'); // Simulate the environment getting reset. Parse.User._currentUser = null; Parse.User._currentUserMatchesDisk = false; const aliceAgain = Parse.User.current(); - equal(aliceAgain.get("username"), "alice"); - equal(aliceAgain.id, alice.id, "currentUser should have objectId"); - ok(aliceAgain.getSessionToken(), - "currentUser should have a sessionToken"); - equal(alice.get("password"), undefined, - "currentUser should not have password"); + equal(aliceAgain.get('username'), 'alice'); + equal(aliceAgain.id, alice.id, 'currentUser should have objectId'); + ok(aliceAgain.getSessionToken(), 'currentUser should have a sessionToken'); + equal( + alice.get('password'), + undefined, + 'currentUser should not have password' + ); done(); }); - - it("user loaded from localStorage from login", (done) => { + it('user loaded from localStorage from login', done => { let id; - Parse.User.signUp("alice", "password").then((alice) => { - id = alice.id; - return Parse.User.logOut(); - }).then(() => { - return Parse.User.logIn("alice", "password"); - }).then(() => { - // Force the current user to read from disk - delete Parse.User._currentUser; - delete Parse.User._currentUserMatchesDisk; - - const userFromDisk = Parse.User.current(); - equal(userFromDisk.get("password"), undefined, - "password should not be in attributes"); - equal(userFromDisk.id, id, "id should be set"); - ok(userFromDisk.getSessionToken(), - "currentUser should have a sessionToken"); - done(); - }); + Parse.User.signUp('alice', 'password') + .then(alice => { + id = alice.id; + return Parse.User.logOut(); + }) + .then(() => { + return Parse.User.logIn('alice', 'password'); + }) + .then(() => { + // Force the current user to read from disk + delete Parse.User._currentUser; + delete Parse.User._currentUserMatchesDisk; + + const userFromDisk = Parse.User.current(); + equal( + userFromDisk.get('password'), + undefined, + 'password should not be in attributes' + ); + equal(userFromDisk.id, id, 'id should be set'); + ok( + userFromDisk.getSessionToken(), + 'currentUser should have a sessionToken' + ); + done(); + }); }); - it("saving user after browser refresh", (done) => { + it('saving user after browser refresh', done => { let id; - Parse.User.signUp("alice", "password", null).then(function(alice) { - id = alice.id; - return Parse.User.logOut(); - }).then(() => { - return Parse.User.logIn("alice", "password"); - }).then(function() { - // Simulate browser refresh by force-reloading user from localStorage - Parse.User._clearCache(); - - // Test that this save works correctly - return Parse.User.current().save({some_field: 1}); - }).then(function() { - // Check the user in memory just after save operation - const userInMemory = Parse.User.current(); - - equal(userInMemory.getUsername(), "alice", - "saving user should not remove existing fields"); - - equal(userInMemory.get('some_field'), 1, - "saving user should save specified field"); - - equal(userInMemory.get("password"), undefined, - "password should not be in attributes after saving user"); - - equal(userInMemory.get("objectId"), undefined, - "objectId should not be in attributes after saving user"); - - equal(userInMemory.get("_id"), undefined, - "_id should not be in attributes after saving user"); - - equal(userInMemory.id, id, "id should be set"); - - expect(userInMemory.updatedAt instanceof Date).toBe(true); - - ok(userInMemory.createdAt instanceof Date); - - ok(userInMemory.getSessionToken(), - "user should have a sessionToken after saving"); - - // Force the current user to read from localStorage, and check again - delete Parse.User._currentUser; - delete Parse.User._currentUserMatchesDisk; - const userFromDisk = Parse.User.current(); - - equal(userFromDisk.getUsername(), "alice", - "userFromDisk should have previously existing fields"); - - equal(userFromDisk.get('some_field'), 1, - "userFromDisk should have saved field"); - - equal(userFromDisk.get("password"), undefined, - "password should not be in attributes of userFromDisk"); - - equal(userFromDisk.get("objectId"), undefined, - "objectId should not be in attributes of userFromDisk"); - - equal(userFromDisk.get("_id"), undefined, - "_id should not be in attributes of userFromDisk"); - - equal(userFromDisk.id, id, "id should be set on userFromDisk"); - - ok(userFromDisk.updatedAt instanceof Date); - - ok(userFromDisk.createdAt instanceof Date); + Parse.User.signUp('alice', 'password', null) + .then(function(alice) { + id = alice.id; + return Parse.User.logOut(); + }) + .then(() => { + return Parse.User.logIn('alice', 'password'); + }) + .then(function() { + // Simulate browser refresh by force-reloading user from localStorage + Parse.User._clearCache(); - ok(userFromDisk.getSessionToken(), - "userFromDisk should have a sessionToken"); + // Test that this save works correctly + return Parse.User.current().save({ some_field: 1 }); + }) + .then( + function() { + // Check the user in memory just after save operation + const userInMemory = Parse.User.current(); + + equal( + userInMemory.getUsername(), + 'alice', + 'saving user should not remove existing fields' + ); + + equal( + userInMemory.get('some_field'), + 1, + 'saving user should save specified field' + ); + + equal( + userInMemory.get('password'), + undefined, + 'password should not be in attributes after saving user' + ); + + equal( + userInMemory.get('objectId'), + undefined, + 'objectId should not be in attributes after saving user' + ); + + equal( + userInMemory.get('_id'), + undefined, + '_id should not be in attributes after saving user' + ); + + equal(userInMemory.id, id, 'id should be set'); + + expect(userInMemory.updatedAt instanceof Date).toBe(true); + + ok(userInMemory.createdAt instanceof Date); + + ok( + userInMemory.getSessionToken(), + 'user should have a sessionToken after saving' + ); + + // Force the current user to read from localStorage, and check again + delete Parse.User._currentUser; + delete Parse.User._currentUserMatchesDisk; + const userFromDisk = Parse.User.current(); + + equal( + userFromDisk.getUsername(), + 'alice', + 'userFromDisk should have previously existing fields' + ); + + equal( + userFromDisk.get('some_field'), + 1, + 'userFromDisk should have saved field' + ); + + equal( + userFromDisk.get('password'), + undefined, + 'password should not be in attributes of userFromDisk' + ); + + equal( + userFromDisk.get('objectId'), + undefined, + 'objectId should not be in attributes of userFromDisk' + ); + + equal( + userFromDisk.get('_id'), + undefined, + '_id should not be in attributes of userFromDisk' + ); + + equal(userFromDisk.id, id, 'id should be set on userFromDisk'); + + ok(userFromDisk.updatedAt instanceof Date); + + ok(userFromDisk.createdAt instanceof Date); + + ok( + userFromDisk.getSessionToken(), + 'userFromDisk should have a sessionToken' + ); - done(); - }, function(error) { - ok(false, error); - done(); - }); + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it("user with missing username", async (done) => { + it('user with missing username', async done => { const user = new Parse.User(); - user.set("password", "foo"); + user.set('password', 'foo'); try { await user.signUp(); done.fail(); - } catch(error) { + } catch (error) { equal(error.code, Parse.Error.OTHER_CAUSE); done(); } }); - it("user with missing password", async (done) => { + it('user with missing password', async done => { const user = new Parse.User(); - user.set("username", "foo"); + user.set('username', 'foo'); try { await user.signUp(); done.fail(); - } catch(error) { + } catch (error) { equal(error.code, Parse.Error.OTHER_CAUSE); done(); } }); - it("user stupid subclassing", async (done) => { - - const SuperUser = Parse.Object.extend("User"); + it('user stupid subclassing', async done => { + const SuperUser = Parse.Object.extend('User'); const user = new SuperUser(); - user.set("username", "bob"); - user.set("password", "welcome"); - ok(user instanceof Parse.User, "Subclassing User should have worked"); + user.set('username', 'bob'); + user.set('password', 'welcome'); + ok(user instanceof Parse.User, 'Subclassing User should have worked'); await user.signUp(); done(); }); - it("user signup class method uses subclassing", async (done) => { - + it('user signup class method uses subclassing', async done => { const SuperUser = Parse.User.extend({ secret: function() { return 1337; - } + }, }); - const user = await Parse.User.signUp("bob", "welcome"); - ok(user instanceof SuperUser, "Subclassing User should have worked"); + const user = await Parse.User.signUp('bob', 'welcome'); + ok(user instanceof SuperUser, 'Subclassing User should have worked'); equal(user.secret(), 1337); done(); }); - it("user on disk gets updated after save", async (done) => { + it('user on disk gets updated after save', async done => { Parse.User.extend({ isSuper: function() { return true; - } + }, }); - const user = await Parse.User.signUp("bob", "welcome"); - await user.save("secret", 1337) + const user = await Parse.User.signUp('bob', 'welcome'); + await user.save('secret', 1337); delete Parse.User._currentUser; delete Parse.User._currentUserMatchesDisk; const userFromDisk = Parse.User.current(); - equal(userFromDisk.get("secret"), 1337); - ok(userFromDisk.isSuper(), "The subclass should have been used"); + equal(userFromDisk.get('secret'), 1337); + ok(userFromDisk.isSuper(), 'The subclass should have been used'); done(); }); - it("current user isn't dirty", async (done) => { - const user = await Parse.User.signUp("andrew", "oppa", { style: "gangnam" }); - ok(!user.dirty("style"), "The user just signed up."); + it("current user isn't dirty", async done => { + const user = await Parse.User.signUp('andrew', 'oppa', { + style: 'gangnam', + }); + ok(!user.dirty('style'), 'The user just signed up.'); Parse.User._currentUser = null; Parse.User._currentUserMatchesDisk = false; const userAgain = Parse.User.current(); - ok(!userAgain.dirty("style"), "The user was just read from disk."); + ok(!userAgain.dirty('style'), 'The user was just read from disk.'); done(); }); @@ -1007,7 +1134,7 @@ describe('Parse.User testing', () => { authenticate: function(options) { if (this.shouldError) { - options.error(this, "An error occurred"); + options.error(this, 'An error occurred'); } else if (this.shouldCancel) { options.error(this, null); } else { @@ -1027,14 +1154,14 @@ describe('Parse.User testing', () => { return true; }, getAuthType: function() { - return "facebook"; + return 'facebook'; }, deauthenticate: function() { this.loggedOut = true; this.restoreAuthentication(null); - } + }, }; - } + }; // Note that this mocks out client-side Facebook action rather than // server-side. @@ -1045,8 +1172,8 @@ describe('Parse.User testing', () => { const getMockMyOauthProvider = function() { return { authData: { - id: "12345", - access_token: "12345", + id: '12345', + access_token: '12345', expiration_date: new Date().toJSON(), }, shouldError: false, @@ -1057,7 +1184,7 @@ describe('Parse.User testing', () => { authenticate: function(options) { if (this.shouldError) { - options.error(this, "An error occurred"); + options.error(this, 'An error occurred'); } else if (this.shouldCancel) { options.error(this, null); } else { @@ -1077,37 +1204,39 @@ describe('Parse.User testing', () => { return true; }, getAuthType: function() { - return "myoauth"; + return 'myoauth'; }, deauthenticate: function() { this.loggedOut = true; this.restoreAuthentication(null); - } + }, }; }; Parse.User.extend({ extended: function() { return true; - } + }, }); - it("log in with provider", async (done) => { + it('log in with provider', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook"); - ok(model instanceof Parse.User, "Model should be a Parse.User"); + const model = await Parse.User._logInWith('facebook'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); strictEqual(Parse.User.current(), model); - ok(model.extended(), "Should have used subclass."); + ok(model.extended(), 'Should have used subclass.'); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked to facebook"); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); done(); }); - it("user authData should be available in cloudcode (#2342)", async (done) => { - + it('user authData should be available in cloudcode (#2342)', async done => { Parse.Cloud.define('checkLogin', (req, res) => { expect(req.user).not.toBeUndefined(); expect(Parse.FacebookUtils.isLinked(req.user)).toBe(true); @@ -1116,361 +1245,388 @@ describe('Parse.User testing', () => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook"); - ok(model instanceof Parse.User, "Model should be a Parse.User"); + const model = await Parse.User._logInWith('facebook'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); strictEqual(Parse.User.current(), model); - ok(model.extended(), "Should have used subclass."); + ok(model.extended(), 'Should have used subclass.'); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked to facebook"); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); Parse.Cloud.run('checkLogin').then(done, done); }); - it("log in with provider and update token", async (done) => { + it('log in with provider and update token', async done => { const provider = getMockFacebookProvider(); - const secondProvider = getMockFacebookProviderWithIdToken('8675309', 'jenny_valid_token'); + const secondProvider = getMockFacebookProviderWithIdToken( + '8675309', + 'jenny_valid_token' + ); Parse.User._registerAuthenticationProvider(provider); - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); Parse.User._registerAuthenticationProvider(secondProvider); await Parse.User.logOut(); - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); expect(secondProvider.synchronizedAuthToken).toEqual('jenny_valid_token'); // Make sure we can login with the new token again await Parse.User.logOut(); - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); done(); }); - it('returns authData when authed and logged in with provider (regression test for #1498)', async (done) => { + it('returns authData when authed and logged in with provider (regression test for #1498)', async done => { Parse.Object.enableSingleInstance(); const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); const user = await Parse.User._logInWith('facebook'); const userQuery = new Parse.Query(Parse.User); - userQuery.get(user.id) - .then(user => { - expect(user.get('authData')).not.toBeUndefined(); - Parse.Object.disableSingleInstance(); - done(); - }); + userQuery.get(user.id).then(user => { + expect(user.get('authData')).not.toBeUndefined(); + Parse.Object.disableSingleInstance(); + done(); + }); }); - it('only creates a single session for an installation / user pair (#2885)', async (done) => { + it('only creates a single session for an installation / user pair (#2885)', async done => { Parse.Object.disableSingleInstance(); const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - await Parse.User.logInWith('facebook') + await Parse.User.logInWith('facebook'); await Parse.User.logInWith('facebook'); const user = await Parse.User.logInWith('facebook'); const sessionToken = user.getSessionToken(); const query = new Parse.Query('_Session'); - return query.find({ useMasterKey: true }) - .then((results) => { + return query + .find({ useMasterKey: true }) + .then(results => { expect(results.length).toBe(1); expect(results[0].get('sessionToken')).toBe(sessionToken); expect(results[0].get('createdWith')).toEqual({ action: 'login', - authProvider: 'facebook' + authProvider: 'facebook', }); done(); - }).catch(done.fail); + }) + .catch(done.fail); }); it('log in with provider with files', done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - const file = new Parse.File("yolo.txt", [1, 2, 3], "text/plain"); - file.save().then(file => { - const user = new Parse.User(); - user.set('file', file); - return user._linkWith('facebook', {}); - }).then(user => { - expect(user._isLinked("facebook")).toBeTruthy(); - return Parse.User._logInWith('facebook', {}); - }).then(user => { - const fileAgain = user.get('file'); - expect(fileAgain.name()).toMatch(/yolo.txt$/); - expect(fileAgain.url()).toMatch(/yolo.txt$/); - }).then(() => { - done(); - }).catch(done.fail); + const file = new Parse.File('yolo.txt', [1, 2, 3], 'text/plain'); + file + .save() + .then(file => { + const user = new Parse.User(); + user.set('file', file); + return user._linkWith('facebook', {}); + }) + .then(user => { + expect(user._isLinked('facebook')).toBeTruthy(); + return Parse.User._logInWith('facebook', {}); + }) + .then(user => { + const fileAgain = user.get('file'); + expect(fileAgain.name()).toMatch(/yolo.txt$/); + expect(fileAgain.url()).toMatch(/yolo.txt$/); + }) + .then(() => { + done(); + }) + .catch(done.fail); }); - it("log in with provider twice", async (done) => { + it('log in with provider twice', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook"); - ok(model instanceof Parse.User, "Model should be a Parse.User"); + const model = await Parse.User._logInWith('facebook'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); strictEqual(Parse.User.current(), model); - ok(model.extended(), "Should have used the subclass."); + ok(model.extended(), 'Should have used the subclass.'); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked to facebook"); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); Parse.User.logOut().then(async () => { ok(provider.loggedOut); provider.loggedOut = false; - const innerModel = await Parse.User._logInWith("facebook"); - ok(innerModel instanceof Parse.User, - "Model should be a Parse.User"); - ok(innerModel === Parse.User.current(), - "Returned model should be the current user"); + const innerModel = await Parse.User._logInWith('facebook'); + ok(innerModel instanceof Parse.User, 'Model should be a Parse.User'); + ok( + innerModel === Parse.User.current(), + 'Returned model should be the current user' + ); ok(provider.authData.id === provider.synchronizedUserId); ok(provider.authData.access_token === provider.synchronizedAuthToken); - ok(innerModel._isLinked("facebook"), - "User should be linked to facebook"); - ok(innerModel.existed(), "User should not be newly-created"); + ok(innerModel._isLinked('facebook'), 'User should be linked to facebook'); + ok(innerModel.existed(), 'User should not be newly-created'); done(); }, done.fail); }); - it("log in with provider failed", async (done) => { + it('log in with provider failed', async done => { const provider = getMockFacebookProvider(); provider.shouldError = true; Parse.User._registerAuthenticationProvider(provider); try { - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); done.fail(); } catch (error) { - ok(error, "Error should be non-null"); + ok(error, 'Error should be non-null'); done(); } }); - it("log in with provider cancelled", async (done) => { + it('log in with provider cancelled', async done => { const provider = getMockFacebookProvider(); provider.shouldCancel = true; Parse.User._registerAuthenticationProvider(provider); try { - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); done.fail(); } catch (error) { - ok(error === null, "Error should be null"); + ok(error === null, 'Error should be null'); done(); } }); - it("login with provider should not call beforeSave trigger", async (done) => { + it('login with provider should not call beforeSave trigger', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); Parse.User.logOut().then(async () => { Parse.Cloud.beforeSave(Parse.User, function(req, res) { res.error("Before save shouldn't be called on login"); }); - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); done(); }); }); - it("link with provider", async (done) => { + it('link with provider', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); const user = new Parse.User(); - user.set("username", "testLinkWithProvider"); - user.set("password", "mypass"); + user.set('username', 'testLinkWithProvider'); + user.set('password', 'mypass'); await user.signUp(); - const model = await user._linkWith("facebook"); - ok(model instanceof Parse.User, "Model should be a Parse.User"); + const model = await user._linkWith('facebook'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); strictEqual(Parse.User.current(), model); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked"); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked'); done(); }); // What this means is, only one Parse User can be linked to a // particular Facebook account. - it("link with provider for already linked user", async (done) => { + it('link with provider for already linked user', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); const user = new Parse.User(); - user.set("username", "testLinkWithProviderToAlreadyLinkedUser"); - user.set("password", "mypass"); + user.set('username', 'testLinkWithProviderToAlreadyLinkedUser'); + user.set('password', 'mypass'); await user.signUp(); - const model = await user._linkWith("facebook"); - ok(model instanceof Parse.User, "Model should be a Parse.User"); + const model = await user._linkWith('facebook'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); strictEqual(Parse.User.current(), model); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked."); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked.'); const user2 = new Parse.User(); - user2.set("username", "testLinkWithProviderToAlreadyLinkedUser2"); - user2.set("password", "mypass"); + user2.set('username', 'testLinkWithProviderToAlreadyLinkedUser2'); + user2.set('password', 'mypass'); await user2.signUp(); try { await user2._linkWith('facebook'); done.fail(); } catch (error) { - expect(error.code).toEqual( - Parse.Error.ACCOUNT_ALREADY_LINKED); + expect(error.code).toEqual(Parse.Error.ACCOUNT_ALREADY_LINKED); done(); } }); - it("link with provider failed", async (done) => { + it('link with provider failed', async done => { const provider = getMockFacebookProvider(); provider.shouldError = true; Parse.User._registerAuthenticationProvider(provider); const user = new Parse.User(); - user.set("username", "testLinkWithProvider"); - user.set("password", "mypass"); + user.set('username', 'testLinkWithProvider'); + user.set('password', 'mypass'); await user.signUp(); try { - await user._linkWith("facebook"); + await user._linkWith('facebook'); done.fail(); - } catch(error) { - ok(error, "Linking should fail"); - ok(!user._isLinked("facebook"), - "User should not be linked to facebook"); + } catch (error) { + ok(error, 'Linking should fail'); + ok(!user._isLinked('facebook'), 'User should not be linked to facebook'); done(); } }); - it("link with provider cancelled", async (done) => { + it('link with provider cancelled', async done => { const provider = getMockFacebookProvider(); provider.shouldCancel = true; Parse.User._registerAuthenticationProvider(provider); const user = new Parse.User(); - user.set("username", "testLinkWithProvider"); - user.set("password", "mypass"); + user.set('username', 'testLinkWithProvider'); + user.set('password', 'mypass'); await user.signUp(); try { - await user._linkWith("facebook"); + await user._linkWith('facebook'); done.fail(); - } catch(error) { - ok(!error, "Linking should be cancelled"); - ok(!user._isLinked("facebook"), - "User should not be linked to facebook"); + } catch (error) { + ok(!error, 'Linking should be cancelled'); + ok(!user._isLinked('facebook'), 'User should not be linked to facebook'); done(); } }); - it("unlink with provider", async (done) => { + it('unlink with provider', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook"); - ok(model instanceof Parse.User, "Model should be a Parse.User."); + const model = await Parse.User._logInWith('facebook'); + ok(model instanceof Parse.User, 'Model should be a Parse.User.'); strictEqual(Parse.User.current(), model); - ok(model.extended(), "Should have used the subclass."); + ok(model.extended(), 'Should have used the subclass.'); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked to facebook."); - await model._unlinkFrom("facebook"); - ok(!model._isLinked("facebook"), "User should not be linked."); - ok(!provider.synchronizedUserId, "User id should be cleared."); - ok(!provider.synchronizedAuthToken, - "Auth token should be cleared."); - ok(!provider.synchronizedExpiration, - "Expiration should be cleared."); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked to facebook.'); + await model._unlinkFrom('facebook'); + ok(!model._isLinked('facebook'), 'User should not be linked.'); + ok(!provider.synchronizedUserId, 'User id should be cleared.'); + ok(!provider.synchronizedAuthToken, 'Auth token should be cleared.'); + ok(!provider.synchronizedExpiration, 'Expiration should be cleared.'); done(); }); - it("unlink and link", async (done) => { + it('unlink and link', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook"); - ok(model instanceof Parse.User, "Model should be a Parse.User"); + const model = await Parse.User._logInWith('facebook'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); strictEqual(Parse.User.current(), model); - ok(model.extended(), "Should have used the subclass."); + ok(model.extended(), 'Should have used the subclass.'); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked to facebook"); - - await model._unlinkFrom("facebook"); - ok(!model._isLinked("facebook"), - "User should not be linked to facebook"); - ok(!provider.synchronizedUserId, "User id should be cleared"); - ok(!provider.synchronizedAuthToken, "Auth token should be cleared"); - ok(!provider.synchronizedExpiration, - "Expiration should be cleared"); - - await model._linkWith("facebook"); - ok(provider.synchronizedUserId, "User id should have a value"); - ok(provider.synchronizedAuthToken, - "Auth token should have a value"); - ok(provider.synchronizedExpiration, - "Expiration should have a value"); - ok(model._isLinked("facebook"), - "User should be linked to facebook"); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); + + await model._unlinkFrom('facebook'); + ok(!model._isLinked('facebook'), 'User should not be linked to facebook'); + ok(!provider.synchronizedUserId, 'User id should be cleared'); + ok(!provider.synchronizedAuthToken, 'Auth token should be cleared'); + ok(!provider.synchronizedExpiration, 'Expiration should be cleared'); + + await model._linkWith('facebook'); + ok(provider.synchronizedUserId, 'User id should have a value'); + ok(provider.synchronizedAuthToken, 'Auth token should have a value'); + ok(provider.synchronizedExpiration, 'Expiration should have a value'); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); done(); }); - it("link multiple providers", async (done) => { + it('link multiple providers', async done => { const provider = getMockFacebookProvider(); const mockProvider = getMockMyOauthProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook"); - ok(model instanceof Parse.User, "Model should be a Parse.User"); + const model = await Parse.User._logInWith('facebook'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); strictEqual(Parse.User.current(), model); - ok(model.extended(), "Should have used the subclass."); + ok(model.extended(), 'Should have used the subclass.'); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked to facebook"); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); Parse.User._registerAuthenticationProvider(mockProvider); const objectId = model.id; - await model._linkWith("myoauth"); + await model._linkWith('myoauth'); expect(model.id).toEqual(objectId); - ok(model._isLinked("facebook"), "User should be linked to facebook"); - ok(model._isLinked("myoauth"), "User should be linked to myoauth"); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); + ok(model._isLinked('myoauth'), 'User should be linked to myoauth'); done(); }); - it("link multiple providers and updates token", async (done) => { + it('link multiple providers and updates token', async done => { const provider = getMockFacebookProvider(); - const secondProvider = getMockFacebookProviderWithIdToken('8675309', 'jenny_valid_token'); + const secondProvider = getMockFacebookProviderWithIdToken( + '8675309', + 'jenny_valid_token' + ); const mockProvider = getMockMyOauthProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook"); + const model = await Parse.User._logInWith('facebook'); Parse.User._registerAuthenticationProvider(mockProvider); const objectId = model.id; - await model._linkWith("myoauth"); + await model._linkWith('myoauth'); Parse.User._registerAuthenticationProvider(secondProvider); await Parse.User.logOut(); - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); await Parse.User.logOut(); - const user = await Parse.User._logInWith("myoauth"); + const user = await Parse.User._logInWith('myoauth'); expect(user.id).toBe(objectId); done(); }); - it("link multiple providers and update token", async (done) => { + it('link multiple providers and update token', async done => { const provider = getMockFacebookProvider(); const mockProvider = getMockMyOauthProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook"); - ok(model instanceof Parse.User, "Model should be a Parse.User"); + const model = await Parse.User._logInWith('facebook'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); strictEqual(Parse.User.current(), model); - ok(model.extended(), "Should have used the subclass."); + ok(model.extended(), 'Should have used the subclass.'); strictEqual(provider.authData.id, provider.synchronizedUserId); strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked to facebook"); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); Parse.User._registerAuthenticationProvider(mockProvider); const objectId = model.id; - await model._linkWith("myoauth"); + await model._linkWith('myoauth'); expect(model.id).toEqual(objectId); - ok(model._isLinked("facebook"), "User should be linked to facebook"); - ok(model._isLinked("myoauth"), "User should be linked to myoauth"); - await model._linkWith("facebook"); - ok(model._isLinked("facebook"), "User should be linked to facebook"); - ok(model._isLinked("myoauth"), "User should be linked to myoauth"); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); + ok(model._isLinked('myoauth'), 'User should be linked to myoauth'); + await model._linkWith('facebook'); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); + ok(model._isLinked('myoauth'), 'User should be linked to myoauth'); done(); }); - it('should fail linking with existing', async (done) => { + it('should fail linking with existing', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); await Parse.User.logOut(); const user = new Parse.User(); user.setUsername('user'); @@ -1480,39 +1636,44 @@ describe('Parse.User testing', () => { try { await user._linkWith('facebook'); done.fail(); - } catch(e) { + } catch (e) { done(); } }); - it('should fail linking with existing through REST', async (done) => { + it('should fail linking with existing through REST', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook"); + const model = await Parse.User._logInWith('facebook'); const userId = model.id; Parse.User.logOut().then(() => { - request.post({ - url:Parse.serverURL + '/classes/_User', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' + request.post( + { + url: Parse.serverURL + '/classes/_User', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: { authData: { facebook: provider.authData } }, }, - json: {authData: {facebook: provider.authData}} - }, (err,res, body) => { - // make sure the location header is properly set - expect(userId).not.toBeUndefined(); - expect(body.objectId).toEqual(userId); - expect(res.headers.location).toEqual(Parse.serverURL + '/users/' + userId); - done(); - }); + (err, res, body) => { + // make sure the location header is properly set + expect(userId).not.toBeUndefined(); + expect(body.objectId).toEqual(userId); + expect(res.headers.location).toEqual( + Parse.serverURL + '/users/' + userId + ); + done(); + } + ); }); }); - it('should allow login with old authData token', (done) => { + it('should allow login with old authData token', done => { const provider = { authData: { id: '12345', - access_token: 'token' + access_token: 'token', }, restoreAuthentication: function() { return true; @@ -1524,29 +1685,36 @@ describe('Parse.User testing', () => { options.success(this, provider.authData); }, getAuthType: function() { - return "shortLivedAuth"; - } - } + return 'shortLivedAuth'; + }, + }; defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('token'); Parse.User._registerAuthenticationProvider(provider); - Parse.User._logInWith("shortLivedAuth", {}).then(() => { - // Simulate a remotely expired token (like a short lived one) - // In this case, we want success as it was valid once. - // If the client needs an updated one, do lock the user out - defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken'); - return Parse.User._logInWith("shortLivedAuth", {}); - }).then(() => { - done(); - }, (err) => { - done.fail(err); - }); + Parse.User._logInWith('shortLivedAuth', {}) + .then(() => { + // Simulate a remotely expired token (like a short lived one) + // In this case, we want success as it was valid once. + // If the client needs an updated one, do lock the user out + defaultConfiguration.auth.shortLivedAuth.setValidAccessToken( + 'otherToken' + ); + return Parse.User._logInWith('shortLivedAuth', {}); + }) + .then( + () => { + done(); + }, + err => { + done.fail(err); + } + ); }); - it('should allow PUT request with stale auth Data', (done) => { + it('should allow PUT request with stale auth Data', done => { const provider = { authData: { id: '12345', - access_token: 'token' + access_token: 'token', }, restoreAuthentication: function() { return true; @@ -1558,64 +1726,78 @@ describe('Parse.User testing', () => { options.success(this, provider.authData); }, getAuthType: function() { - return "shortLivedAuth"; - } - } + return 'shortLivedAuth'; + }, + }; defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('token'); Parse.User._registerAuthenticationProvider(provider); - Parse.User._logInWith("shortLivedAuth", {}).then(() => { - // Simulate a remotely expired token (like a short lived one) - // In this case, we want success as it was valid once. - // If the client needs an updated one, do lock the user out - defaultConfiguration.auth.shortLivedAuth.setValidAccessToken('otherToken'); - return rp.put({ - url: Parse.serverURL + '/users/' + Parse.User.current().id, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey, - 'X-Parse-Session-Token': Parse.User.current().getSessionToken(), - 'Content-Type': 'application/json' + Parse.User._logInWith('shortLivedAuth', {}) + .then(() => { + // Simulate a remotely expired token (like a short lived one) + // In this case, we want success as it was valid once. + // If the client needs an updated one, do lock the user out + defaultConfiguration.auth.shortLivedAuth.setValidAccessToken( + 'otherToken' + ); + return rp.put({ + url: Parse.serverURL + '/users/' + Parse.User.current().id, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey, + 'X-Parse-Session-Token': Parse.User.current().getSessionToken(), + 'Content-Type': 'application/json', + }, + json: { + key: 'value', // update a key + authData: { + // pass the original auth data + shortLivedAuth: { + id: '12345', + access_token: 'token', + }, + }, + }, + }); + }) + .then( + () => { + done(); }, - json: { - key: 'value', // update a key - authData: { // pass the original auth data - shortLivedAuth: { - id: '12345', - access_token: 'token' - } - } + err => { + done.fail(err); } - }) - }).then(() => { - done(); - }, (err) => { - done.fail(err); - }); + ); }); - it('should properly error when password is missing', async (done) => { + it('should properly error when password is missing', async done => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - const user = await Parse.User._logInWith("facebook"); + const user = await Parse.User._logInWith('facebook'); 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(); - }); + 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('should have authData in beforeSave and afterSave', async (done) => { - - Parse.Cloud.beforeSave('_User', (request) => { + it('should have authData in beforeSave and afterSave', async done => { + Parse.Cloud.beforeSave('_User', request => { const authData = request.object.get('authData'); expect(authData).not.toBeUndefined(); if (authData) { @@ -1626,7 +1808,7 @@ describe('Parse.User testing', () => { } }); - Parse.Cloud.afterSave('_User', (request) => { + Parse.Cloud.afterSave('_User', request => { const authData = request.object.get('authData'); expect(authData).not.toBeUndefined(); if (authData) { @@ -1639,707 +1821,885 @@ describe('Parse.User testing', () => { const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); - await Parse.User._logInWith("facebook"); + await Parse.User._logInWith('facebook'); done(); }); - it('set password then change password', (done) => { - Parse.User.signUp('bob', 'barker').then((bob) => { - bob.setPassword('meower'); - return bob.save(); - }).then(() => { - return Parse.User.logIn('bob', 'meower'); - }).then((bob) => { - expect(bob.getUsername()).toEqual('bob'); - done(); - }, (e) => { - console.log(e); - fail(); - }); + it('set password then change password', done => { + Parse.User.signUp('bob', 'barker') + .then(bob => { + bob.setPassword('meower'); + return bob.save(); + }) + .then(() => { + return Parse.User.logIn('bob', 'meower'); + }) + .then( + bob => { + expect(bob.getUsername()).toEqual('bob'); + done(); + }, + e => { + console.log(e); + fail(); + } + ); }); - it("authenticated check", async (done) => { + it('authenticated check', async done => { const user = new Parse.User(); - user.set("username", "darkhelmet"); - user.set("password", "onetwothreefour"); + user.set('username', 'darkhelmet'); + user.set('password', 'onetwothreefour'); ok(!user.authenticated()); await user.signUp(null); ok(user.authenticated()); done(); }); - it("log in with explicit facebook auth data", async (done) => { + it('log in with explicit facebook auth data', async done => { await Parse.FacebookUtils.logIn({ - id: "8675309", - access_token: "jenny", - expiration_date: new Date().toJSON() + id: '8675309', + access_token: 'jenny', + expiration_date: new Date().toJSON(), }); done(); }); - it("log in async with explicit facebook auth data", (done) => { + it('log in async with explicit facebook auth data', done => { Parse.FacebookUtils.logIn({ - id: "8675309", - access_token: "jenny", - expiration_date: new Date().toJSON() - }).then(function() { - done(); - }, function(error) { - ok(false, error); - done(); - }); + id: '8675309', + access_token: 'jenny', + expiration_date: new Date().toJSON(), + }).then( + function() { + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it("link with explicit facebook auth data", async (done) => { - const user = await Parse.User.signUp("mask", "open sesame"); + it('link with explicit facebook auth data', async done => { + const user = await Parse.User.signUp('mask', 'open sesame'); Parse.FacebookUtils.link(user, { - id: "8675309", - access_token: "jenny", - expiration_date: new Date().toJSON() - }).then(done, (error) => { + id: '8675309', + access_token: 'jenny', + expiration_date: new Date().toJSON(), + }).then(done, error => { jfail(error); done(); }); }); - it("link async with explicit facebook auth data", async (done) => { - const user = await Parse.User.signUp("mask", "open sesame"); + it('link async with explicit facebook auth data', async done => { + const user = await Parse.User.signUp('mask', 'open sesame'); Parse.FacebookUtils.link(user, { - id: "8675309", - access_token: "jenny", - expiration_date: new Date().toJSON() - }).then(function() { - done(); - }, function(error) { - ok(false, error); - done(); - }); + id: '8675309', + access_token: 'jenny', + expiration_date: new Date().toJSON(), + }).then( + function() { + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it("async methods", (done) => { - const data = { foo: "bar" }; - - Parse.User.signUp("finn", "human", data).then(function(user) { - equal(Parse.User.current(), user); - equal(user.get("foo"), "bar"); - return Parse.User.logOut(); - }).then(function() { - return Parse.User.logIn("finn", "human"); - }).then(function(user) { - equal(user, Parse.User.current()); - equal(user.get("foo"), "bar"); - return Parse.User.logOut(); - }).then(function() { - const user = new Parse.User(); - user.set("username", "jake"); - user.set("password", "dog"); - user.set("foo", "baz"); - return user.signUp(); - }).then(function(user) { - equal(user, Parse.User.current()); - equal(user.get("foo"), "baz"); - user = new Parse.User(); - user.set("username", "jake"); - user.set("password", "dog"); - return user.logIn(); - }).then(function(user) { - equal(user, Parse.User.current()); - equal(user.get("foo"), "baz"); - const userAgain = new Parse.User(); - userAgain.id = user.id; - return userAgain.fetch(); - }).then(function(userAgain) { - equal(userAgain.get("foo"), "baz"); - done(); - }); - }); + it('async methods', done => { + const data = { foo: 'bar' }; - it("querying for users doesn't get session tokens", (done) => { - Parse.User.signUp("finn", "human", { foo: "bar" }) + Parse.User.signUp('finn', 'human', data) + .then(function(user) { + equal(Parse.User.current(), user); + equal(user.get('foo'), 'bar'); + return Parse.User.logOut(); + }) .then(function() { + return Parse.User.logIn('finn', 'human'); + }) + .then(function(user) { + equal(user, Parse.User.current()); + equal(user.get('foo'), 'bar'); return Parse.User.logOut(); - }).then(() => { + }) + .then(function() { const user = new Parse.User(); - user.set("username", "jake"); - user.set("password", "dog"); - user.set("foo", "baz"); + user.set('username', 'jake'); + user.set('password', 'dog'); + user.set('foo', 'baz'); return user.signUp(); + }) + .then(function(user) { + equal(user, Parse.User.current()); + equal(user.get('foo'), 'baz'); + user = new Parse.User(); + user.set('username', 'jake'); + user.set('password', 'dog'); + return user.logIn(); + }) + .then(function(user) { + equal(user, Parse.User.current()); + equal(user.get('foo'), 'baz'); + const userAgain = new Parse.User(); + userAgain.id = user.id; + return userAgain.fetch(); + }) + .then(function(userAgain) { + equal(userAgain.get('foo'), 'baz'); + done(); + }); + }); - }).then(function() { + it("querying for users doesn't get session tokens", done => { + Parse.User.signUp('finn', 'human', { foo: 'bar' }) + .then(function() { return Parse.User.logOut(); - }).then(() => { + }) + .then(() => { + const user = new Parse.User(); + user.set('username', 'jake'); + user.set('password', 'dog'); + user.set('foo', 'baz'); + return user.signUp(); + }) + .then(function() { + return Parse.User.logOut(); + }) + .then(() => { const query = new Parse.Query(Parse.User); return query.find({ sessionToken: null }); - }).then(function(users) { - equal(users.length, 2); - users.forEach((user) => { - expect(user.getSessionToken()).toBeUndefined(); - ok(!user.getSessionToken(), "user should not have a session token."); - }); - done(); - }, function(error) { - ok(false, error); - done(); - }); + }) + .then( + function(users) { + equal(users.length, 2); + users.forEach(user => { + expect(user.getSessionToken()).toBeUndefined(); + ok( + !user.getSessionToken(), + 'user should not have a session token.' + ); + }); + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it("querying for users only gets the expected fields", (done) => { - Parse.User.signUp("finn", "human", { foo: "bar" }) - .then(() => { - request.get({ - headers: {'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest'}, + it('querying for users only gets the expected fields', done => { + Parse.User.signUp('finn', 'human', { foo: 'bar' }).then(() => { + request.get( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, url: 'http://localhost:8378/1/users', - }, (error, response, body) => { + }, + (error, response, body) => { expect(error).toBe(null); const b = JSON.parse(body); expect(b.results.length).toEqual(1); const user = b.results[0]; expect(Object.keys(user).length).toEqual(6); done(); - }); - }); + } + ); + }); }); - it('retrieve user data from fetch, make sure the session token hasn\'t changed', (done) => { + it("retrieve user data from fetch, make sure the session token hasn't changed", done => { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - let currentSessionToken = ""; - Promise.resolve().then(function() { - return user.signUp(); - }).then(function(){ - currentSessionToken = user.getSessionToken(); - return user.fetch(); - }).then(function(u){ - expect(currentSessionToken).toEqual(u.getSessionToken()); - done(); - }, function(error) { - ok(false, error); - done(); - }) + user.setPassword('asdf'); + user.setUsername('zxcv'); + let currentSessionToken = ''; + Promise.resolve() + .then(function() { + return user.signUp(); + }) + .then(function() { + currentSessionToken = user.getSessionToken(); + return user.fetch(); + }) + .then( + function(u) { + expect(currentSessionToken).toEqual(u.getSessionToken()); + done(); + }, + function(error) { + ok(false, error); + done(); + } + ); }); - it('user save should fail with invalid email', (done) => { + it('user save should fail with invalid email', done => { const user = new Parse.User(); user.set('username', 'teste'); user.set('password', 'test'); user.set('email', 'invalid'); - user.signUp().then(() => { - fail('Should not have been able to save.'); - done(); - }, (error) => { - expect(error.code).toEqual(125); - done(); - }); + user.signUp().then( + () => { + fail('Should not have been able to save.'); + done(); + }, + error => { + expect(error.code).toEqual(125); + done(); + } + ); }); - it('user signup should error if email taken', (done) => { + it('user signup should error if email taken', done => { const user = new Parse.User(); user.set('username', 'test1'); user.set('password', 'test'); user.set('email', 'test@test.com'); - user.signUp().then(() => { - const user2 = new Parse.User(); - user2.set('username', 'test2'); - user2.set('password', 'test'); - user2.set('email', 'test@test.com'); - return user2.signUp(); - }).then(() => { - fail('Should not have been able to sign up.'); - done(); - }, () => { - done(); - }); + user + .signUp() + .then(() => { + const user2 = new Parse.User(); + user2.set('username', 'test2'); + user2.set('password', 'test'); + user2.set('email', 'test@test.com'); + return user2.signUp(); + }) + .then( + () => { + fail('Should not have been able to sign up.'); + done(); + }, + () => { + done(); + } + ); }); - it('user cannot update email to existing user', (done) => { + it('user cannot update email to existing user', done => { const user = new Parse.User(); user.set('username', 'test1'); user.set('password', 'test'); user.set('email', 'test@test.com'); - user.signUp().then(() => { - const user2 = new Parse.User(); - user2.set('username', 'test2'); - user2.set('password', 'test'); - return user2.signUp(); - }).then((user2) => { - user2.set('email', 'test@test.com'); - return user2.save(); - }).then(() => { - fail('Should not have been able to sign up.'); - done(); - }, () => { - done(); - }); + user + .signUp() + .then(() => { + const user2 = new Parse.User(); + user2.set('username', 'test2'); + user2.set('password', 'test'); + return user2.signUp(); + }) + .then(user2 => { + user2.set('email', 'test@test.com'); + return user2.save(); + }) + .then( + () => { + fail('Should not have been able to sign up.'); + done(); + }, + () => { + done(); + } + ); }); - it('unset user email', (done) => { + it('unset user email', done => { const 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) => { - Promise.resolve().then(() => { - return Parse.User.signUp("finn", "human", { foo: "bar" }); - }).then((user) => { - request.post({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(typeof b.sessionToken).toEqual('string'); - expect(typeof b.createdWith).toEqual('object'); - expect(b.createdWith.action).toEqual('create'); - expect(typeof b.user).toEqual('object'); - expect(b.user.objectId).toEqual(user.id); - done(); - }); - }); - }); - - it('user get session from token on signup', (done) => { - Promise.resolve().then(() => { - return Parse.User.signUp("finn", "human", { foo: "bar" }); - }).then((user) => { - request.get({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions/me', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(typeof b.sessionToken).toEqual('string'); - expect(typeof b.createdWith).toEqual('object'); - expect(b.createdWith.action).toEqual('signup'); - expect(typeof b.user).toEqual('object'); - expect(b.user.objectId).toEqual(user.id); - done(); - }); - }); - }); - - it('user get session from token on login', (done) => { - Promise.resolve().then(() => { - return Parse.User.signUp("finn", "human", { foo: "bar" }); - }).then(() => { - return Parse.User.logOut().then(() => { - return Parse.User.logIn("finn", "human"); + user + .signUp() + .then(() => { + user.unset('email'); + return user.save(); }) - }).then((user) => { - request.get({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions/me', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(typeof b.sessionToken).toEqual('string'); - expect(typeof b.createdWith).toEqual('object'); - expect(b.createdWith.action).toEqual('login'); - expect(typeof b.user).toEqual('object'); - expect(b.user.objectId).toEqual(user.id); + .then(() => { + return Parse.User.logIn('test', 'test'); + }) + .then(user => { + expect(user.getEmail()).toBeUndefined(); done(); }); - }); - }); - - it('user update session with other field', (done) => { - Promise.resolve().then(() => { - return Parse.User.signUp("finn", "human", { foo: "bar" }); - }).then((user) => { - request.get({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions/me', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - request.put({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions/' + b.objectId, - body: JSON.stringify({ foo: 'bar' }) - }, (error, response, body) => { - expect(error).toBe(null); - JSON.parse(body); - done(); - }); - }); - }); }); - it('cannot update session if invalid or no session token', (done) => { - Promise.resolve().then(() => { - return Parse.User.signUp("finn", "human", { foo: "bar" }); - }).then((user) => { - request.get({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions/me', - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - request.put({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': 'foo', - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions/' + b.objectId, - body: JSON.stringify({ foo: 'bar' }) - }, (error, response, body) => { - expect(error).toBe(null); - const b = JSON.parse(body); - expect(b.error).toBe('Invalid session token'); - request.put({ + it('create session from user', done => { + Promise.resolve() + .then(() => { + return Parse.User.signUp('finn', 'human', { foo: 'bar' }); + }) + .then(user => { + request.post( + { headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', }, - url: 'http://localhost:8378/1/sessions/' + b.objectId, - body: JSON.stringify({ foo: 'bar' }) - }, (error, response, body) => { + url: 'http://localhost:8378/1/sessions', + }, + (error, response, body) => { expect(error).toBe(null); const b = JSON.parse(body); - expect(b.error).toBe('Session token required.'); + expect(typeof b.sessionToken).toEqual('string'); + expect(typeof b.createdWith).toEqual('object'); + expect(b.createdWith.action).toEqual('create'); + expect(typeof b.user).toEqual('object'); + expect(b.user.objectId).toEqual(user.id); done(); - }); - }); - }); - }); - }); - - it('get session only for current user', (done) => { - Promise.resolve().then(() => { - return Parse.User.signUp("test1", "test", { foo: "bar" }); - }).then(() => { - return Parse.User.signUp("test2", "test", { foo: "bar" }); - }).then((user) => { - request.get({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions' - }, (error, response, body) => { - expect(error).toBe(null); - try { - const b = JSON.parse(body); - expect(b.results.length).toEqual(1); - expect(typeof b.results[0].user).toEqual('object'); - expect(b.results[0].user.objectId).toEqual(user.id); - } catch(e) { - jfail(e); - } - done(); + } + ); }); - }); }); - it('delete session by object', (done) => { - Promise.resolve().then(() => { - return Parse.User.signUp("test1", "test", { foo: "bar" }); - }).then(() => { - return Parse.User.signUp("test2", "test", { foo: "bar" }); - }).then((user) => { - request.get({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions' - }, (error, response, body) => { - expect(error).toBe(null); - let objId; - try { - const b = JSON.parse(body); - expect(b.results.length).toEqual(1); - objId = b.results[0].objectId; - } catch(e) { - jfail(e); - done(); - return; - } - request.del({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions/' + objId - }, (error) => { - expect(error).toBe(null); - request.get({ + it('user get session from token on signup', done => { + Promise.resolve() + .then(() => { + return Parse.User.signUp('finn', 'human', { foo: 'bar' }); + }) + .then(user => { + request.get( + { headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }, - url: 'http://localhost:8378/1/sessions' - }, (error, response, body) => { + url: 'http://localhost:8378/1/sessions/me', + }, + (error, response, body) => { expect(error).toBe(null); const b = JSON.parse(body); - expect(b.code).toEqual(209); - expect(b.error).toBe('Invalid session token'); + expect(typeof b.sessionToken).toEqual('string'); + expect(typeof b.createdWith).toEqual('object'); + expect(b.createdWith.action).toEqual('signup'); + expect(typeof b.user).toEqual('object'); + expect(b.user.objectId).toEqual(user.id); done(); - }); - }); + } + ); }); - }); }); - it('cannot delete session if no sessionToken', (done) => { - Promise.resolve().then(() => { - return Parse.User.signUp("test1", "test", { foo: "bar" }); - }).then(() => { - return Parse.User.signUp("test2", "test", { foo: "bar" }); - }).then((user) => { - request.get({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Session-Token': user.getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions' - }, (error, response, body) => { - expect(error).toBe(null); - let objId; - try { - const b = JSON.parse(body); - expect(b.results.length).toEqual(1); - objId = b.results[0].objectId; - } catch(e) { - jfail(e); - done(); - return; - } - request.del({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' - }, - url: 'http://localhost:8378/1/sessions/' + objId - }, (error,response,body) => { - const b = JSON.parse(body); - expect(b.code).toEqual(209); - expect(b.error).toBe('Invalid session token'); - done(); + it('user get session from token on login', done => { + Promise.resolve() + .then(() => { + return Parse.User.signUp('finn', 'human', { foo: 'bar' }); + }) + .then(() => { + return Parse.User.logOut().then(() => { + return Parse.User.logIn('finn', 'human'); }); + }) + .then(user => { + request.get( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions/me', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(typeof b.sessionToken).toEqual('string'); + expect(typeof b.createdWith).toEqual('object'); + expect(b.createdWith.action).toEqual('login'); + expect(typeof b.user).toEqual('object'); + expect(b.user.objectId).toEqual(user.id); + done(); + } + ); + }); + }); + + it('user update session with other field', done => { + Promise.resolve() + .then(() => { + return Parse.User.signUp('finn', 'human', { foo: 'bar' }); + }) + .then(user => { + request.get( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions/me', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + request.put( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions/' + b.objectId, + body: JSON.stringify({ foo: 'bar' }), + }, + (error, response, body) => { + expect(error).toBe(null); + JSON.parse(body); + done(); + } + ); + } + ); + }); + }); + + it('cannot update session if invalid or no session token', done => { + Promise.resolve() + .then(() => { + return Parse.User.signUp('finn', 'human', { foo: 'bar' }); + }) + .then(user => { + request.get( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions/me', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + request.put( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': 'foo', + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions/' + b.objectId, + body: JSON.stringify({ foo: 'bar' }), + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(b.error).toBe('Invalid session token'); + request.put( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions/' + b.objectId, + body: JSON.stringify({ foo: 'bar' }), + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(b.error).toBe('Session token required.'); + done(); + } + ); + } + ); + } + ); + }); + }); + + it('get session only for current user', done => { + Promise.resolve() + .then(() => { + return Parse.User.signUp('test1', 'test', { foo: 'bar' }); + }) + .then(() => { + return Parse.User.signUp('test2', 'test', { foo: 'bar' }); + }) + .then(user => { + request.get( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions', + }, + (error, response, body) => { + expect(error).toBe(null); + try { + const b = JSON.parse(body); + expect(b.results.length).toEqual(1); + expect(typeof b.results[0].user).toEqual('object'); + expect(b.results[0].user.objectId).toEqual(user.id); + } catch (e) { + jfail(e); + } + done(); + } + ); + }); + }); + + it('delete session by object', done => { + Promise.resolve() + .then(() => { + return Parse.User.signUp('test1', 'test', { foo: 'bar' }); + }) + .then(() => { + return Parse.User.signUp('test2', 'test', { foo: 'bar' }); + }) + .then(user => { + request.get( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions', + }, + (error, response, body) => { + expect(error).toBe(null); + let objId; + try { + const b = JSON.parse(body); + expect(b.results.length).toEqual(1); + objId = b.results[0].objectId; + } catch (e) { + jfail(e); + done(); + return; + } + request.del( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions/' + objId, + }, + error => { + expect(error).toBe(null); + request.get( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions', + }, + (error, response, body) => { + expect(error).toBe(null); + const b = JSON.parse(body); + expect(b.code).toEqual(209); + expect(b.error).toBe('Invalid session token'); + done(); + } + ); + } + ); + } + ); + }); + }); + + it('cannot delete session if no sessionToken', done => { + Promise.resolve() + .then(() => { + return Parse.User.signUp('test1', 'test', { foo: 'bar' }); + }) + .then(() => { + return Parse.User.signUp('test2', 'test', { foo: 'bar' }); + }) + .then(user => { + request.get( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions', + }, + (error, response, body) => { + expect(error).toBe(null); + let objId; + try { + const b = JSON.parse(body); + expect(b.results.length).toEqual(1); + objId = b.results[0].objectId; + } catch (e) { + jfail(e); + done(); + return; + } + request.del( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/sessions/' + objId, + }, + (error, response, body) => { + const b = JSON.parse(body); + expect(b.code).toEqual(209); + expect(b.error).toBe('Invalid session token'); + done(); + } + ); + } + ); }); - }); }); - it('password format matches hosted parse', (done) => { - const hashed = '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie'; - passwordCrypto.compare('test', hashed) - .then((pass) => { + it('password format matches hosted parse', done => { + const hashed = + '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie'; + passwordCrypto.compare('test', hashed).then( + pass => { expect(pass).toBe(true); done(); - }, () => { + }, + () => { fail('Password format did not match.'); done(); - }); + } + ); }); - it('changing password clears sessions', (done) => { + it('changing password clears sessions', done => { let sessionToken = null; - Promise.resolve().then(function() { - return Parse.User.signUp("fosco", "parse"); - }).then(function(newUser) { - equal(Parse.User.current(), newUser); - sessionToken = newUser.getSessionToken(); - ok(sessionToken); - newUser.set('password', 'facebook'); - return newUser.save(); - }).then(function() { - return Parse.User.become(sessionToken); - }).then(function() { - fail('Session should have been invalidated'); - done(); - }, function(err) { - expect(err.code).toBe(Parse.Error.INVALID_SESSION_TOKEN); - expect(err.message).toBe('Invalid session token'); - done(); - }); + Promise.resolve() + .then(function() { + return Parse.User.signUp('fosco', 'parse'); + }) + .then(function(newUser) { + equal(Parse.User.current(), newUser); + sessionToken = newUser.getSessionToken(); + ok(sessionToken); + newUser.set('password', 'facebook'); + return newUser.save(); + }) + .then(function() { + return Parse.User.become(sessionToken); + }) + .then( + function() { + fail('Session should have been invalidated'); + done(); + }, + function(err) { + expect(err.code).toBe(Parse.Error.INVALID_SESSION_TOKEN); + expect(err.message).toBe('Invalid session token'); + done(); + } + ); }); - it('test parse user become', (done) => { + it('test parse user become', done => { let sessionToken = null; - Promise.resolve().then(function() { - return Parse.User.signUp("flessard", "folo",{'foo':1}); - }).then(function(newUser) { - equal(Parse.User.current(), newUser); - sessionToken = newUser.getSessionToken(); - ok(sessionToken); - newUser.set('foo',2); - return newUser.save(); - }).then(function() { - return Parse.User.become(sessionToken); - }).then(function(newUser) { - equal(newUser.get('foo'), 2); - done(); - }, function() { - fail('The session should still be valid'); - done(); - }); + Promise.resolve() + .then(function() { + return Parse.User.signUp('flessard', 'folo', { foo: 1 }); + }) + .then(function(newUser) { + equal(Parse.User.current(), newUser); + sessionToken = newUser.getSessionToken(); + ok(sessionToken); + newUser.set('foo', 2); + return newUser.save(); + }) + .then(function() { + return Parse.User.become(sessionToken); + }) + .then( + function(newUser) { + equal(newUser.get('foo'), 2); + done(); + }, + function() { + fail('The session should still be valid'); + done(); + } + ); }); - it('ensure logout works', (done) => { + it('ensure logout works', done => { let user = null; let sessionToken = null; - Promise.resolve().then(function() { - return Parse.User.signUp('log', 'out'); - }).then((newUser) => { - user = newUser; - sessionToken = user.getSessionToken(); - return Parse.User.logOut(); - }).then(() => { - user.set('foo', 'bar'); - return user.save(null, { sessionToken: sessionToken }); - }).then(() => { - fail('Save should have failed.'); - done(); - }, (e) => { - expect(e.code).toEqual(Parse.Error.INVALID_SESSION_TOKEN); - done(); - }); + Promise.resolve() + .then(function() { + return Parse.User.signUp('log', 'out'); + }) + .then(newUser => { + user = newUser; + sessionToken = user.getSessionToken(); + return Parse.User.logOut(); + }) + .then(() => { + user.set('foo', 'bar'); + return user.save(null, { sessionToken: sessionToken }); + }) + .then( + () => { + fail('Save should have failed.'); + done(); + }, + e => { + expect(e.code).toEqual(Parse.Error.INVALID_SESSION_TOKEN); + done(); + } + ); }); - it('support user/password signup with empty authData block', (done) => { + it('support user/password signup with empty authData block', done => { // The android SDK can send an empty authData object along with username and password. - Parse.User.signUp('artof', 'thedeal', { authData: {} }).then(() => { - done(); - }, () => { - fail('Signup should have succeeded.'); - done(); - }); + Parse.User.signUp('artof', 'thedeal', { authData: {} }).then( + () => { + done(); + }, + () => { + fail('Signup should have succeeded.'); + done(); + } + ); }); - it("session expiresAt correct format", async (done) => { - await Parse.User.signUp("asdf", "zxcv"); - request.get({ - url: 'http://localhost:8378/1/classes/_Session', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', + it('session expiresAt correct format', async done => { + await Parse.User.signUp('asdf', 'zxcv'); + request.get( + { + url: 'http://localhost:8378/1/classes/_Session', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, }, - }, (error, response, body) => { - expect(body.results[0].expiresAt.__type).toEqual('Date'); - done(); - }); + (error, response, body) => { + expect(body.results[0].expiresAt.__type).toEqual('Date'); + done(); + } + ); }); - it("Invalid session tokens are rejected", async (done) => { - await Parse.User.signUp("asdf", "zxcv"); - request.get({ - url: 'http://localhost:8378/1/classes/AClass', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Rest-API-Key': 'rest', - 'X-Parse-Session-Token': 'text' + it('Invalid session tokens are rejected', async done => { + await Parse.User.signUp('asdf', 'zxcv'); + request.get( + { + url: 'http://localhost:8378/1/classes/AClass', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Rest-API-Key': 'rest', + 'X-Parse-Session-Token': 'text', + }, }, - }, (error, response, body) => { - expect(body.code).toBe(209); - expect(body.error).toBe('Invalid session token'); - done(); - }); + (error, response, body) => { + expect(body.code).toBe(209); + expect(body.error).toBe('Invalid session token'); + done(); + } + ); }); - it_exclude_dbs(['postgres'])('should cleanup null authData keys (regression test for #935)', (done) => { - const database = Config.get(Parse.applicationId).database; - database.create('_User', { - username: 'user', - _hashed_password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie', - _auth_data_facebook: null - }, {}).then(() => { - return new Promise((resolve, reject) => { - request.get({ - url: 'http://localhost:8378/1/login?username=user&password=test', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', + it_exclude_dbs(['postgres'])( + 'should cleanup null authData keys (regression test for #935)', + done => { + const database = Config.get(Parse.applicationId).database; + database + .create( + '_User', + { + username: 'user', + _hashed_password: + '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie', + _auth_data_facebook: null, }, - json: true - }, (err, res, body) => { - if (err) { - reject(err); - } else { - resolve(body); - } + {} + ) + .then(() => { + return new Promise((resolve, reject) => { + request.get( + { + url: + 'http://localhost:8378/1/login?username=user&password=test', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + json: true, + }, + (err, res, body) => { + if (err) { + reject(err); + } else { + resolve(body); + } + } + ); + }); }) - }) - }).then((user) => { - const authData = user.authData; - expect(user.username).toEqual('user'); - expect(authData).toBeUndefined(); - done(); - }).catch(() => { - fail('this should not fail'); - done(); - }) - }); + .then(user => { + const authData = user.authData; + expect(user.username).toEqual('user'); + expect(authData).toBeUndefined(); + done(); + }) + .catch(() => { + fail('this should not fail'); + done(); + }); + } + ); - it_exclude_dbs(['postgres'])('should not serve null authData keys', (done) => { + it_exclude_dbs(['postgres'])('should not serve null authData keys', done => { const database = Config.get(Parse.applicationId).database; - database.create('_User', { - username: 'user', - _hashed_password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie', - _auth_data_facebook: null - }, {}).then(() => { - return new Parse.Query(Parse.User) - .equalTo('username', 'user') - .first({useMasterKey: true}); - }).then((user) => { - const authData = user.get('authData'); - expect(user.get('username')).toEqual('user'); - expect(authData).toBeUndefined(); - done(); - }).catch(() => { - fail('this should not fail'); - done(); - }) + database + .create( + '_User', + { + username: 'user', + _hashed_password: + '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie', + _auth_data_facebook: null, + }, + {} + ) + .then(() => { + return new Parse.Query(Parse.User) + .equalTo('username', 'user') + .first({ useMasterKey: true }); + }) + .then(user => { + const authData = user.get('authData'); + expect(user.get('username')).toEqual('user'); + expect(authData).toBeUndefined(); + done(); + }) + .catch(() => { + fail('this should not fail'); + done(); + }); }); - it('should cleanup null authData keys ParseUser update (regression test for #1198, #2252)', (done) => { - Parse.Cloud.beforeSave('_User', (req) => { + it('should cleanup null authData keys ParseUser update (regression test for #1198, #2252)', done => { + Parse.Cloud.beforeSave('_User', req => { req.object.set('foo', 'bar'); }); @@ -2347,142 +2707,165 @@ describe('Parse.User testing', () => { let originalUserId; // 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) => { - originalSessionToken = user.sessionToken; - originalUserId = user.objectId; - // Simulate registration - return new Promise((resolve, reject) => { - request.put({ - url: 'http://localhost:8378/1/classes/_User/' + user.objectId, + request.post( + { + url: 'http://localhost:8378/1/classes/_User', headers: { 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Session-Token': user.sessionToken, 'X-Parse-REST-API-Key': 'rest', }, json: { - authData: {anonymous: null}, - username: 'user', - password: 'password', - } - }, (err, res, body) => { - if (err) { - reject(err); - } else { - resolve(body); - } - }); - }); - }).then((user) => { - expect(typeof user).toEqual('object'); - expect(user.authData).toBeUndefined(); - expect(user.sessionToken).not.toBeUndefined(); - // Session token should have changed - expect(user.sessionToken).not.toEqual(originalSessionToken); - // test that the sessionToken is valid - return new Promise((resolve, reject) => { - request.get({ - url: 'http://localhost:8378/1/users/me', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Session-Token': user.sessionToken, - 'X-Parse-REST-API-Key': 'rest', + authData: { + anonymous: { id: '00000000-0000-0000-0000-000000000001' }, + }, }, - json: true - }, (err, res, body) => { - expect(body.username).toEqual('user'); - expect(body.objectId).toEqual(originalUserId); + }, + (err, res, body) => { if (err) { reject(err); } else { resolve(body); } - done(); + } + ); + }) + .then(user => { + originalSessionToken = user.sessionToken; + originalUserId = user.objectId; + // 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 }, + username: 'user', + password: 'password', + }, + }, + (err, res, body) => { + if (err) { + reject(err); + } else { + resolve(body); + } + } + ); + }); + }) + .then(user => { + expect(typeof user).toEqual('object'); + expect(user.authData).toBeUndefined(); + expect(user.sessionToken).not.toBeUndefined(); + // Session token should have changed + expect(user.sessionToken).not.toEqual(originalSessionToken); + // test that the sessionToken is valid + return new Promise((resolve, reject) => { + request.get( + { + url: 'http://localhost:8378/1/users/me', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Session-Token': user.sessionToken, + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + }, + (err, res, body) => { + expect(body.username).toEqual('user'); + expect(body.objectId).toEqual(originalUserId); + if (err) { + reject(err); + } else { + resolve(body); + } + done(); + } + ); }); + }) + .catch(err => { + fail('no request should fail: ' + JSON.stringify(err)); + done(); }); - }).catch((err) => { - fail('no request should fail: ' + JSON.stringify(err)); - done(); - }); }); - it('should send email when upgrading from anon', (done) => { - + it('should send email when upgrading from anon', done => { let emailCalled = false; let emailOptions; const emailAdapter = { - sendVerificationEmail: (options) => { + sendVerificationEmail: options => { emailOptions = options; emailCalled = true; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }) + publicServerURL: 'http://localhost:8378/1', + }); // Simulate anonymous user save - return rp.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'}}} - }).then((user) => { - return rp.put({ - url: 'http://localhost:8378/1/classes/_User/' + user.objectId, + return rp + .post({ + url: 'http://localhost:8378/1/classes/_User', headers: { 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Session-Token': user.sessionToken, 'X-Parse-REST-API-Key': 'rest', }, json: { - authData: {anonymous: null}, - username: 'user', - email: 'user@email.com', - password: 'password', - } + authData: { + anonymous: { id: '00000000-0000-0000-0000-000000000001' }, + }, + }, + }) + .then(user => { + return rp.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 }, + username: 'user', + email: 'user@email.com', + password: 'password', + }, + }); + }) + .then(() => { + expect(emailCalled).toBe(true); + expect(emailOptions).not.toBeUndefined(); + expect(emailOptions.user.get('email')).toEqual('user@email.com'); + done(); + }) + .catch(err => { + jfail(err); + fail('no request should fail: ' + JSON.stringify(err)); + done(); }); - }).then(() => { - expect(emailCalled).toBe(true); - expect(emailOptions).not.toBeUndefined(); - expect(emailOptions.user.get('email')).toEqual('user@email.com'); - done(); - }).catch((err) => { - jfail(err); - fail('no request should fail: ' + JSON.stringify(err)); - done(); - }); }); - it('should not send email when email is not a string', async (done) => { + it('should not send email when email is not a string', async done => { let emailCalled = false; let emailOptions; const emailAdapter = { - sendVerificationEmail: (options) => { + sendVerificationEmail: options => { emailOptions = options; emailCalled = true; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; reconfigureServer({ appName: 'unused', verifyUserEmails: true, @@ -2502,164 +2885,199 @@ describe('Parse.User testing', () => { 'X-Parse-REST-API-Key': 'rest', }, json: { - email: {"$regex":"^asd"}, - } - }).then((res) => { - fail('no request should succeed: ' + JSON.stringify(res)); - done(); - }).catch((err) => { - expect(emailCalled).toBeTruthy(); - expect(emailOptions).toBeDefined(); - expect(err.statusCode).toBe(400); - expect(err.message).toMatch('{"code":125,"error":"you must provide a valid email string"}'); - done(); - }); + email: { $regex: '^asd' }, + }, + }) + .then(res => { + fail('no request should succeed: ' + JSON.stringify(res)); + done(); + }) + .catch(err => { + expect(emailCalled).toBeTruthy(); + expect(emailOptions).toBeDefined(); + expect(err.statusCode).toBe(400); + expect(err.message).toMatch( + '{"code":125,"error":"you must provide a valid email string"}' + ); + done(); + }); }); - - it('should aftersave with full object', (done) => { + it('should aftersave with full object', done => { let hit = 0; Parse.Cloud.afterSave('_User', (req, res) => { hit++; expect(req.object.get('username')).toEqual('User'); res.success(); }); - const user = new Parse.User() + const user = new Parse.User(); user.setUsername('User'); user.setPassword('pass'); - user.signUp().then(()=> { - user.set('hello', 'world'); - return user.save(); - }).then(() => { - expect(hit).toBe(2); - done(); - }); + user + .signUp() + .then(() => { + user.set('hello', 'world'); + return user.save(); + }) + .then(() => { + expect(hit).toBe(2); + done(); + }); }); - it('changes to a user should update the cache', (done) => { - Parse.Cloud.define('testUpdatedUser', (req) => { + it('changes to a user should update the cache', done => { + Parse.Cloud.define('testUpdatedUser', req => { expect(req.user.get('han')).toEqual('solo'); return {}; }); const user = new Parse.User(); user.setUsername('harrison'); user.setPassword('ford'); - user.signUp().then(() => { - user.set('han', 'solo'); - return user.save(); - }).then(() => { - return Parse.Cloud.run('testUpdatedUser'); - }).then(() => { - done(); - }, () => { - fail('Should not have failed.'); - done(); - }); - + user + .signUp() + .then(() => { + user.set('han', 'solo'); + return user.save(); + }) + .then(() => { + return Parse.Cloud.run('testUpdatedUser'); + }) + .then( + () => { + done(); + }, + () => { + fail('Should not have failed.'); + done(); + } + ); }); - it('should fail to become user with expired token', (done) => { + it('should fail to become user with expired token', done => { let token; - Parse.User.signUp("auser", "somepass", null) - .then(() => rp({ - method: 'GET', - url: 'http://localhost:8378/1/classes/_Session', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - })) + Parse.User.signUp('auser', 'somepass', null) + .then(() => + 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 => { const id = body.results[0].objectId; - const expiresAt = new Date((new Date()).setYear(2015)); + const expiresAt = new Date(new Date().setYear(2015)); token = body.results[0].sessionToken; return rp({ method: 'PUT', - url: "http://localhost:8378/1/classes/_Session/" + id, + 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() }, + 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(); - }) + .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) => { + it('should not create extraneous session tokens', done => { const config = Config.get(Parse.applicationId); - config.database.loadSchema().then((s) => { - // Lock down the _User class for creation - return s.addClassIfNotExists('_User', {}, {create: {}}) - }).then(() => { - const user = new Parse.User(); - return user.save({'username': 'user', 'password': 'pass'}); - }).then(() => { - fail('should not be able to save the user'); - }, () => { - return Promise.resolve(); - }).then(() => { - const q = new Parse.Query('_Session'); - return q.find({useMasterKey: true}) - }).then((res) => { - // We should have no session created - expect(res.length).toBe(0); - done(); - }, () => { - fail('should not fail'); - done(); - }); + config.database + .loadSchema() + .then(s => { + // Lock down the _User class for creation + return s.addClassIfNotExists('_User', {}, { create: {} }); + }) + .then(() => { + const user = new Parse.User(); + return user.save({ username: 'user', password: 'pass' }); + }) + .then( + () => { + fail('should not be able to save the user'); + }, + () => { + return Promise.resolve(); + } + ) + .then(() => { + const q = new Parse.Query('_Session'); + return q.find({ useMasterKey: true }); + }) + .then( + res => { + // We should have no session created + expect(res.length).toBe(0); + done(); + }, + () => { + fail('should not fail'); + done(); + } + ); }); - it('should not overwrite username when unlinking facebook user (regression test for #1532)', async (done) => { + it('should not overwrite username when unlinking facebook user (regression test for #1532)', async done => { Parse.Object.disableSingleInstance(); const provider = getMockFacebookProvider(); Parse.User._registerAuthenticationProvider(provider); let user = new Parse.User(); - user.set("username", "testLinkWithProvider"); - user.set("password", "mypass"); + user.set('username', 'testLinkWithProvider'); + user.set('password', 'mypass'); await user.signUp(); - await user._linkWith("facebook"); + await user._linkWith('facebook'); expect(user.get('username')).toEqual('testLinkWithProvider'); expect(Parse.FacebookUtils.isLinked(user)).toBeTruthy(); - await user._unlinkFrom('facebook') - user = await user.fetch() + await user._unlinkFrom('facebook'); + user = await user.fetch(); expect(user.get('username')).toEqual('testLinkWithProvider'); expect(Parse.FacebookUtils.isLinked(user)).toBeFalsy(); done(); }); it('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', + 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' }, + }, + }, }, - json: {authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}} - }, (err, res, body) => { - Parse.User.become(body.sessionToken) - .then(user => { + (err, res, body) => { + Parse.User.become(body.sessionToken).then(user => { const obj = new Parse.Object('TestObject'); obj.setACL(new Parse.ACL(user)); - return obj.save() + return obj + .save() .then(() => { // Change password, revoking session user.set('username', 'no longer anonymous'); user.set('password', 'password'); - return user.save() + return user.save(); }) .then(() => { // Session token should have been recycled @@ -2670,262 +3088,310 @@ describe('Parse.User testing', () => { done(); }) .catch(() => { - fail('should not fail') + fail('should not fail'); done(); }); - }) - }); + }); + } + ); }); it('should not revoke session tokens if the server is configures to not revoke session tokens', done => { - reconfigureServer({ revokeSessionOnPasswordReset: false }) - .then(() => { - request.post({ + 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 => { - const obj = new Parse.Object('TestObject'); - obj.setACL(new Parse.ACL(user)); - return obj.save() + json: { + authData: { + anonymous: { id: '00000000-0000-0000-0000-000000000001' }, + }, + }, + }, + (err, res, body) => { + Parse.User.become(body.sessionToken).then(user => { + const 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() + return user.save(); }) .then(() => obj.fetch()) - // fetch should succeed as we still have our session token - .then(done, fail); - }) - }); - }); + // fetch should succeed as we still have our session token + .then(done, fail) + ); + }); + } + ); + }); }); it('should not fail querying non existing relations', done => { const user = new Parse.User(); user.set({ username: 'hello', - password: 'world' - }) - user.signUp().then(() => { - return Parse.User.current().relation('relation').query().find(); - }).then((res) => { - expect(res.length).toBe(0); - done(); - }).catch((err) => { - fail(JSON.stringify(err)); - done(); + password: 'world', }); + user + .signUp() + .then(() => { + return Parse.User.current() + .relation('relation') + .query() + .find(); + }) + .then(res => { + expect(res.length).toBe(0); + done(); + }) + .catch(err => { + fail(JSON.stringify(err)); + done(); + }); }); it('should not allow updates to emailVerified', done => { const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; const user = new Parse.User(); user.set({ username: 'hello', password: 'world', - email: "test@email.com" - }) + email: 'test@email.com', + }); reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - return user.signUp(); - }).then(() => { - return Parse.User.current().set('emailVerified', true).save(); - }).then(() => { - fail("Should not be able to update emailVerified"); - done(); - }).catch((err) => { - expect(err.message).toBe("Clients aren't allowed to manually update email verification."); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + return user.signUp(); + }) + .then(() => { + return Parse.User.current() + .set('emailVerified', true) + .save(); + }) + .then(() => { + fail('Should not be able to update emailVerified'); + done(); + }) + .catch(err => { + expect(err.message).toBe( + "Clients aren't allowed to manually update email verification." + ); + done(); + }); }); it('should not retrieve hidden fields on GET users/me (#3432)', done => { - const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; const user = new Parse.User(); user.set({ username: 'hello', password: 'world', - email: "test@email.com" - }) + email: 'test@email.com', + }); reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - return user.signUp(); - }).then(() => rp({ - method: 'GET', - url: 'http://localhost:8378/1/users/me', - json: true, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Session-Token': Parse.User.current().getSessionToken(), - 'X-Parse-REST-API-Key': 'rest' - }, - })).then((res) => { - expect(res.emailVerified).toBe(false); - expect(res._email_verify_token).toBeUndefined(); - done() - }).catch((err) => { - fail(JSON.stringify(err)); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + return user.signUp(); + }) + .then(() => + rp({ + method: 'GET', + url: 'http://localhost:8378/1/users/me', + json: true, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Session-Token': Parse.User.current().getSessionToken(), + 'X-Parse-REST-API-Key': 'rest', + }, + }) + ) + .then(res => { + expect(res.emailVerified).toBe(false); + expect(res._email_verify_token).toBeUndefined(); + done(); + }) + .catch(err => { + fail(JSON.stringify(err)); + done(); + }); }); it('should not retrieve hidden fields on GET users/id (#3432)', done => { - const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; const user = new Parse.User(); user.set({ username: 'hello', password: 'world', - email: "test@email.com" - }) + email: 'test@email.com', + }); reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - return user.signUp(); - }).then(() => rp({ - method: 'GET', - url: 'http://localhost:8378/1/users/' + Parse.User.current().id, - json: true, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' - }, - })).then((res) => { - expect(res.emailVerified).toBe(false); - expect(res._email_verify_token).toBeUndefined(); - done() - }).catch((err) => { - fail(JSON.stringify(err)); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + return user.signUp(); + }) + .then(() => + rp({ + method: 'GET', + url: 'http://localhost:8378/1/users/' + Parse.User.current().id, + json: true, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + }) + ) + .then(res => { + expect(res.emailVerified).toBe(false); + expect(res._email_verify_token).toBeUndefined(); + done(); + }) + .catch(err => { + fail(JSON.stringify(err)); + done(); + }); }); it('should not retrieve hidden fields on login (#3432)', done => { - const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; const user = new Parse.User(); user.set({ username: 'hello', password: 'world', - email: "test@email.com" - }) + email: 'test@email.com', + }); reconfigureServer({ appName: 'unused', verifyUserEmails: true, - emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - return user.signUp(); - }).then(() => rp.get({ - url: 'http://localhost:8378/1/login?email=test@email.com&username=hello&password=world', - json: true, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' - }, - })).then((res) => { - expect(res.emailVerified).toBe(false); - expect(res._email_verify_token).toBeUndefined(); - done(); - }).catch((err) => { - fail(JSON.stringify(err)); - done(); - }); + emailAdapter: emailAdapter, + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + return user.signUp(); + }) + .then(() => + rp.get({ + url: + 'http://localhost:8378/1/login?email=test@email.com&username=hello&password=world', + json: true, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + }) + ) + .then(res => { + expect(res.emailVerified).toBe(false); + expect(res._email_verify_token).toBeUndefined(); + done(); + }) + .catch(err => { + fail(JSON.stringify(err)); + done(); + }); }); it('should not allow updates to hidden fields', done => { const emailAdapter = { sendVerificationEmail: () => {}, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; const user = new Parse.User(); user.set({ username: 'hello', password: 'world', - email: "test@email.com" - }) + email: 'test@email.com', + }); reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - return user.signUp(); - }).then(() => { - return Parse.User.current().set('_email_verify_token', 'bad').save(); - }).then(() => { - fail("Should not be able to update email verification token"); - done(); - }).catch((err) => { - expect(err).toBeDefined(); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + return user.signUp(); + }) + .then(() => { + return Parse.User.current() + .set('_email_verify_token', 'bad') + .save(); + }) + .then(() => { + fail('Should not be able to update email verification token'); + done(); + }) + .catch(err => { + expect(err).toBeDefined(); + done(); + }); }); - it('should revoke sessions when setting paswword with masterKey (#3289)', (done) => { + it('should revoke sessions when setting paswword with masterKey (#3289)', done => { let user; Parse.User.signUp('username', 'password') - .then((newUser) => { + .then(newUser => { user = newUser; user.set('password', 'newPassword'); - return user.save(null, {useMasterKey: true}); - }).then(() => { + return user.save(null, { useMasterKey: true }); + }) + .then(() => { const query = new Parse.Query('_Session'); query.equalTo('user', user); - return query.find({useMasterKey: true}); - }).then((results) => { + return query.find({ useMasterKey: true }); + }) + .then(results => { expect(results.length).toBe(0); done(); }, done.fail); }); - it('should not send a verification email if the user signed up using oauth', (done) => { + it('should not send a verification email if the user signed up using oauth', done => { let emailCalledCount = 0; const emailAdapter = { sendVerificationEmail: () => { @@ -2933,21 +3399,21 @@ describe('Parse.User testing', () => { return Promise.resolve(); }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }); const user = new Parse.User(); user.set('email', 'email1@host.com'); Parse.FacebookUtils.link(user, { - id: "8675309", - access_token: "jenny", - expiration_date: new Date().toJSON() - }).then((user) => { + id: '8675309', + access_token: 'jenny', + expiration_date: new Date().toJSON(), + }).then(user => { user.set('email', 'email2@host.com'); user.save().then(() => { expect(emailCalledCount).toBe(0); @@ -2956,20 +3422,22 @@ describe('Parse.User testing', () => { }); }); - it('should be able to update user with authData passed', (done) => { + it('should be able to update user with authData passed', done => { let objectId; let sessionToken; function validate(block) { - return rp.get({ - url: `http://localhost:8378/1/classes/_User/${objectId}`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Session-Token': sessionToken - }, - json: true, - }).then(block); + return rp + .get({ + url: `http://localhost:8378/1/classes/_User/${objectId}`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + }, + json: true, + }) + .then(block); } rp.post({ @@ -2978,317 +3446,390 @@ describe('Parse.User testing', () => { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-REST-API-Key': 'rest', }, - json: { key: "value", authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}} - }).then((body) => { - objectId = body.objectId; - sessionToken = body.sessionToken; - expect(sessionToken).toBeDefined(); - expect(objectId).toBeDefined(); - return validate((user) => { // validate that keys are set on creation - expect(user.key).toBe("value"); - }); - }).then(() => { - // update the user - const options = { - url: `http://localhost:8378/1/classes/_User/${objectId}`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Session-Token': sessionToken - }, - json: { key: "otherValue", authData: {anonymous: {id: '00000000-0000-0000-0000-000000000001'}}} - } - return rp.put(options); - }).then(() => { - return validate((user) => { // validate that keys are set on update - expect(user.key).toBe("otherValue"); - }); - }).then(() => { - done(); + json: { + key: 'value', + authData: { anonymous: { id: '00000000-0000-0000-0000-000000000001' } }, + }, }) + .then(body => { + objectId = body.objectId; + sessionToken = body.sessionToken; + expect(sessionToken).toBeDefined(); + expect(objectId).toBeDefined(); + return validate(user => { + // validate that keys are set on creation + expect(user.key).toBe('value'); + }); + }) + .then(() => { + // update the user + const options = { + url: `http://localhost:8378/1/classes/_User/${objectId}`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + }, + json: { + key: 'otherValue', + authData: { + anonymous: { id: '00000000-0000-0000-0000-000000000001' }, + }, + }, + }; + return rp.put(options); + }) + .then(() => { + return validate(user => { + // validate that keys are set on update + expect(user.key).toBe('otherValue'); + }); + }) + .then(() => { + done(); + }) .then(done) .catch(done.fail); }); - it('can login with email', (done) => { + it('can login with email', done => { const user = new Parse.User(); - user.save({ - username: 'yolo', - password: 'yolopass', - email: 'yo@lo.com' - }).then(() => { - const options = { - url: `http://localhost:8378/1/login`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: { email: "yo@lo.com", password: 'yolopass'} - } - return rp.get(options); - }).then(done).catch(done.fail); + user + .save({ + username: 'yolo', + password: 'yolopass', + email: 'yo@lo.com', + }) + .then(() => { + const options = { + url: `http://localhost:8378/1/login`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: { email: 'yo@lo.com', password: 'yolopass' }, + }; + return rp.get(options); + }) + .then(done) + .catch(done.fail); }); - it('cannot login with email and invalid password', (done) => { + it('cannot login with email and invalid password', done => { const user = new Parse.User(); - user.save({ - username: 'yolo', - password: 'yolopass', - email: 'yo@lo.com' - }).then(() => { - const options = { - url: `http://localhost:8378/1/login`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: { email: "yo@lo.com", password: 'yolopass2'} - } - return rp.get(options); - }).then(done.fail).catch(() => done()); + user + .save({ + username: 'yolo', + password: 'yolopass', + email: 'yo@lo.com', + }) + .then(() => { + const options = { + url: `http://localhost:8378/1/login`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: { email: 'yo@lo.com', password: 'yolopass2' }, + }; + return rp.get(options); + }) + .then(done.fail) + .catch(() => done()); }); - it('can login with email through query string', (done) => { + it('can login with email through query string', done => { const user = new Parse.User(); - user.save({ - username: 'yolo', - password: 'yolopass', - email: 'yo@lo.com' - }).then(() => { - const options = { - url: `http://localhost:8378/1/login?email=yo@lo.com&password=yolopass`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - } - return rp.get(options); - }).then(done).catch(done.fail); + user + .save({ + username: 'yolo', + password: 'yolopass', + email: 'yo@lo.com', + }) + .then(() => { + const options = { + url: `http://localhost:8378/1/login?email=yo@lo.com&password=yolopass`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + }; + return rp.get(options); + }) + .then(done) + .catch(done.fail); }); - it('can login when both email and username are passed', (done) => { + it('can login when both email and username are passed', done => { const user = new Parse.User(); - user.save({ - username: 'yolo', - password: 'yolopass', - email: 'yo@lo.com' - }).then(() => { - const options = { - url: `http://localhost:8378/1/login?email=yo@lo.com&username=yolo&password=yolopass`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - } - return rp.get(options); - }).then(done).catch(done.fail); + user + .save({ + username: 'yolo', + password: 'yolopass', + email: 'yo@lo.com', + }) + .then(() => { + const options = { + url: `http://localhost:8378/1/login?email=yo@lo.com&username=yolo&password=yolopass`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + }; + return rp.get(options); + }) + .then(done) + .catch(done.fail); }); - it("fails to login when username doesn't match email", (done) => { + it("fails to login when username doesn't match email", done => { const user = new Parse.User(); - user.save({ - username: 'yolo', - password: 'yolopass', - email: 'yo@lo.com' - }).then(() => { - const options = { - url: `http://localhost:8378/1/login?email=yo@lo.com&username=yolo2&password=yolopass`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: true, - } - return rp.get(options); - }).then(done.fail).catch((err) => { - expect(err.response.body.error).toEqual('Invalid username/password.'); - done(); - }); + user + .save({ + username: 'yolo', + password: 'yolopass', + email: 'yo@lo.com', + }) + .then(() => { + const options = { + url: `http://localhost:8378/1/login?email=yo@lo.com&username=yolo2&password=yolopass`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + }; + return rp.get(options); + }) + .then(done.fail) + .catch(err => { + expect(err.response.body.error).toEqual('Invalid username/password.'); + done(); + }); }); - it("fails to login when email doesn't match username", (done) => { + it("fails to login when email doesn't match username", done => { const user = new Parse.User(); - user.save({ - username: 'yolo', - password: 'yolopass', - email: 'yo@lo.com' - }).then(() => { - const options = { - url: `http://localhost:8378/1/login?email=yo@lo2.com&username=yolo&password=yolopass`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: true, - } - return rp.get(options); - }).then(done.fail).catch((err) => { - expect(err.response.body.error).toEqual('Invalid username/password.'); - done(); - }); + user + .save({ + username: 'yolo', + password: 'yolopass', + email: 'yo@lo.com', + }) + .then(() => { + const options = { + url: `http://localhost:8378/1/login?email=yo@lo2.com&username=yolo&password=yolopass`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + }; + return rp.get(options); + }) + .then(done.fail) + .catch(err => { + expect(err.response.body.error).toEqual('Invalid username/password.'); + done(); + }); }); - it('fails to login when email and username are not provided', (done) => { + it('fails to login when email and username are not provided', done => { const user = new Parse.User(); - user.save({ - username: 'yolo', - password: 'yolopass', - email: 'yo@lo.com' - }).then(() => { - const options = { - url: `http://localhost:8378/1/login?password=yolopass`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: true, - } - return rp.get(options); - }).then(done.fail).catch((err) => { - expect(err.response.body.error).toEqual('username/email is required.'); - done(); - }); + user + .save({ + username: 'yolo', + password: 'yolopass', + email: 'yo@lo.com', + }) + .then(() => { + const options = { + url: `http://localhost:8378/1/login?password=yolopass`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + }; + return rp.get(options); + }) + .then(done.fail) + .catch(err => { + expect(err.response.body.error).toEqual('username/email is required.'); + done(); + }); }); - it('allows login when providing email as username', (done) => { + it('allows login when providing email as username', done => { const user = new Parse.User(); - user.save({ - username: 'yolo', - password: 'yolopass', - email: 'yo@lo.com' - }).then(() => { - return Parse.User.logIn('yo@lo.com', 'yolopass'); - }).then((user) => { - expect(user.get('username')).toBe('yolo'); - }).then(done).catch(done.fail); + user + .save({ + username: 'yolo', + password: 'yolopass', + email: 'yo@lo.com', + }) + .then(() => { + return Parse.User.logIn('yo@lo.com', 'yolopass'); + }) + .then(user => { + expect(user.get('username')).toBe('yolo'); + }) + .then(done) + .catch(done.fail); }); - it('handles properly when 2 users share username / email pairs', (done) => { + it('handles properly when 2 users share username / email pairs', done => { const user = new Parse.User({ username: 'yo@loname.com', password: 'yolopass', - email: 'yo@lo.com' + email: 'yo@lo.com', }); const user2 = new Parse.User({ username: 'yo@lo.com', email: 'yo@loname.com', - password: 'yolopass2' // different passwords + password: 'yolopass2', // different passwords }); - Parse.Object.saveAll([user, user2]).then(() => { - return Parse.User.logIn('yo@loname.com', 'yolopass'); - }).then((user) => { - // the username takes precedence over the email, - // so we get the user with username as passed in - expect(user.get('username')).toBe('yo@loname.com'); - }).then(done).catch(done.fail); + Parse.Object.saveAll([user, user2]) + .then(() => { + return Parse.User.logIn('yo@loname.com', 'yolopass'); + }) + .then(user => { + // the username takes precedence over the email, + // so we get the user with username as passed in + expect(user.get('username')).toBe('yo@loname.com'); + }) + .then(done) + .catch(done.fail); }); - it('handles properly when 2 users share username / email pairs, counterpart', (done) => { + it('handles properly when 2 users share username / email pairs, counterpart', done => { const user = new Parse.User({ username: 'yo@loname.com', password: 'yolopass', - email: 'yo@lo.com' + email: 'yo@lo.com', }); const user2 = new Parse.User({ username: 'yo@lo.com', email: 'yo@loname.com', - password: 'yolopass2' // different passwords + password: 'yolopass2', // different passwords }); - Parse.Object.saveAll([user, user2]).then(() => { - return Parse.User.logIn('yo@loname.com', 'yolopass2'); - }).then(done.fail).catch((err) => { - expect(err.message).toEqual('Invalid username/password.'); - done(); - }); + Parse.Object.saveAll([user, user2]) + .then(() => { + return Parse.User.logIn('yo@loname.com', 'yolopass2'); + }) + .then(done.fail) + .catch(err => { + expect(err.message).toEqual('Invalid username/password.'); + done(); + }); }); - it('fails to login when password is not provided', (done) => { + it('fails to login when password is not provided', done => { const user = new Parse.User(); - user.save({ - username: 'yolo', - password: 'yolopass', - email: 'yo@lo.com' - }).then(() => { - const options = { - url: `http://localhost:8378/1/login?username=yolo`, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest', - }, - json: true, - } - return rp.get(options); - }).then(done.fail).catch((err) => { - expect(err.response.body.error).toEqual('password is required.'); - done(); - }); + user + .save({ + username: 'yolo', + password: 'yolopass', + email: 'yo@lo.com', + }) + .then(() => { + const options = { + url: `http://localhost:8378/1/login?username=yolo`, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + json: true, + }; + return rp.get(options); + }) + .then(done.fail) + .catch(err => { + expect(err.response.body.error).toEqual('password is required.'); + done(); + }); }); - it('does not duplicate session when logging in multiple times #3451', (done) => { + it('does not duplicate session when logging in multiple times #3451', done => { const user = new Parse.User(); - user.signUp({ - username: 'yolo', - password: 'yolo', - email: 'yo@lo.com' - }).then(() => { - const token = user.getSessionToken(); - let promise = Promise.resolve(); - let count = 0; - while(count < 5) { - promise = promise.then(() => { - return Parse.User.logIn('yolo', 'yolo').then((res) => { - // ensure a new session token is generated at each login - expect(res.getSessionToken()).not.toBe(token); + user + .signUp({ + username: 'yolo', + password: 'yolo', + email: 'yo@lo.com', + }) + .then(() => { + const token = user.getSessionToken(); + let promise = Promise.resolve(); + let count = 0; + while (count < 5) { + promise = promise.then(() => { + return Parse.User.logIn('yolo', 'yolo').then(res => { + // ensure a new session token is generated at each login + expect(res.getSessionToken()).not.toBe(token); + }); }); + count++; + } + return promise; + }) + .then(() => { + // wait because session destruction is not synchronous + return new Promise(resolve => { + setTimeout(resolve, 100); }); - count++; - } - return promise; - }).then(() => { - // wait because session destruction is not synchronous - return new Promise((resolve) => { - setTimeout(resolve, 100); - }); - }).then(() => { - const query = new Parse.Query('_Session'); - return query.find({ useMasterKey: true }); - }).then((results) => { - // only one session in the end - expect(results.length).toBe(1); - }).then(done, done.fail); + }) + .then(() => { + const query = new Parse.Query('_Session'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + // only one session in the end + expect(results.length).toBe(1); + }) + .then(done, done.fail); }); describe('issue #4897', () => { - it_only_db('mongo')("should be able to login with a legacy user (no ACL)", async () => { - // This issue is a side effect of the locked users and legacy users which don't have ACL's - // In this scenario, a legacy user wasn't be able to login as there's no ACL on it - const database = Config.get(Parse.applicationId).database; - const collection = await database.adapter._adaptiveCollection('_User'); - await collection.insertOne({ - "_id": "ABCDEF1234", - "name": "", - "email": "", - "username": "", - "_hashed_password": "", - "_auth_data_facebook": { - "id": "8675309", - "access_token": "jenny" - }, - "sessionToken": "", - }); - const provider = getMockFacebookProvider(); - Parse.User._registerAuthenticationProvider(provider); - const model = await Parse.User._logInWith("facebook", {}); - expect(model.id).toBe('ABCDEF1234'); - ok(model instanceof Parse.User, "Model should be a Parse.User"); - strictEqual(Parse.User.current(), model); - ok(model.extended(), "Should have used subclass."); - strictEqual(provider.authData.id, provider.synchronizedUserId); - strictEqual(provider.authData.access_token, provider.synchronizedAuthToken); - strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration); - ok(model._isLinked("facebook"), "User should be linked to facebook"); - }); + it_only_db('mongo')( + 'should be able to login with a legacy user (no ACL)', + async () => { + // This issue is a side effect of the locked users and legacy users which don't have ACL's + // In this scenario, a legacy user wasn't be able to login as there's no ACL on it + const database = Config.get(Parse.applicationId).database; + const collection = await database.adapter._adaptiveCollection('_User'); + await collection.insertOne({ + _id: 'ABCDEF1234', + name: '', + email: '', + username: '', + _hashed_password: '', + _auth_data_facebook: { + id: '8675309', + access_token: 'jenny', + }, + sessionToken: '', + }); + const provider = getMockFacebookProvider(); + Parse.User._registerAuthenticationProvider(provider); + const model = await Parse.User._logInWith('facebook', {}); + expect(model.id).toBe('ABCDEF1234'); + ok(model instanceof Parse.User, 'Model should be a Parse.User'); + strictEqual(Parse.User.current(), model); + ok(model.extended(), 'Should have used subclass.'); + strictEqual(provider.authData.id, provider.synchronizedUserId); + strictEqual( + provider.authData.access_token, + provider.synchronizedAuthToken + ); + strictEqual( + provider.authData.expiration_date, + provider.synchronizedExpiration + ); + ok(model._isLinked('facebook'), 'User should be linked to facebook'); + } + ); }); }); diff --git a/spec/ParseWebSocket.spec.js b/spec/ParseWebSocket.spec.js index b72dd28434..baf232c1ee 100644 --- a/spec/ParseWebSocket.spec.js +++ b/spec/ParseWebSocket.spec.js @@ -1,7 +1,7 @@ -const ParseWebSocket = require('../lib/LiveQuery/ParseWebSocketServer').ParseWebSocket; +const ParseWebSocket = require('../lib/LiveQuery/ParseWebSocketServer') + .ParseWebSocket; describe('ParseWebSocket', function() { - it('can be initialized', function() { const ws = {}; const parseWebSocket = new ParseWebSocket(ws); @@ -11,7 +11,7 @@ describe('ParseWebSocket', function() { it('can handle events defined in typeMap', function() { const ws = { - on: jasmine.createSpy('on') + on: jasmine.createSpy('on'), }; const callback = {}; const parseWebSocket = new ParseWebSocket(ws); @@ -22,7 +22,7 @@ describe('ParseWebSocket', function() { it('can handle events which are not defined in typeMap', function() { const ws = { - on: jasmine.createSpy('on') + on: jasmine.createSpy('on'), }; const callback = {}; const parseWebSocket = new ParseWebSocket(ws); @@ -33,10 +33,10 @@ describe('ParseWebSocket', function() { it('can send a message', function() { const ws = { - send: jasmine.createSpy('send') + send: jasmine.createSpy('send'), }; const parseWebSocket = new ParseWebSocket(ws); - parseWebSocket.send('message') + parseWebSocket.send('message'); expect(parseWebSocket.ws.send).toHaveBeenCalledWith('message'); }); diff --git a/spec/ParseWebSocketServer.spec.js b/spec/ParseWebSocketServer.spec.js index b1b1ae5ad8..e821a0979e 100644 --- a/spec/ParseWebSocketServer.spec.js +++ b/spec/ParseWebSocketServer.spec.js @@ -1,7 +1,7 @@ -const ParseWebSocketServer = require('../lib/LiveQuery/ParseWebSocketServer').ParseWebSocketServer; +const ParseWebSocketServer = require('../lib/LiveQuery/ParseWebSocketServer') + .ParseWebSocketServer; describe('ParseWebSocketServer', function() { - beforeEach(function(done) { // Mock ws server const EventEmitter = require('events'); @@ -16,11 +16,15 @@ describe('ParseWebSocketServer', function() { const onConnectCallback = jasmine.createSpy('onConnectCallback'); const http = require('http'); const server = http.createServer(); - const parseWebSocketServer = new ParseWebSocketServer(server, onConnectCallback, 5).server; + const parseWebSocketServer = new ParseWebSocketServer( + server, + onConnectCallback, + 5 + ).server; const ws = { readyState: 0, OPEN: 0, - ping: jasmine.createSpy('ping') + ping: jasmine.createSpy('ping'), }; parseWebSocketServer.emit('connection', ws); @@ -31,10 +35,10 @@ describe('ParseWebSocketServer', function() { expect(ws.ping).toHaveBeenCalled(); server.close(); done(); - }, 10) + }, 10); }); - afterEach(function(){ + afterEach(function() { jasmine.restoreLibrary('ws', 'Server'); }); }); diff --git a/spec/PasswordPolicy.spec.js b/spec/PasswordPolicy.spec.js index b03ca7fff9..1cab84ab45 100644 --- a/spec/PasswordPolicy.spec.js +++ b/spec/PasswordPolicy.spec.js @@ -1,9 +1,8 @@ -"use strict"; +'use strict'; const requestp = require('request-promise'); -describe("Password Policy: ", () => { - +describe('Password Policy: ', () => { it('should show the invalid link page if the user clicks on the password reset link after the token expires', done => { const user = new Parse.User(); let sendEmailOptions; @@ -12,8 +11,7 @@ describe("Password Policy: ", () => { sendPasswordResetEmail: options => { sendEmailOptions = options; }, - sendMail: () => { - } + sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', @@ -21,40 +19,49 @@ describe("Password Policy: ", () => { passwordPolicy: { resetTokenValidityDuration: 0.5, // 0.5 second }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - user.setUsername("testResetTokenValidity"); - user.setPassword("original"); - user.set('email', 'user@parse.com'); - return user.signUp(); - }).then(() => { - Parse.User.requestPasswordReset("user@parse.com").catch((err) => { + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + user.setUsername('testResetTokenValidity'); + user.setPassword('original'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + Parse.User.requestPasswordReset('user@parse.com').catch(err => { + jfail(err); + fail('Reset password request should not fail'); + done(); + }); + }) + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + requestp + .get({ + uri: sendEmailOptions.link, + followRedirect: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' + ); + done(); + }) + .catch(error => { + fail(error); + }); + }, 1000); + }) + .catch(err => { jfail(err); - fail("Reset password request should not fail"); done(); }); - }).then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { - expect(sendEmailOptions).not.toBeUndefined(); - - requestp.get({ - uri: sendEmailOptions.link, - followRedirect: false, - simple: false, - resolveWithFullResponse: true - }).then((response) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); - done(); - }).catch((error) => { - fail(error); - }); - }, 1000); - }).catch((err) => { - jfail(err); - done(); - }); }); it('should show the reset password page if the user clicks on the password reset link before the token expires', done => { @@ -65,8 +72,7 @@ describe("Password Policy: ", () => { sendPasswordResetEmail: options => { sendEmailOptions = options; }, - sendMail: () => { - } + sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', @@ -74,376 +80,465 @@ describe("Password Policy: ", () => { passwordPolicy: { resetTokenValidityDuration: 5, // 5 seconds }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - user.setUsername("testResetTokenValidity"); - user.setPassword("original"); - user.set('email', 'user@parse.com'); - return user.signUp(); - }).then(() => { - Parse.User.requestPasswordReset('user@parse.com').catch((err) => { + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + user.setUsername('testResetTokenValidity'); + user.setPassword('original'); + user.set('email', 'user@parse.com'); + return user.signUp(); + }) + .then(() => { + Parse.User.requestPasswordReset('user@parse.com').catch(err => { + jfail(err); + fail('Reset password request should not fail'); + done(); + }); + }) + .then(() => { + // wait for a bit but less than the validity duration + setTimeout(() => { + expect(sendEmailOptions).not.toBeUndefined(); + + requestp + .get({ + uri: sendEmailOptions.link, + simple: false, + resolveWithFullResponse: true, + followRedirect: false, + }) + .then(response => { + expect(response.statusCode).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; + expect(response.body.match(re)).not.toBe(null); + done(); + }) + .catch(error => { + fail(error); + }); + }, 1000); + }) + .catch(err => { jfail(err); - fail("Reset password request should not fail"); done(); }); - }).then(() => { - // wait for a bit but less than the validity duration - setTimeout(() => { - expect(sendEmailOptions).not.toBeUndefined(); - - requestp.get({ - uri: sendEmailOptions.link, - simple: false, - resolveWithFullResponse: true, - followRedirect: false - }).then((response) => { - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=testResetTokenValidity/; - expect(response.body.match(re)).not.toBe(null); - done(); - }).catch((error) => { - fail(error); - }); - }, 1000); - }).catch((err) => { - jfail(err); - done(); - }); }); it('should fail if passwordPolicy.resetTokenValidityDuration is not a number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - resetTokenValidityDuration: "not a number" + resetTokenValidityDuration: 'not a number', }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('passwordPolicy.resetTokenValidityDuration "not a number" test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.resetTokenValidityDuration must be a positive number'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail( + 'passwordPolicy.resetTokenValidityDuration "not a number" test failed' + ); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.resetTokenValidityDuration must be a positive number' + ); + done(); + }); }); it('should fail if passwordPolicy.resetTokenValidityDuration is zero or a negative number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - resetTokenValidityDuration: 0 + resetTokenValidityDuration: 0, }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('resetTokenValidityDuration negative number test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.resetTokenValidityDuration must be a positive number'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail('resetTokenValidityDuration negative number test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.resetTokenValidityDuration must be a positive number' + ); + done(); + }); }); it('should fail if passwordPolicy.validatorPattern setting is invalid type', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: 1234 // number is not a valid setting + validatorPattern: 1234, // number is not a valid setting }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('passwordPolicy.validatorPattern type test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.validatorPattern must be a regex string or RegExp object.'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail('passwordPolicy.validatorPattern type test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.validatorPattern must be a regex string or RegExp object.' + ); + done(); + }); }); it('should fail if passwordPolicy.validatorCallback setting is invalid type', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorCallback: "abc" // string is not a valid setting + validatorCallback: 'abc', // string is not a valid setting }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('passwordPolicy.validatorCallback type test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.validatorCallback must be a function.'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail('passwordPolicy.validatorCallback type test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.validatorCallback must be a function.' + ); + done(); + }); }); - it('signup should fail if password does not conform to the policy enforced using validatorPattern', (done) => { + it('signup should fail if password does not conform to the policy enforced using validatorPattern', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/, // password should contain at least one digit }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("nodigit"); + user.setUsername('user1'); + user.setPassword('nodigit'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - fail('Should have failed as password does not conform to the policy.'); - done(); - }).catch((error) => { - expect(error.code).toEqual(142); - done(); - }); - }) + user + .signUp() + .then(() => { + fail( + 'Should have failed as password does not conform to the policy.' + ); + done(); + }) + .catch(error => { + expect(error.code).toEqual(142); + done(); + }); + }); }); - it('signup should fail if password does not conform to the policy enforced using validatorPattern string', (done) => { + it('signup should fail if password does not conform to the policy enforced using validatorPattern string', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: "^.{8,}" // password should contain at least 8 char + validatorPattern: '^.{8,}', // password should contain at least 8 char }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("less"); + user.setUsername('user1'); + user.setPassword('less'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - fail('Should have failed as password does not conform to the policy.'); - done(); - }).catch((error) => { - expect(error.code).toEqual(142); - done(); - }); - }) + user + .signUp() + .then(() => { + fail( + 'Should have failed as password does not conform to the policy.' + ); + done(); + }) + .catch(error => { + expect(error.code).toEqual(142); + done(); + }); + }); }); - it('signup should fail if password is empty', (done) => { + it('signup should fail if password is empty', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: "^.{8,}" // password should contain at least 8 char + validatorPattern: '^.{8,}', // password should contain at least 8 char }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword(""); + user.setUsername('user1'); + user.setPassword(''); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - fail('Should have failed as password does not conform to the policy.'); - done(); - }).catch((error) => { - expect(error.message).toEqual('Cannot sign up user with an empty password.'); - done(); - }); - }) + user + .signUp() + .then(() => { + fail( + 'Should have failed as password does not conform to the policy.' + ); + done(); + }) + .catch(error => { + expect(error.message).toEqual( + 'Cannot sign up user with an empty password.' + ); + done(); + }); + }); }); - it('signup should succeed if password conforms to the policy enforced using validatorPattern', (done) => { + it('signup should succeed if password conforms to the policy enforced using validatorPattern', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/, // password should contain at least one digit }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("1digit"); + user.setUsername('user1'); + user.setPassword('1digit'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.logOut().then(() => { - Parse.User.logIn("user1", "1digit").then(function () { - done(); - }).catch((err) => { - jfail(err); - fail("Should be able to login"); - done(); - }); - }).catch((error) => { + user + .signUp() + .then(() => { + Parse.User.logOut() + .then(() => { + Parse.User.logIn('user1', '1digit') + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + fail('Should be able to login'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('logout should have succeeded'); + done(); + }); + }) + .catch(error => { jfail(error); - fail('logout should have succeeded'); + fail( + 'Signup should have succeeded as password conforms to the policy.' + ); done(); }); - }).catch((error) => { - jfail(error); - fail('Signup should have succeeded as password conforms to the policy.'); - done(); - }); - }) + }); }); - it('signup should succeed if password conforms to the policy enforced using validatorPattern string', (done) => { + it('signup should succeed if password conforms to the policy enforced using validatorPattern string', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: "[!@#$]+" // password should contain at least one special char + validatorPattern: '[!@#$]+', // password should contain at least one special char }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("p@sswrod"); + user.setUsername('user1'); + user.setPassword('p@sswrod'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.logOut().then(() => { - Parse.User.logIn("user1", "p@sswrod").then(function () { - done(); - }).catch((err) => { - jfail(err); - fail("Should be able to login"); - done(); - }); - }).catch((error) => { + user + .signUp() + .then(() => { + Parse.User.logOut() + .then(() => { + Parse.User.logIn('user1', 'p@sswrod') + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + fail('Should be able to login'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('logout should have succeeded'); + done(); + }); + }) + .catch(error => { jfail(error); - fail('logout should have succeeded'); + fail( + 'Signup should have succeeded as password conforms to the policy.' + ); done(); }); - }).catch((error) => { - jfail(error); - fail('Signup should have succeeded as password conforms to the policy.'); - done(); - }); - }) + }); }); - it('signup should fail if password does not conform to the policy enforced using validatorCallback', (done) => { + it('signup should fail if password does not conform to the policy enforced using validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorCallback: () => false // just fail + validatorCallback: () => false, // just fail }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("any"); + user.setUsername('user1'); + user.setPassword('any'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - fail('Should have failed as password does not conform to the policy.'); - done(); - }).catch((error) => { - expect(error.code).toEqual(142); - done(); - }); - }) + user + .signUp() + .then(() => { + fail( + 'Should have failed as password does not conform to the policy.' + ); + done(); + }) + .catch(error => { + expect(error.code).toEqual(142); + done(); + }); + }); }); - it('signup should succeed if password conforms to the policy enforced using validatorCallback', (done) => { + it('signup should succeed if password conforms to the policy enforced using validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorCallback: () => true // never fail + validatorCallback: () => true, // never fail }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("oneUpper"); + user.setUsername('user1'); + user.setPassword('oneUpper'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.logOut().then(() => { - Parse.User.logIn("user1", "oneUpper").then(function () { - done(); - }).catch((err) => { - jfail(err); - fail("Should be able to login"); - done(); - }); - }).catch(error => { + user + .signUp() + .then(() => { + Parse.User.logOut() + .then(() => { + Parse.User.logIn('user1', 'oneUpper') + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + fail('Should be able to login'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('Logout should have succeeded'); + done(); + }); + }) + .catch(error => { jfail(error); - fail("Logout should have succeeded"); + fail('Should have succeeded as password conforms to the policy.'); done(); }); - }).catch((error) => { - jfail(error); - fail('Should have succeeded as password conforms to the policy.'); - done(); - }); - }) + }); }); - it('signup should fail if password does not match validatorPattern but succeeds validatorCallback', (done) => { + it('signup should fail if password does not match validatorPattern but succeeds validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter - validatorCallback: () => true + validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter + validatorCallback: () => true, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("all lower"); + user.setUsername('user1'); + user.setPassword('all lower'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - fail('Should have failed as password does not conform to the policy.'); - done(); - }).catch((error) => { - expect(error.code).toEqual(142); - done(); - }); - }) + user + .signUp() + .then(() => { + fail( + 'Should have failed as password does not conform to the policy.' + ); + done(); + }) + .catch(error => { + expect(error.code).toEqual(142); + done(); + }); + }); }); - it('signup should fail if password matches validatorPattern but fails validatorCallback', (done) => { + it('signup should fail if password matches validatorPattern but fails validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter - validatorCallback: () => false + validatorPattern: /[A-Z]+/, // password should contain at least one UPPER case letter + validatorCallback: () => false, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("oneUpper"); + user.setUsername('user1'); + user.setPassword('oneUpper'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - fail('Should have failed as password does not conform to the policy.'); - done(); - }).catch((error) => { - expect(error.code).toEqual(142); - done(); - }); - }) + user + .signUp() + .then(() => { + fail( + 'Should have failed as password does not conform to the policy.' + ); + done(); + }) + .catch(error => { + expect(error.code).toEqual(142); + done(); + }); + }); }); - it('signup should succeed if password conforms to both validatorPattern and validatorCallback', (done) => { + it('signup should succeed if password conforms to both validatorPattern and validatorCallback', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: /[A-Z]+/, // password should contain at least one digit - validatorCallback: () => true + validatorPattern: /[A-Z]+/, // password should contain at least one digit + validatorCallback: () => true, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("oneUpper"); + user.setUsername('user1'); + user.setPassword('oneUpper'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.logOut().then(() => { - Parse.User.logIn("user1", "oneUpper").then(function () { - done(); - }).catch((err) => { - jfail(err); - fail("Should be able to login"); - done(); - }); - }).catch(error => { + user + .signUp() + .then(() => { + Parse.User.logOut() + .then(() => { + Parse.User.logIn('user1', 'oneUpper') + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + fail('Should be able to login'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('logout should have succeeded'); + done(); + }); + }) + .catch(error => { jfail(error); - fail("logout should have succeeded"); + fail('Should have succeeded as password conforms to the policy.'); done(); }); - }).catch((error) => { - jfail(error); - fail('Should have succeeded as password conforms to the policy.'); - done(); - }); - }) + }); }); it('should reset password if new password conforms to password policy', done => { @@ -451,79 +546,91 @@ describe("Password Policy: ", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - requestp.get({ - uri: options.link, - followRedirect: false, - simple: false, - resolveWithFullResponse: true - }).then((response) => { - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; - const match = response.body.match(re); - if (!match) { - fail("should have a token"); - done(); - return; - } - const token = match[1]; - - requestp.post({ - uri: "http://localhost:8378/1/apps/test/request_password_reset", - body: `new_password=has2init&token=${token}&username=user1`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, + requestp + .get({ + uri: options.link, followRedirect: false, simple: false, - resolveWithFullResponse: true - }).then((response) => { + resolveWithFullResponse: true, + }) + .then(response => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'); - - Parse.User.logIn("user1", "has2init").then(function () { - done(); - }).catch((err) => { - jfail(err); - fail("should login with new password"); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.body.match(re); + if (!match) { + fail('should have a token'); done(); - }); - }).catch((error) => { + return; + } + const token = match[1]; + + requestp + .post({ + uri: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=has2init&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirect: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' + ); + + Parse.User.logIn('user1', 'has2init') + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + fail('should login with new password'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('Failed to POST request password reset'); + done(); + }); + }) + .catch(error => { jfail(error); - fail("Failed to POST request password reset"); + fail('Failed to get the reset link'); done(); }); - }).catch((error) => { - jfail(error); - fail("Failed to get the reset link"); - done(); - }); }, - sendMail: () => { - } + sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - validatorPattern: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/, // password should contain at least one digit }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("has 1 digit"); + user.setUsername('user1'); + user.setPassword('has 1 digit'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.requestPasswordReset('user1@parse.com').catch((err) => { - jfail(err); - fail("Reset password request should not fail"); + user + .signUp() + .then(() => { + Parse.User.requestPasswordReset('user1@parse.com').catch(err => { + jfail(err); + fail('Reset password request should not fail'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('signUp should not fail'); done(); }); - }).catch(error => { - jfail(error); - fail("signUp should not fail"); - done(); - }); }); }); @@ -532,161 +639,186 @@ describe("Password Policy: ", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - requestp.get({ - uri: options.link, - followRedirect: false, - simple: false, - resolveWithFullResponse: true - }).then((response) => { - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; - const match = response.body.match(re); - if (!match) { - fail("should have a token"); - done(); - return; - } - const token = match[1]; - - requestp.post({ - uri: "http://localhost:8378/1/apps/test/request_password_reset", - body: `new_password=hasnodigit&token=${token}&username=user1`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, + requestp + .get({ + uri: options.link, followRedirect: false, simple: false, - resolveWithFullResponse: true - }).then((response) => { + resolveWithFullResponse: true, + }) + .then(response => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual(`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20meet%20the%20Password%20Policy%20requirements.&app=passwordPolicy`); - - Parse.User.logIn("user1", "has 1 digit").then(function () { - done(); - }).catch((err) => { - jfail(err); - fail("should login with old password"); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.body.match(re); + if (!match) { + fail('should have a token'); done(); - }); - }).catch((error) => { + return; + } + const token = match[1]; + + requestp + .post({ + uri: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=hasnodigit&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirect: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20meet%20the%20Password%20Policy%20requirements.&app=passwordPolicy` + ); + + Parse.User.logIn('user1', 'has 1 digit') + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + fail('should login with old password'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('Failed to POST request password reset'); + done(); + }); + }) + .catch(error => { jfail(error); - fail("Failed to POST request password reset"); + fail('Failed to get the reset link'); done(); }); - }).catch((error) => { - jfail(error); - fail("Failed to get the reset link"); - done(); - }); }, - sendMail: () => { - } + sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - validatorPattern: /[0-9]+/ // password should contain at least one digit + validatorPattern: /[0-9]+/, // password should contain at least one digit }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("has 1 digit"); + user.setUsername('user1'); + user.setPassword('has 1 digit'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.requestPasswordReset('user1@parse.com').catch((err) => { - jfail(err); - fail("Reset password request should not fail"); + user + .signUp() + .then(() => { + Parse.User.requestPasswordReset('user1@parse.com').catch(err => { + jfail(err); + fail('Reset password request should not fail'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('signUp should not fail'); done(); }); - }).catch(error => { - jfail(error); - fail("signUp should not fail"); - done(); - }); }); }); - it('should fail if passwordPolicy.doNotAllowUsername is not a boolean value', (done) => { + it('should fail if passwordPolicy.doNotAllowUsername is not a boolean value', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - doNotAllowUsername: 'no' + doNotAllowUsername: 'no', }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('passwordPolicy.doNotAllowUsername type test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.doNotAllowUsername must be a boolean value.'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail('passwordPolicy.doNotAllowUsername type test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.doNotAllowUsername must be a boolean value.' + ); + done(); + }); }); - it('signup should fail if password contains the username and is not allowed by policy', (done) => { + it('signup should fail if password contains the username and is not allowed by policy', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { validatorPattern: /[0-9]+/, - doNotAllowUsername: true + doNotAllowUsername: true, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("@user11"); + user.setUsername('user1'); + user.setPassword('@user11'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - fail('Should have failed as password contains username.'); - done(); - }).catch((error) => { - expect(error.code).toEqual(142); - done(); - }); - }) + user + .signUp() + .then(() => { + fail('Should have failed as password contains username.'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(142); + done(); + }); + }); }); - it('signup should succeed if password does not contain the username and is not allowed by policy', (done) => { + it('signup should succeed if password does not contain the username and is not allowed by policy', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - doNotAllowUsername: true + doNotAllowUsername: true, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("r@nd0m"); + user.setUsername('user1'); + user.setPassword('r@nd0m'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - done(); - }).catch(() => { - fail('Should have succeeded as password does not contain username.'); - done(); - }); - }) + user + .signUp() + .then(() => { + done(); + }) + .catch(() => { + fail('Should have succeeded as password does not contain username.'); + done(); + }); + }); }); - it('signup should succeed if password contains the username and it is allowed by policy', (done) => { + it('signup should succeed if password contains the username and it is allowed by policy', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - validatorPattern: /[0-9]+/ + validatorPattern: /[0-9]+/, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("user1"); + user.setUsername('user1'); + user.setPassword('user1'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - done(); - }).catch(() => { - fail('Should have succeeded as policy allows username in password.'); - done(); - }); - }) + user + .signUp() + .then(() => { + done(); + }) + .catch(() => { + fail('Should have succeeded as policy allows username in password.'); + done(); + }); + }); }); it('should fail to reset password if the new password contains username and not allowed by password policy', done => { @@ -694,80 +826,91 @@ describe("Password Policy: ", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - requestp.get({ - uri: options.link, - followRedirect: false, - simple: false, - resolveWithFullResponse: true - }).then((response) => { - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; - const match = response.body.match(re); - if (!match) { - fail("should have a token"); - done(); - return; - } - const token = match[1]; - - requestp.post({ - uri: "http://localhost:8378/1/apps/test/request_password_reset", - body: `new_password=xuser12&token=${token}&username=user1`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, + requestp + .get({ + uri: options.link, followRedirect: false, simple: false, - resolveWithFullResponse: true - }).then((response) => { + resolveWithFullResponse: true, + }) + .then(response => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual(`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20meet%20the%20Password%20Policy%20requirements.&app=passwordPolicy`); - - Parse.User.logIn("user1", "r@nd0m").then(function () { - done(); - }).catch((err) => { - jfail(err); - fail("should login with old password"); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.body.match(re); + if (!match) { + fail('should have a token'); done(); - }); + return; + } + const token = match[1]; + + requestp + .post({ + uri: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=xuser12&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirect: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=Password%20does%20not%20meet%20the%20Password%20Policy%20requirements.&app=passwordPolicy` + ); - }).catch((error) => { + Parse.User.logIn('user1', 'r@nd0m') + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + fail('should login with old password'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('Failed to POST request password reset'); + done(); + }); + }) + .catch(error => { jfail(error); - fail("Failed to POST request password reset"); + fail('Failed to get the reset link'); done(); }); - }).catch((error) => { - jfail(error); - fail("Failed to get the reset link"); - done(); - }); }, - sendMail: () => { - } + sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - doNotAllowUsername: true + doNotAllowUsername: true, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("r@nd0m"); + user.setUsername('user1'); + user.setPassword('r@nd0m'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.requestPasswordReset('user1@parse.com').catch((err) => { - jfail(err); - fail("Reset password request should not fail"); + user + .signUp() + .then(() => { + Parse.User.requestPasswordReset('user1@parse.com').catch(err => { + jfail(err); + fail('Reset password request should not fail'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('signUp should not fail'); done(); }); - }).catch(error => { - jfail(error); - fail("signUp should not fail"); - done(); - }); }); }); @@ -776,54 +919,62 @@ describe("Password Policy: ", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - requestp.get({ - uri: options.link, - followRedirect: false, - simple: false, - resolveWithFullResponse: true - }).then(response => { - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; - const match = response.body.match(re); - if (!match) { - fail("should have a token"); - done(); - return; - } - const token = match[1]; - - requestp.post({ - uri: "http://localhost:8378/1/apps/test/request_password_reset", - body: `new_password=uuser11&token=${token}&username=user1`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, + requestp + .get({ + uri: options.link, followRedirect: false, simple: false, - resolveWithFullResponse: true - }).then(response => { + resolveWithFullResponse: true, + }) + .then(response => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'); - - Parse.User.logIn("user1", "uuser11").then(function () { - done(); - }).catch(err => { - jfail(err); - fail("should login with new password"); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.body.match(re); + if (!match) { + fail('should have a token'); done(); - }); + return; + } + const token = match[1]; + + requestp + .post({ + uri: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=uuser11&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirect: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' + ); - }).catch(error => { + Parse.User.logIn('user1', 'uuser11') + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + fail('should login with new password'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('Failed to POST request password reset'); + }); + }) + .catch(error => { jfail(error); - fail("Failed to POST request password reset"); + fail('Failed to get the reset link'); }); - }).catch(error => { - jfail(error); - fail("Failed to get the reset link"); - }); }, - sendMail: () => { - } + sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', @@ -831,173 +982,209 @@ describe("Password Policy: ", () => { emailAdapter: emailAdapter, passwordPolicy: { validatorPattern: /[0-9]+/, - doNotAllowUsername: false + doNotAllowUsername: false, }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - user.setUsername("user1"); - user.setPassword("has 1 digit"); - user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.requestPasswordReset('user1@parse.com').catch((err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + user.setUsername('user1'); + user.setPassword('has 1 digit'); + user.set('email', 'user1@parse.com'); + user.signUp().then(() => { + Parse.User.requestPasswordReset('user1@parse.com').catch(err => { + jfail(err); + fail('Reset password request should not fail'); + done(); + }); }); + }) + .catch(error => { + jfail(error); + fail('signUp should not fail'); + done(); }); - }).catch(error => { - jfail(error); - fail("signUp should not fail"); - done(); - }); }); it('should fail if passwordPolicy.maxPasswordAge is not a number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - maxPasswordAge: "not a number" + maxPasswordAge: 'not a number', }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('passwordPolicy.maxPasswordAge "not a number" test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.maxPasswordAge must be a positive number'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail('passwordPolicy.maxPasswordAge "not a number" test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.maxPasswordAge must be a positive number' + ); + done(); + }); }); it('should fail if passwordPolicy.maxPasswordAge is a negative number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - maxPasswordAge: -100 + maxPasswordAge: -100, }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('passwordPolicy.maxPasswordAge negative number test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.maxPasswordAge must be a positive number'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail('passwordPolicy.maxPasswordAge negative number test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.maxPasswordAge must be a positive number' + ); + done(); + }); }); - it('should succeed if logged in before password expires', (done) => { + it('should succeed if logged in before password expires', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - maxPasswordAge: 1 // 1 day + maxPasswordAge: 1, // 1 day }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("user1"); + user.setUsername('user1'); + user.setPassword('user1'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.logIn("user1", "user1").then(() => { - done(); - }).catch((error) => { + user + .signUp() + .then(() => { + Parse.User.logIn('user1', 'user1') + .then(() => { + done(); + }) + .catch(error => { + jfail(error); + fail('Login should have succeeded before password expiry.'); + done(); + }); + }) + .catch(error => { jfail(error); - fail('Login should have succeeded before password expiry.'); + fail('Signup failed.'); done(); }); - }).catch((error) => { - jfail(error); - fail('Signup failed.'); - done(); - }); - }) + }); }); - it('should fail if logged in after password expires', (done) => { + it('should fail if logged in after password expires', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - maxPasswordAge: 0.5 / (24 * 60 * 60) // 0.5 sec + maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("user1"); + user.setUsername('user1'); + user.setPassword('user1'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { - Parse.User.logIn("user1", "user1").then(() => { - fail("logIn should have failed"); - done(); - }).catch((error) => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - expect(error.message).toEqual('Your password has expired. Please reset your password.'); - done(); - }); - }, 1000); - }).catch((error) => { - jfail(error); - fail('Signup failed.'); - done(); - }); + user + .signUp() + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + Parse.User.logIn('user1', 'user1') + .then(() => { + fail('logIn should have failed'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + expect(error.message).toEqual( + 'Your password has expired. Please reset your password.' + ); + done(); + }); + }, 1000); + }) + .catch(error => { + jfail(error); + fail('Signup failed.'); + done(); + }); }); }); - it('should apply password expiry policy to existing user upon first login after policy is enabled', (done) => { + it('should apply password expiry policy to existing user upon first login after policy is enabled', done => { const user = new Parse.User(); reconfigureServer({ appName: 'passwordPolicy', - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("user1"); + user.setUsername('user1'); + user.setPassword('user1'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - Parse.User.logOut().then(() => { - reconfigureServer({ - appName: 'passwordPolicy', - passwordPolicy: { - maxPasswordAge: 0.5 / (24 * 60 * 60) // 0.5 sec - }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - Parse.User.logIn("user1", "user1").then(() => { - Parse.User.logOut().then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { - Parse.User.logIn("user1", "user1").then(() => { - fail("logIn should have failed"); - done(); - }).catch((error) => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - expect(error.message).toEqual('Your password has expired. Please reset your password.'); + user + .signUp() + .then(() => { + Parse.User.logOut() + .then(() => { + reconfigureServer({ + appName: 'passwordPolicy', + passwordPolicy: { + maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec + }, + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + Parse.User.logIn('user1', 'user1') + .then(() => { + Parse.User.logOut() + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + Parse.User.logIn('user1', 'user1') + .then(() => { + fail('logIn should have failed'); + done(); + }) + .catch(error => { + expect(error.code).toEqual( + Parse.Error.OBJECT_NOT_FOUND + ); + expect(error.message).toEqual( + 'Your password has expired. Please reset your password.' + ); + done(); + }); + }, 2000); + }) + .catch(error => { + jfail(error); + fail('logout should have succeeded'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('Login failed.'); done(); }); - }, 2000); - }).catch(error => { - jfail(error); - fail("logout should have succeeded"); - done(); }); - }).catch((error) => { + }) + .catch(error => { jfail(error); - fail('Login failed.'); + fail('logout should have succeeded'); done(); }); - }); - }).catch(error => { + }) + .catch(error => { jfail(error); - fail("logout should have succeeded"); + fail('Signup failed.'); done(); }); - }).catch((error) => { - jfail(error); - fail('Signup failed.'); - done(); - }); }); - }); it('should reset password timestamp when password is reset', done => { @@ -1005,86 +1192,104 @@ describe("Password Policy: ", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - requestp.get({ - uri: options.link, - followRedirect: false, - simple: false, - resolveWithFullResponse: true - }).then(response => { - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; - const match = response.body.match(re); - if (!match) { - fail("should have a token"); - done(); - return; - } - const token = match[1]; - - requestp.post({ - uri: "http://localhost:8378/1/apps/test/request_password_reset", - body: `new_password=uuser11&token=${token}&username=user1`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, + requestp + .get({ + uri: options.link, followRedirect: false, simple: false, - resolveWithFullResponse: true - }).then(response => { + resolveWithFullResponse: true, + }) + .then(response => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1'); - - Parse.User.logIn("user1", "uuser11").then(function () { + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.body.match(re); + if (!match) { + fail('should have a token'); done(); - }).catch(err => { - jfail(err); - fail("should login with new password"); - done(); - }); - }).catch(error => { + return; + } + const token = match[1]; + + requestp + .post({ + uri: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=uuser11&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirect: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=user1' + ); + + Parse.User.logIn('user1', 'uuser11') + .then(function() { + done(); + }) + .catch(err => { + jfail(err); + fail('should login with new password'); + done(); + }); + }) + .catch(error => { + jfail(error); + fail('Failed to POST request password reset'); + }); + }) + .catch(error => { jfail(error); - fail("Failed to POST request password reset"); + fail('Failed to get the reset link'); }); - }).catch(error => { - jfail(error); - fail("Failed to get the reset link"); - }); }, - sendMail: () => { - } + sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', emailAdapter: emailAdapter, passwordPolicy: { - maxPasswordAge: 0.5 / (24 * 60 * 60) // 0.5 sec + maxPasswordAge: 0.5 / (24 * 60 * 60), // 0.5 sec }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("user1"); + user.setUsername('user1'); + user.setPassword('user1'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - // wait for a bit more than the validity duration set - setTimeout(() => { - Parse.User.logIn("user1", "user1").then(() => { - fail("logIn should have failed"); - done(); - }).catch((error) => { - expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - expect(error.message).toEqual('Your password has expired. Please reset your password.'); - Parse.User.requestPasswordReset('user1@parse.com').catch((err) => { - jfail(err); - fail("Reset password request should not fail"); - done(); - }); - }); - }, 1000); - }).catch((error) => { - jfail(error); - fail('Signup failed.'); - done(); - }); + user + .signUp() + .then(() => { + // wait for a bit more than the validity duration set + setTimeout(() => { + Parse.User.logIn('user1', 'user1') + .then(() => { + fail('logIn should have failed'); + done(); + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + expect(error.message).toEqual( + 'Your password has expired. Please reset your password.' + ); + Parse.User.requestPasswordReset('user1@parse.com').catch( + err => { + jfail(err); + fail('Reset password request should not fail'); + done(); + } + ); + }); + }, 1000); + }) + .catch(error => { + jfail(error); + fail('Signup failed.'); + done(); + }); }); }); @@ -1092,48 +1297,60 @@ describe("Password Policy: ", () => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - maxPasswordHistory: "not a number" + maxPasswordHistory: 'not a number', }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('passwordPolicy.maxPasswordHistory "not a number" test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail('passwordPolicy.maxPasswordHistory "not a number" test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20' + ); + done(); + }); }); it('should fail if passwordPolicy.maxPasswordHistory is a negative number', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - maxPasswordHistory: -10 + maxPasswordHistory: -10, }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('passwordPolicy.maxPasswordHistory negative number test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail('passwordPolicy.maxPasswordHistory negative number test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20' + ); + done(); + }); }); it('should fail if passwordPolicy.maxPasswordHistory is greater than 20', done => { reconfigureServer({ appName: 'passwordPolicy', passwordPolicy: { - maxPasswordHistory: 21 + maxPasswordHistory: 21, }, - publicServerURL: "http://localhost:8378/1" - }).then(() => { - fail('passwordPolicy.maxPasswordHistory negative number test failed'); - done(); - }).catch(err => { - expect(err).toEqual('passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }) + .then(() => { + fail('passwordPolicy.maxPasswordHistory negative number test failed'); + done(); + }) + .catch(err => { + expect(err).toEqual( + 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20' + ); + done(); + }); }); it('should fail to reset if the new password is same as the last password', done => { @@ -1141,78 +1358,91 @@ describe("Password Policy: ", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - requestp.get({ - uri: options.link, - followRedirect: false, - simple: false, - resolveWithFullResponse: true - }).then(response => { - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; - const match = response.body.match(re); - if (!match) { - fail("should have a token"); - return Promise.reject("Invalid password link"); - } - return Promise.resolve(match[1]); // token - }).then(token => { - return new Promise((resolve, reject) => { - requestp.post({ - uri: "http://localhost:8378/1/apps/test/request_password_reset", - body: `new_password=user1&token=${token}&username=user1`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - followRedirect: false, - simple: false, - resolveWithFullResponse: true - }).then(response => { - resolve([response, token]); - }).catch(error => { - reject(error); + requestp + .get({ + uri: options.link, + followRedirect: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + expect(response.statusCode).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=user1/; + const match = response.body.match(re); + if (!match) { + fail('should have a token'); + return Promise.reject('Invalid password link'); + } + return Promise.resolve(match[1]); // token + }) + .then(token => { + return new Promise((resolve, reject) => { + requestp + .post({ + uri: + 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=user1&token=${token}&username=user1`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirect: false, + simple: false, + resolveWithFullResponse: true, + }) + .then(response => { + resolve([response, token]); + }) + .catch(error => { + reject(error); + }); }); + }) + .then(data => { + const response = data[0]; + const token = data[1]; + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + `Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy` + ); + done(); + return Promise.resolve(); + }) + .catch(error => { + jfail(error); + fail('Repeat password test failed'); + done(); }); - }).then(data => { - const response = data[0]; - const token = data[1]; - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual(`Found. Redirecting to http://localhost:8378/1/apps/choose_password?username=user1&token=${token}&id=test&error=New%20password%20should%20not%20be%20the%20same%20as%20last%201%20passwords.&app=passwordPolicy`); - done(); - return Promise.resolve(); - }).catch(error => { - jfail(error); - fail("Repeat password test failed"); - done(); - }); }, - sendMail: () => { - } + sendMail: () => {}, }; reconfigureServer({ appName: 'passwordPolicy', verifyUserEmails: false, emailAdapter: emailAdapter, passwordPolicy: { - maxPasswordHistory: 1 + maxPasswordHistory: 1, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("user1"); + user.setUsername('user1'); + user.setPassword('user1'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - return Parse.User.logOut(); - }).then(() => { - return Parse.User.requestPasswordReset('user1@parse.com'); - }).catch(error => { - jfail(error); - fail("SignUp or reset request failed"); - done(); - }); + user + .signUp() + .then(() => { + return Parse.User.logOut(); + }) + .then(() => { + return Parse.User.requestPasswordReset('user1@parse.com'); + }) + .catch(error => { + jfail(error); + fail('SignUp or reset request failed'); + done(); + }); }); }); - it('should fail if the new password is same as the previous one', done => { const user = new Parse.User(); @@ -1220,25 +1450,33 @@ describe("Password Policy: ", () => { appName: 'passwordPolicy', verifyUserEmails: false, passwordPolicy: { - maxPasswordHistory: 5 + maxPasswordHistory: 5, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("user1"); + user.setUsername('user1'); + user.setPassword('user1'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - // try to set the same password as the previous one - user.setPassword('user1'); - return user.save(); - }).then(() => { - fail("should have failed because the new password is same as the old"); - done(); - }).catch(error => { - expect(error.message).toEqual('New password should not be the same as last 5 passwords.'); - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - done(); - }); + user + .signUp() + .then(() => { + // try to set the same password as the previous one + user.setPassword('user1'); + return user.save(); + }) + .then(() => { + fail( + 'should have failed because the new password is same as the old' + ); + done(); + }) + .catch(error => { + expect(error.message).toEqual( + 'New password should not be the same as last 5 passwords.' + ); + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + done(); + }); }); }); @@ -1249,38 +1487,50 @@ describe("Password Policy: ", () => { appName: 'passwordPolicy', verifyUserEmails: false, passwordPolicy: { - maxPasswordHistory: 5 + maxPasswordHistory: 5, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("user1"); + user.setUsername('user1'); + user.setPassword('user1'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - // build history - user.setPassword('user2'); - return user.save(); - }).then(() => { - user.setPassword('user3'); - return user.save(); - }).then(() => { - user.setPassword('user4'); - return user.save(); - }).then(() => { - user.setPassword('user5'); - return user.save(); - }).then(() => { - // set the same password as the initial one - user.setPassword('user1'); - return user.save(); - }).then(() => { - fail("should have failed because the new password is same as the old"); - done(); - }).catch(error => { - expect(error.message).toEqual('New password should not be the same as last 5 passwords.'); - expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); - done(); - }); + user + .signUp() + .then(() => { + // build history + user.setPassword('user2'); + return user.save(); + }) + .then(() => { + user.setPassword('user3'); + return user.save(); + }) + .then(() => { + user.setPassword('user4'); + return user.save(); + }) + .then(() => { + user.setPassword('user5'); + return user.save(); + }) + .then(() => { + // set the same password as the initial one + user.setPassword('user1'); + return user.save(); + }) + .then(() => { + fail( + 'should have failed because the new password is same as the old' + ); + done(); + }) + .catch(error => { + expect(error.message).toEqual( + 'New password should not be the same as last 5 passwords.' + ); + expect(error.code).toEqual(Parse.Error.VALIDATION_ERROR); + done(); + }); }); }); @@ -1291,39 +1541,50 @@ describe("Password Policy: ", () => { appName: 'passwordPolicy', verifyUserEmails: false, passwordPolicy: { - maxPasswordHistory: 5 + maxPasswordHistory: 5, }, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(() => { - user.setUsername("user1"); - user.setPassword("user1"); + user.setUsername('user1'); + user.setPassword('user1'); user.set('email', 'user1@parse.com'); - user.signUp().then(() => { - // build history - user.setPassword('user2'); - return user.save(); - }).then(() => { - user.setPassword('user3'); - return user.save(); - }).then(() => { - user.setPassword('user4'); - return user.save(); - }).then(() => { - user.setPassword('user5'); - return user.save(); - }).then(() => { - user.setPassword('user6'); // this pushes initial password out of history - return user.save(); - }).then(() => { - // set the same password as the initial one - user.setPassword('user1'); - return user.save(); - }).then(() => { - done(); - }).catch(() => { - fail("should have succeeded because the new password is not in history"); - done(); - }); + user + .signUp() + .then(() => { + // build history + user.setPassword('user2'); + return user.save(); + }) + .then(() => { + user.setPassword('user3'); + return user.save(); + }) + .then(() => { + user.setPassword('user4'); + return user.save(); + }) + .then(() => { + user.setPassword('user5'); + return user.save(); + }) + .then(() => { + user.setPassword('user6'); // this pushes initial password out of history + return user.save(); + }) + .then(() => { + // set the same password as the initial one + user.setPassword('user1'); + return user.save(); + }) + .then(() => { + done(); + }) + .catch(() => { + fail( + 'should have succeeded because the new password is not in history' + ); + done(); + }); }); }); -}) +}); diff --git a/spec/PointerPermissions.spec.js b/spec/PointerPermissions.spec.js index 5c83365062..8463239f96 100644 --- a/spec/PointerPermissions.spec.js +++ b/spec/PointerPermissions.spec.js @@ -2,240 +2,324 @@ const Config = require('../lib/Config'); describe('Pointer Permissions', () => { - beforeEach(() => { Config.get(Parse.applicationId).database.schemaCache.clear(); }); - it('should work with find', (done) => { + it('should work with find', done => { const config = Config.get(Parse.applicationId); const user = new Parse.User(); const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); const 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']}) + 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(() => { + const 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(); }); - }).then(() => { - return Parse.User.logIn('user1', 'password'); - }).then(() => { - const 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('should work with write', (done) => { + it('should work with write', done => { const config = Config.get(Parse.applicationId); const user = new Parse.User(); const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); const 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(() => { - 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'); - }, () => { - fail('User should be able to update'); - return Promise.resolve(); - }).then(() => { - const q = new Parse.Query('AnObject'); - return q.find(); - }, () => { - 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); + 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( + () => { + 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(); }) - done(); - }, () => { - fail("failed"); - done(); - }) + .then( + () => { + return Parse.User.logIn('user2', 'password'); + }, + () => { + fail('User should be able to update'); + return Promise.resolve(); + } + ) + .then( + () => { + const q = new Parse.Query('AnObject'); + return q.find(); + }, + () => { + 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(); + }, + () => { + fail('failed'); + done(); + } + ); }); - it('should let a proper user find', (done) => { + it('should let a proper user find', done => { const config = Config.get(Parse.applicationId); const user = new Parse.User(); const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); const 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']}) + 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(() => { + const q = new Parse.Query('AnObject'); + return q.find(); + }) + .then(res => { + expect(res.length).toBe(0); + }) + .then(() => { + return Parse.User.logIn('user2', 'password'); + }) + .then(() => { + const q = new Parse.Query('AnObject'); + return q.find(); + }) + .then(res => { + expect(res.length).toBe(0); + const 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(() => { + const q = new Parse.Query('AnObject'); + return q.find(); + }) + .then(res => { + expect(res.length).toBe(1); + done(); + }) + .catch(err => { + jfail(err); + fail('should not fail'); + done(); }); - }).then(() => { - const q = new Parse.Query('AnObject'); - return q.find(); - }).then((res) => { - expect(res.length).toBe(0); - }).then(() => { - return Parse.User.logIn('user2', 'password'); - }).then(() => { - const q = new Parse.Query('AnObject'); - return q.find(); - }).then((res) => { - expect(res.length).toBe(0); - const 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(() => { - const q = new Parse.Query('AnObject'); - return q.find(); - }).then((res) => { - expect(res.length).toBe(1); - done(); - }).catch((err) => { - jfail(err); - fail('should not fail'); - done(); - }) }); - it('should query on pointer permission enabled column', (done) => { + it('should query on pointer permission enabled column', done => { const config = Config.get(Parse.applicationId); const user = new Parse.User(); const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); const 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']}) + 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(() => { + return Parse.User.logIn('user1', 'password'); + }) + .then(() => { + const q = new Parse.Query('AnObject'); + q.equalTo('owner', user2); + return q.find(); + }) + .then(res => { + expect(res.length).toBe(0); + done(); + }) + .catch(err => { + jfail(err); + fail('should not fail'); + done(); }); - }).then(() => { - return Parse.User.logIn('user1', 'password'); - }).then(() => { - const q = new Parse.Query('AnObject'); - q.equalTo('owner', user2); - return q.find(); - }).then((res) => { - expect(res.length).toBe(0); - done(); - }).catch((err) => { - jfail(err); - fail('should not fail'); - done(); - }) }); - it('should not allow creating objects', (done) => { + it('should not allow creating objects', done => { const config = Config.get(Parse.applicationId); const user = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); const 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(); - }) + 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('should handle multiple writeUserFields', done => { @@ -244,11 +328,11 @@ describe('Pointer Permissions', () => { const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); Parse.Object.saveAll([user, user2]) @@ -258,11 +342,17 @@ describe('Pointer Permissions', () => { return obj.save(); }) .then(() => config.database.loadSchema()) - .then(schema => schema.updateClass('AnObject', {}, {find: {"*": true},writeUserFields: ['owner', 'otherOwner']})) + .then(schema => + schema.updateClass( + 'AnObject', + {}, + { find: { '*': true }, writeUserFields: ['owner', 'otherOwner'] } + ) + ) .then(() => Parse.User.logIn('user1', 'password')) - .then(() => obj.save({hello: 'fromUser1'})) + .then(() => obj.save({ hello: 'fromUser1' })) .then(() => Parse.User.logIn('user2', 'password')) - .then(() => obj.save({hello: 'fromUser2'})) + .then(() => obj.save({ hello: 'fromUser2' })) .then(() => Parse.User.logOut()) .then(() => { const q = new Parse.Query('AnObject'); @@ -271,56 +361,88 @@ describe('Pointer Permissions', () => { .then(result => { expect(result.get('hello')).toBe('fromUser2'); done(); - }).catch(() => { + }) + .catch(() => { fail('should not fail'); done(); - }) + }); }); - it('should prevent creating pointer permission on missing field', (done) => { + it('should prevent creating pointer permission on missing field', done => { const config = Config.get(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(); - }) + 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) => { + it('should prevent creating pointer permission on bad field', done => { const config = Config.get(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(); - }) + 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) => { + it('should prevent creating pointer permission on bad field', done => { const config = Config.get(Parse.applicationId); const 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(); - }) + 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('tests CLP / Pointer Perms / ACL write (PP Locked)', (done) => { + it('tests CLP / Pointer Perms / ACL write (PP Locked)', done => { /* tests: CLP: update closed ({}) @@ -334,40 +456,52 @@ describe('Pointer Permissions', () => { const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); - Parse.Object.saveAll([user, user2]).then(() => { - const 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(); - }); + Parse.Object.saveAll([user, user2]) + .then(() => { + const 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('tests CLP / Pointer Perms / ACL write (ACL Locked)', (done) => { + it('tests CLP / Pointer Perms / ACL write (ACL Locked)', done => { /* tests: CLP: update closed ({}) @@ -379,40 +513,52 @@ describe('Pointer Permissions', () => { const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); - Parse.Object.saveAll([user, user2]).then(() => { - const 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(); - }); + Parse.Object.saveAll([user, user2]) + .then(() => { + const 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('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', (done) => { + it('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', done => { /* tests: CLP: update closed ({}) @@ -424,40 +570,52 @@ describe('Pointer Permissions', () => { const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); - Parse.Object.saveAll([user, user2]).then(() => { - const 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(); - }, () => { - fail('Should not fail saving'); - done(); - }); + Parse.Object.saveAll([user, user2]) + .then(() => { + const 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(); + }, + () => { + fail('Should not fail saving'); + done(); + } + ); }); - it('tests CLP / Pointer Perms / ACL read (PP locked)', (done) => { + it('tests CLP / Pointer Perms / ACL read (PP locked)', done => { /* tests: CLP: find/get open ({}) @@ -471,40 +629,52 @@ describe('Pointer Permissions', () => { const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); - Parse.Object.saveAll([user, user2]).then(() => { - const 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(); - }); + Parse.Object.saveAll([user, user2]) + .then(() => { + const 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('tests CLP / Pointer Perms / ACL read (PP/ACL OK)', (done) => { + it('tests CLP / Pointer Perms / ACL read (PP/ACL OK)', done => { /* tests: CLP: find/get open ({"*": true}) @@ -516,42 +686,58 @@ describe('Pointer Permissions', () => { const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); - Parse.Object.saveAll([user, user2]).then(() => { - const 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(); - }, () => { - fail('Should not fail fetching'); - done(); - }); + Parse.Object.saveAll([user, user2]) + .then(() => { + const 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(); + }, + () => { + fail('Should not fail fetching'); + done(); + } + ); }); - it('tests CLP / Pointer Perms / ACL read (ACL locked)', (done) => { + it('tests CLP / Pointer Perms / ACL read (ACL locked)', done => { /* tests: CLP: find/get open ({"*": true}) @@ -563,171 +749,258 @@ describe('Pointer Permissions', () => { const user2 = new Parse.User(); user.set({ username: 'user1', - password: 'password' + password: 'password', }); user2.set({ username: 'user2', - password: 'password' + password: 'password', }); const obj = new Parse.Object('AnObject'); - Parse.Object.saveAll([user, user2]).then(() => { - const 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(); - }); + Parse.Object.saveAll([user, user2]) + .then(() => { + const 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('should let master key find objects', (done) => { + it('should let master key find objects', done => { const config = Config.get(Parse.applicationId); const 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(() => { - const q = new Parse.Query('AnObject'); - return q.find(); - }).then(() => { - - }, (err) => { - expect(err.code).toBe(101); - return Promise.resolve(); - }).then(() => { - const q = new Parse.Query('AnObject'); - return q.find({useMasterKey: true}); - }).then((objects) => { - expect(objects.length).toBe(1); - done(); - }, () => { - fail('master key should find the object'); - done(); - }) + 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(() => { + const q = new Parse.Query('AnObject'); + return q.find(); + }) + .then( + () => {}, + err => { + expect(err.code).toBe(101); + return Promise.resolve(); + } + ) + .then(() => { + const q = new Parse.Query('AnObject'); + return q.find({ useMasterKey: true }); + }) + .then( + objects => { + expect(objects.length).toBe(1); + done(); + }, + () => { + fail('master key should find the object'); + done(); + } + ); }); - it('should let master key get objects', (done) => { + it('should let master key get objects', done => { const config = Config.get(Parse.applicationId); const 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(() => { - const q = new Parse.Query('AnObject'); - return q.get(object.id); - }).then(() => { - - }, (err) => { - expect(err.code).toBe(101); - return Promise.resolve(); - }).then(() => { - const 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(); - }, () => { - fail('master key should find the object'); - done(); - }) + 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(() => { + const q = new Parse.Query('AnObject'); + return q.get(object.id); + }) + .then( + () => {}, + err => { + expect(err.code).toBe(101); + return Promise.resolve(); + } + ) + .then(() => { + const 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(); + }, + () => { + fail('master key should find the object'); + done(); + } + ); }); - - it('should let master key update objects', (done) => { + it('should let master key update objects', done => { const config = Config.get(Parse.applicationId); const 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(); - }, () => { - fail('master key should save the object'); - done(); - }) + 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(); + }, + () => { + fail('master key should save the object'); + done(); + } + ); }); - it('should let master key delete objects', (done) => { + it('should let master key delete objects', done => { const config = Config.get(Parse.applicationId); const 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(() => { - done(); - }, () => { - fail('master key should destroy the object'); - done(); - }) + 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( + () => { + done(); + }, + () => { + fail('master key should destroy the object'); + done(); + } + ); }); - it('should fail with invalid pointer perms', (done) => { + it('should fail with invalid pointer perms', done => { const config = Config.get(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(); - }); + 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', (done) => { + it('should fail with invalid pointer perms', done => { const config = Config.get(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(); - }); - }) + 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(); + }); + }); }); diff --git a/spec/PostgresConfigParser.spec.js b/spec/PostgresConfigParser.spec.js index 4b966b1e2a..40aab5772f 100644 --- a/spec/PostgresConfigParser.spec.js +++ b/spec/PostgresConfigParser.spec.js @@ -3,47 +3,45 @@ const parser = require('../lib/Adapters/Storage/Postgres/PostgresConfigParser'); const queryParamTests = { 'a=1&b=2': { a: '1', b: '2' }, 'a=abcd%20efgh&b=abcd%3Defgh': { a: 'abcd efgh', b: 'abcd=efgh' }, - 'a=1&b&c=true': { a: '1', b: '', c: 'true' } -} + 'a=1&b&c=true': { a: '1', b: '', c: 'true' }, +}; describe('PostgresConfigParser.parseQueryParams', () => { it('creates a map from a query string', () => { - for (const key in queryParamTests) { const result = parser.parseQueryParams(key); const testObj = queryParamTests[key]; - expect(Object.keys(result).length) - .toEqual(Object.keys(testObj).length); + expect(Object.keys(result).length).toEqual(Object.keys(testObj).length); for (const k in result) { expect(result[k]).toEqual(testObj[k]); } } - - }) + }); }); -const baseURI = 'postgres://username:password@localhost:5432/db-name' +const baseURI = 'postgres://username:password@localhost:5432/db-name'; const dbOptionsTest = {}; -dbOptionsTest[`${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=10`] = { +dbOptionsTest[ + `${baseURI}?ssl=true&binary=true&application_name=app_name&fallback_application_name=f_app_name&poolSize=10` +] = { ssl: true, binary: true, application_name: 'app_name', fallback_application_name: 'f_app_name', - poolSize: 10 + poolSize: 10, }; dbOptionsTest[`${baseURI}?ssl=&binary=aa`] = { ssl: false, - binary: false -} + binary: false, +}; describe('PostgresConfigParser.getDatabaseOptionsFromURI', () => { it('creates a db options map from a query string', () => { - - for (const key in dbOptionsTest) { + for (const key in dbOptionsTest) { const result = parser.getDatabaseOptionsFromURI(key); const testObj = dbOptionsTest[key]; @@ -52,14 +50,11 @@ describe('PostgresConfigParser.getDatabaseOptionsFromURI', () => { expect(result[k]).toEqual(testObj[k]); } } - }); it('sets the poolSize to 10 if the it is not a number', () => { - const result = parser.getDatabaseOptionsFromURI(`${baseURI}?poolSize=sdf`); expect(result.poolSize).toEqual(10); - }); }); diff --git a/spec/PostgresInitOptions.spec.js b/spec/PostgresInitOptions.spec.js index f870b3b7de..92674f2107 100644 --- a/spec/PostgresInitOptions.spec.js +++ b/spec/PostgresInitOptions.spec.js @@ -1,43 +1,46 @@ const Parse = require('parse/node').Parse; -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter').default; -const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; -const ParseServer = require("../lib/index"); +const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') + .default; +const postgresURI = + 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; +const ParseServer = require('../lib/index'); const express = require('express'); //public schema const databaseOptions1 = { initOptions: { - schema: 'public' - } + schema: 'public', + }, }; //not exists schema const databaseOptions2 = { initOptions: { - schema: 'not_exists_schema' - } + schema: 'not_exists_schema', + }, }; const GameScore = Parse.Object.extend({ - className: "GameScore" + className: 'GameScore', }); function createParseServer(options) { return new Promise((resolve, reject) => { - const parseServer = new ParseServer.default(Object.assign({}, - defaultConfiguration, options, { - serverURL: "http://localhost:12666/parse", + const parseServer = new ParseServer.default( + Object.assign({}, defaultConfiguration, options, { + serverURL: 'http://localhost:12666/parse', __indexBuildCompletionCallbackForTests: promise => { - promise - .then(() => { - expect(Parse.applicationId).toEqual("test"); - const app = express(); - app.use('/parse', parseServer.app); + promise.then(() => { + expect(Parse.applicationId).toEqual('test'); + const app = express(); + app.use('/parse', parseServer.app); - const server = app.listen(12666); - Parse.serverURL = "http://localhost:12666/parse"; - resolve(server); - }, reject); - }})); + const server = app.listen(12666); + Parse.serverURL = 'http://localhost:12666/parse'; + resolve(server); + }, reject); + }, + }) + ); }); } @@ -48,27 +51,37 @@ describe_only_db('postgres')('Postgres database init options', () => { if (server) { server.close(); } - }) + }); - it('should create server with public schema databaseOptions', (done) => { + it('should create server with public schema databaseOptions', done => { const adapter = new PostgresStorageAdapter({ - uri: postgresURI, collectionPrefix: 'test_', - databaseOptions: databaseOptions1 - }) + uri: postgresURI, + collectionPrefix: 'test_', + databaseOptions: databaseOptions1, + }); - createParseServer({ databaseAdapter: adapter }).then((newServer) => { - server = newServer; - const score = new GameScore({ "score": 1337, "playerName": "Sean Plott", "cheatMode": false }); - return score.save(); - }).then(done, done.fail); + createParseServer({ databaseAdapter: adapter }) + .then(newServer => { + server = newServer; + const score = new GameScore({ + score: 1337, + playerName: 'Sean Plott', + cheatMode: false, + }); + return score.save(); + }) + .then(done, done.fail); }); - it('should fail to create server if schema databaseOptions does not exist', (done) => { + it('should fail to create server if schema databaseOptions does not exist', done => { const adapter = new PostgresStorageAdapter({ - uri: postgresURI, collectionPrefix: 'test_', - databaseOptions: databaseOptions2 - }) + uri: postgresURI, + collectionPrefix: 'test_', + databaseOptions: databaseOptions2, + }); - createParseServer({ databaseAdapter: adapter }).then(done.fail, () => done()); + createParseServer({ databaseAdapter: adapter }).then(done.fail, () => + done() + ); }); }); diff --git a/spec/PostgresStorageAdapter.spec.js b/spec/PostgresStorageAdapter.spec.js index d54cb9f2a8..c557b472b1 100644 --- a/spec/PostgresStorageAdapter.spec.js +++ b/spec/PostgresStorageAdapter.spec.js @@ -1,16 +1,22 @@ -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter').default; -const databaseURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; +const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') + .default; +const databaseURI = + 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; const getColumns = (client, className) => { - return client.map('SELECT column_name FROM information_schema.columns WHERE table_name = $', { className }, a => a.column_name); + return client.map( + 'SELECT column_name FROM information_schema.columns WHERE table_name = $', + { className }, + a => a.column_name + ); }; const dropTable = (client, className) => { return client.none('DROP TABLE IF EXISTS $', { className }); -} +}; describe_only_db('postgres')('PostgresStorageAdapter', () => { - const adapter = new PostgresStorageAdapter({ uri: databaseURI }) + const adapter = new PostgresStorageAdapter({ uri: databaseURI }); beforeEach(() => { return adapter.deleteAllClasses(); }); @@ -20,13 +26,14 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { const className = '_PushStatus'; const schema = { fields: { - "pushTime": { type: 'String' }, - "source": { type: 'String' }, - "query": { type: 'String' }, + pushTime: { type: 'String' }, + source: { type: 'String' }, + query: { type: 'String' }, }, }; - adapter.createTable(className, schema) + adapter + .createTable(className, schema) .then(() => getColumns(client, className)) .then(columns => { expect(columns).toContain('pushTime'); @@ -34,7 +41,7 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { expect(columns).toContain('query'); expect(columns).not.toContain('expiration_interval'); - schema.fields.expiration_interval = { type:'Number' }; + schema.fields.expiration_interval = { type: 'Number' }; return adapter.schemaUpgrade(className, schema); }) .then(() => getColumns(client, className)) @@ -53,13 +60,14 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { const className = 'Table'; const schema = { fields: { - "columnA": { type: 'String' }, - "columnB": { type: 'String' }, - "columnC": { type: 'String' }, + columnA: { type: 'String' }, + columnB: { type: 'String' }, + columnC: { type: 'String' }, }, }; - adapter.createTable(className, schema) + adapter + .createTable(className, schema) .then(() => getColumns(client, className)) .then(columns => { expect(columns).toContain('columnA'); @@ -82,15 +90,16 @@ describe_only_db('postgres')('PostgresStorageAdapter', () => { it('Create a table without columns and upgrade with columns', done => { const client = adapter._client; const className = 'EmptyTable'; - dropTable(client, className).then(() => adapter.createTable(className, {})) + dropTable(client, className) + .then(() => adapter.createTable(className, {})) .then(() => getColumns(client, className)) .then(columns => { expect(columns.length).toBe(0); const newSchema = { fields: { - "columnA": { type: 'String' }, - "columnB": { type: 'String' } + columnA: { type: 'String' }, + columnB: { type: 'String' }, }, }; diff --git a/spec/PromiseRouter.spec.js b/spec/PromiseRouter.spec.js index 7dc0b1a234..51a4ce21a1 100644 --- a/spec/PromiseRouter.spec.js +++ b/spec/PromiseRouter.spec.js @@ -1,25 +1,33 @@ -const PromiseRouter = require("../lib/PromiseRouter").default; +const PromiseRouter = require('../lib/PromiseRouter').default; -describe("PromiseRouter", () => { - it("should properly handle rejects", (done) => { +describe('PromiseRouter', () => { + it('should properly handle rejects', done => { const router = new PromiseRouter(); - router.route("GET", "/dummy", ()=> { - return Promise.reject({ - error: "an error", - code: -1 - }) - }, () => { - fail("this should not be called"); - }); + router.route( + 'GET', + '/dummy', + () => { + return Promise.reject({ + error: 'an error', + code: -1, + }); + }, + () => { + fail('this should not be called'); + } + ); - router.routes[0].handler({}).then((result) => { - jfail(result); - fail("this should not be called"); - done(); - }, (error)=> { - expect(error.error).toEqual("an error"); - expect(error.code).toEqual(-1); - done(); - }); + router.routes[0].handler({}).then( + result => { + jfail(result); + fail('this should not be called'); + done(); + }, + error => { + expect(error.error).toEqual('an error'); + expect(error.code).toEqual(-1); + done(); + } + ); }); -}) +}); diff --git a/spec/PublicAPI.spec.js b/spec/PublicAPI.spec.js index 0dadf6083b..97bfd0bd22 100644 --- a/spec/PublicAPI.spec.js +++ b/spec/PublicAPI.spec.js @@ -1,109 +1,139 @@ - const request = require('request'); -describe("public API", () => { - it("should get invalid_link.html", (done) => { - request('http://localhost:8378/1/apps/invalid_link.html', (err, httpResponse) => { - expect(httpResponse.statusCode).toBe(200); - done(); - }); +describe('public API', () => { + it('should get invalid_link.html', done => { + request( + 'http://localhost:8378/1/apps/invalid_link.html', + (err, httpResponse) => { + expect(httpResponse.statusCode).toBe(200); + done(); + } + ); }); - it("should get choose_password", (done) => { + it('should get choose_password', done => { reconfigureServer({ appName: 'unused', publicServerURL: 'http://localhost:8378/1', - }) - .then(() => { - request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse) => { + }).then(() => { + request( + 'http://localhost:8378/1/apps/choose_password?id=test', + (err, httpResponse) => { 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) => { - 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) => { + 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) => { - 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) => { + expect(httpResponse.statusCode).toBe(200); + done(); + } + ); }); }); -describe("public API without publicServerURL", () => { +describe('public API without publicServerURL', () => { beforeEach(done => { - reconfigureServer({ appName: 'unused' }) - .then(done, fail); + 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) => { - expect(httpResponse.statusCode).toBe(404); - done(); - }); + it('should get 404 on verify_email', done => { + request( + 'http://localhost:8378/1/apps/test/verify_email', + (err, httpResponse) => { + expect(httpResponse.statusCode).toBe(404); + done(); + } + ); }); - it("should get 404 choose_password", (done) => { - request('http://localhost:8378/1/apps/choose_password?id=test', (err, httpResponse) => { - expect(httpResponse.statusCode).toBe(404); - done(); - }); + it('should get 404 choose_password', done => { + request( + 'http://localhost:8378/1/apps/choose_password?id=test', + (err, httpResponse) => { + 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) => { - 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) => { + expect(httpResponse.statusCode).toBe(404); + done(); + } + ); }); }); - -describe("public API supplied with invalid application id", () => { +describe('public API supplied with invalid application id', () => { beforeEach(done => { - reconfigureServer({appName: "unused"}) - .then(done, fail); + reconfigureServer({ appName: 'unused' }).then(done, fail); }); - it("should get 403 on verify_email", (done) => { - request('http://localhost:8378/1/apps/invalid/verify_email', (err, httpResponse) => { - expect(httpResponse.statusCode).toBe(403); - done(); - }); + it('should get 403 on verify_email', done => { + request( + 'http://localhost:8378/1/apps/invalid/verify_email', + (err, httpResponse) => { + expect(httpResponse.statusCode).toBe(403); + done(); + } + ); }); - it("should get 403 choose_password", (done) => { - request('http://localhost:8378/1/apps/choose_password?id=invalid', (err, httpResponse) => { - expect(httpResponse.statusCode).toBe(403); - done(); - }); + it('should get 403 choose_password', done => { + request( + 'http://localhost:8378/1/apps/choose_password?id=invalid', + (err, httpResponse) => { + expect(httpResponse.statusCode).toBe(403); + done(); + } + ); }); - it("should get 403 on get of request_password_reset", (done) => { - request('http://localhost:8378/1/apps/invalid/request_password_reset', (err, httpResponse) => { - expect(httpResponse.statusCode).toBe(403); - done(); - }); + it('should get 403 on get of request_password_reset', done => { + request( + 'http://localhost:8378/1/apps/invalid/request_password_reset', + (err, httpResponse) => { + expect(httpResponse.statusCode).toBe(403); + done(); + } + ); }); - - it("should get 403 on post of request_password_reset", (done) => { - request.post('http://localhost:8378/1/apps/invalid/request_password_reset', (err, httpResponse) => { - expect(httpResponse.statusCode).toBe(403); - done(); - }); + it('should get 403 on post of request_password_reset', done => { + request.post( + 'http://localhost:8378/1/apps/invalid/request_password_reset', + (err, httpResponse) => { + expect(httpResponse.statusCode).toBe(403); + done(); + } + ); }); - it("should get 403 on resendVerificationEmail", (done) => { - request('http://localhost:8378/1/apps/invalid/resend_verification_email', (err, httpResponse) => { - expect(httpResponse.statusCode).toBe(403); - done(); - }); + it('should get 403 on resendVerificationEmail', done => { + request( + 'http://localhost:8378/1/apps/invalid/resend_verification_email', + (err, httpResponse) => { + expect(httpResponse.statusCode).toBe(403); + done(); + } + ); }); }); diff --git a/spec/PurchaseValidation.spec.js b/spec/PurchaseValidation.spec.js index 39d5b4e50d..0cce88cd26 100644 --- a/spec/PurchaseValidation.spec.js +++ b/spec/PurchaseValidation.spec.js @@ -1,208 +1,249 @@ -const request = require("request"); +const request = require('request'); function createProduct() { - const file = new Parse.File("name", { - base64: new Buffer("download_file", "utf-8").toString("base64") - }, "text"); - return file.save().then(function(){ - const product = new Parse.Object("_Product"); + const file = new Parse.File( + 'name', + { + base64: new Buffer('download_file', 'utf-8').toString('base64'), + }, + 'text' + ); + return file.save().then(function() { + const product = new Parse.Object('_Product'); product.set({ download: file, icon: file, - title: "a product", - subtitle: "a product", + title: 'a product', + subtitle: 'a product', order: 1, - productIdentifier: "a-product" - }) + productIdentifier: 'a-product', + }); return product.save(); - }) - + }); } -describe("test validate_receipt endpoint", () => { +describe('test validate_receipt endpoint', () => { beforeEach(done => { - createProduct().then(done).catch(function(){ - done(); - }); - }) - - it("should bypass appstore validation", (done) => { + createProduct() + .then(done) + .catch(function() { + done(); + }); + }); - request.post({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest'}, - url: 'http://localhost:8378/1/validate_purchase', - json: true, - body: { - productIdentifier: "a-product", - receipt: { - __type: "Bytes", - base64: new Buffer("receipt", "utf-8").toString("base64") + it('should bypass appstore validation', done => { + request.post( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', }, - bypassAppStoreValidation: true - } - }, function(err, res, body){ - if (typeof body != "object") { - fail("Body is not an object"); - done(); - } else { - expect(body.__type).toEqual("File"); - const url = body.url; - request.get({ - url: url - }, function(err, res, body) { - expect(body).toEqual("download_file"); + url: 'http://localhost:8378/1/validate_purchase', + json: true, + body: { + productIdentifier: 'a-product', + receipt: { + __type: 'Bytes', + base64: new Buffer('receipt', 'utf-8').toString('base64'), + }, + bypassAppStoreValidation: true, + }, + }, + function(err, res, body) { + if (typeof body != 'object') { + fail('Body is not an object'); done(); - }); + } else { + expect(body.__type).toEqual('File'); + const url = body.url; + request.get( + { + url: url, + }, + function(err, res, body) { + expect(body).toEqual('download_file'); + done(); + } + ); + } } - }); + ); }); - it("should fail for missing receipt", (done) => { - request.post({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest'}, - url: 'http://localhost:8378/1/validate_purchase', - json: true, - body: { - productIdentifier: "a-product", - bypassAppStoreValidation: true - } - }, function(err, res, body){ - if (typeof body != "object") { - fail("Body is not an object"); - done(); - } else { - expect(body.code).toEqual(Parse.Error.INVALID_JSON); - done(); + it('should fail for missing receipt', done => { + request.post( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + url: 'http://localhost:8378/1/validate_purchase', + json: true, + body: { + productIdentifier: 'a-product', + bypassAppStoreValidation: true, + }, + }, + function(err, res, body) { + if (typeof body != 'object') { + fail('Body is not an object'); + done(); + } else { + expect(body.code).toEqual(Parse.Error.INVALID_JSON); + done(); + } } - }); + ); }); - it("should fail for missing product identifier", (done) => { - request.post({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest'}, - url: 'http://localhost:8378/1/validate_purchase', - json: true, - body: { - receipt: { - __type: "Bytes", - base64: new Buffer("receipt", "utf-8").toString("base64") + it('should fail for missing product identifier', done => { + request.post( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', }, - bypassAppStoreValidation: true - } - }, function(err, res, body){ - if (typeof body != "object") { - fail("Body is not an object"); - done(); - } else { - expect(body.code).toEqual(Parse.Error.INVALID_JSON); - done(); + url: 'http://localhost:8378/1/validate_purchase', + json: true, + body: { + receipt: { + __type: 'Bytes', + base64: new Buffer('receipt', 'utf-8').toString('base64'), + }, + bypassAppStoreValidation: true, + }, + }, + function(err, res, body) { + if (typeof body != 'object') { + fail('Body is not an object'); + done(); + } else { + expect(body.code).toEqual(Parse.Error.INVALID_JSON); + done(); + } } - }); + ); }); - it("should bypass appstore validation and not find product", (done) => { - - request.post({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest'}, - url: 'http://localhost:8378/1/validate_purchase', - json: true, - body: { - productIdentifier: "another-product", - receipt: { - __type: "Bytes", - base64: new Buffer("receipt", "utf-8").toString("base64") + it('should bypass appstore validation and not find product', done => { + request.post( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', }, - bypassAppStoreValidation: true - } - }, function(err, res, body){ - if (typeof body != "object") { - fail("Body is not an object"); - done(); - } else { - expect(body.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); - expect(body.error).toEqual('Object not found.'); - done(); + url: 'http://localhost:8378/1/validate_purchase', + json: true, + body: { + productIdentifier: 'another-product', + receipt: { + __type: 'Bytes', + base64: new Buffer('receipt', 'utf-8').toString('base64'), + }, + bypassAppStoreValidation: true, + }, + }, + function(err, res, body) { + if (typeof body != 'object') { + fail('Body is not an object'); + done(); + } else { + expect(body.code).toEqual(Parse.Error.OBJECT_NOT_FOUND); + expect(body.error).toEqual('Object not found.'); + done(); + } } - }); + ); }); - it("should fail at appstore validation", done => { - request.post({ - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest'}, - url: 'http://localhost:8378/1/validate_purchase', - json: true, - body: { - productIdentifier: "a-product", - receipt: { - __type: "Bytes", - base64: new Buffer("receipt", "utf-8").toString("base64") + it('should fail at appstore validation', done => { + request.post( + { + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', }, + url: 'http://localhost:8378/1/validate_purchase', + json: true, + body: { + productIdentifier: 'a-product', + receipt: { + __type: 'Bytes', + base64: new Buffer('receipt', 'utf-8').toString('base64'), + }, + }, + }, + function(err, res, body) { + if (typeof body != 'object') { + fail('Body is not an object'); + } else { + expect(body.status).toBe(21002); + expect(body.error).toBe( + 'The data in the receipt-data property was malformed or missing.' + ); + } + done(); } - }, function(err, res, body){ - if (typeof body != "object") { - fail("Body is not an object"); - } else { - expect(body.status).toBe(21002); - expect(body.error).toBe('The data in the receipt-data property was malformed or missing.'); - } - done(); - }); + ); }); - it("should not create a _Product", (done) => { - const product = new Parse.Object("_Product"); - product.save().then(function(){ - fail("Should not be able to save"); - done(); - }, function(err){ - expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE); - done(); - }) + it('should not create a _Product', done => { + const product = new Parse.Object('_Product'); + product.save().then( + function() { + fail('Should not be able to save'); + done(); + }, + function(err) { + expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE); + done(); + } + ); }); - it("should be able to update a _Product", (done) => { - const query = new Parse.Query("_Product"); - query.first().then(function(product) { - if (!product) { - return Promise.reject(new Error('Product should be found')); - } - product.set("title", "a new title"); - return product.save(); - }).then(function(productAgain){ - expect(productAgain.get('downloadName')).toEqual(productAgain.get('download').name()); - expect(productAgain.get("title")).toEqual("a new title"); - done(); - }).catch(function(err){ - fail(JSON.stringify(err)); - done(); - }); + it('should be able to update a _Product', done => { + const query = new Parse.Query('_Product'); + query + .first() + .then(function(product) { + if (!product) { + return Promise.reject(new Error('Product should be found')); + } + product.set('title', 'a new title'); + return product.save(); + }) + .then(function(productAgain) { + expect(productAgain.get('downloadName')).toEqual( + productAgain.get('download').name() + ); + expect(productAgain.get('title')).toEqual('a new title'); + done(); + }) + .catch(function(err) { + fail(JSON.stringify(err)); + done(); + }); }); - it("should not be able to remove a require key in a _Product", (done) => { - const query = new Parse.Query("_Product"); - query.first().then(function(product){ - if (!product) { - return Promise.reject(new Error('Product should be found')); - } - product.unset("title"); - return product.save(); - }).then(function(){ - fail("Should not succeed"); - done(); - }).catch(function(err){ - expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE); - expect(err.message).toEqual("title is required."); - done(); - }); + it('should not be able to remove a require key in a _Product', done => { + const query = new Parse.Query('_Product'); + query + .first() + .then(function(product) { + if (!product) { + return Promise.reject(new Error('Product should be found')); + } + product.unset('title'); + return product.save(); + }) + .then(function() { + fail('Should not succeed'); + done(); + }) + .catch(function(err) { + expect(err.code).toEqual(Parse.Error.INCORRECT_TYPE); + expect(err.message).toEqual('title is required.'); + done(); + }); }); }); diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index f512f96292..40eccd2ba6 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -1,415 +1,475 @@ -"use strict"; -const PushController = require('../lib/Controllers/PushController').PushController; +'use strict'; +const PushController = require('../lib/Controllers/PushController') + .PushController; const StatusHandler = require('../lib/StatusHandler'); const Config = require('../lib/Config'); const validatePushType = require('../lib/Push/utils').validatePushType; const successfulTransmissions = function(body, installations) { - - const promises = installations.map((device) => { + const promises = installations.map(device => { return Promise.resolve({ transmitted: true, device: device, - }) + }); }); return Promise.all(promises); -} +}; const successfulIOS = function(body, installations) { - - const promises = installations.map((device) => { + const promises = installations.map(device => { return Promise.resolve({ - transmitted: device.deviceType == "ios", + transmitted: device.deviceType == 'ios', device: device, - }) + }); }); return Promise.all(promises); -} +}; describe('PushController', () => { - it('can validate device type when no device type is set', (done) => { + it('can validate device type when no device type is set', done => { // Make query condition - const where = { - }; + const where = {}; const validPushTypes = ['ios', 'android']; - expect(function(){ + expect(function() { validatePushType(where, validPushTypes); }).not.toThrow(); done(); }); - it('can validate device type when single valid device type is set', (done) => { + it('can validate device type when single valid device type is set', done => { // Make query condition const where = { - 'deviceType': 'ios' + deviceType: 'ios', }; const validPushTypes = ['ios', 'android']; - expect(function(){ + expect(function() { validatePushType(where, validPushTypes); }).not.toThrow(); done(); }); - it('can validate device type when multiple valid device types are set', (done) => { + it('can validate device type when multiple valid device types are set', done => { // Make query condition const where = { - 'deviceType': { - '$in': ['android', 'ios'] - } + deviceType: { + $in: ['android', 'ios'], + }, }; const validPushTypes = ['ios', 'android']; - expect(function(){ + expect(function() { validatePushType(where, validPushTypes); }).not.toThrow(); done(); }); - it('can throw on validateDeviceType when single invalid device type is set', (done) => { + it('can throw on validateDeviceType when single invalid device type is set', done => { // Make query condition const where = { - 'deviceType': 'osx' + deviceType: 'osx', }; const validPushTypes = ['ios', 'android']; - expect(function(){ + expect(function() { validatePushType(where, validPushTypes); }).toThrow(); done(); }); - it('can throw on validateDeviceType when single invalid device type is set', (done) => { + it('can throw on validateDeviceType when single invalid device type is set', done => { // Make query condition const where = { - 'deviceType': 'osx' + deviceType: 'osx', }; const validPushTypes = ['ios', 'android']; - expect(function(){ + expect(function() { validatePushType(where, validPushTypes); }).toThrow(); done(); }); - it('can get expiration time in string format', (done) => { + it('can get expiration time in string format', done => { // Make mock request const timeStr = '2015-03-19T22:05:08Z'; const body = { - 'expiration_time': timeStr - } + expiration_time: timeStr, + }; const time = PushController.getExpirationTime(body); expect(time).toEqual(new Date(timeStr).valueOf()); done(); }); - it('can get expiration time in number format', (done) => { + it('can get expiration time in number format', done => { // Make mock request const timeNumber = 1426802708; const body = { - 'expiration_time': timeNumber - } + expiration_time: timeNumber, + }; const time = PushController.getExpirationTime(body); expect(time).toEqual(timeNumber * 1000); done(); }); - it('can throw on getExpirationTime in invalid format', (done) => { + it('can throw on getExpirationTime in invalid format', done => { // Make mock request const body = { - 'expiration_time': 'abcd' - } + expiration_time: 'abcd', + }; - expect(function(){ + expect(function() { PushController.getExpirationTime(body); }).toThrow(); done(); }); - it('can get push time in string format', (done) => { + it('can get push time in string format', done => { // Make mock request const timeStr = '2015-03-19T22:05:08Z'; const body = { - 'push_time': timeStr - } + push_time: timeStr, + }; const { date } = PushController.getPushTime(body); expect(date).toEqual(new Date(timeStr)); done(); }); - it('can get push time in number format', (done) => { + it('can get push time in number format', done => { // Make mock request const timeNumber = 1426802708; const body = { - 'push_time': timeNumber - } + push_time: timeNumber, + }; const { date } = PushController.getPushTime(body); expect(date.valueOf()).toEqual(timeNumber * 1000); done(); }); - it('can throw on getPushTime in invalid format', (done) => { + it('can throw on getPushTime in invalid format', done => { // Make mock request const body = { - 'push_time': 'abcd' - } + push_time: 'abcd', + }; - expect(function(){ + expect(function() { PushController.getPushTime(body); }).toThrow(); done(); }); - it('properly increment badges', (done) => { + it('properly increment badges', done => { const pushAdapter = { send: function(body, installations) { const badge = body.data.badge; - installations.forEach((installation) => { + installations.forEach(installation => { expect(installation.badge).toEqual(badge); expect(installation.originalBadge + 1).toEqual(installation.badge); - }) + }); return successfulTransmissions(body, installations); }, getValidPushTypes: function() { - return ["ios", "android"]; - } - } - const payload = {data:{ - alert: "Hello World!", - badge: "Increment", - }} + return ['ios', 'android']; + }, + }; + const payload = { + data: { + alert: 'Hello World!', + badge: 'Increment', + }, + }; const installations = []; - while(installations.length != 10) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } - while(installations.length != 15) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length); - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "android"); + while (installations.length != 15) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'android'); installations.push(installation); } const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const pushController = new PushController(); reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - return Parse.Object.saveAll(installations) - }).then(() => { - return pushController.sendPush(payload, {}, config, auth); - }).then(() => { - // Wait so the push is completed. - return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); - }).then(() => { - // Check we actually sent 15 pushes. - const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }) - }).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('numSent')).toBe(15); - }).then(() => { - // Check that the installations were actually updated. - const query = new Parse.Query('_Installation'); - return query.find({ useMasterKey: true }) - }).then((results) => { - expect(results.length).toBe(15); - for (let i = 0; i < 15; i++) { - const installation = results[i]; - expect(installation.get('badge')).toBe(parseInt(installation.get('originalBadge')) + 1); - } - done() - }).catch((err) => { - jfail(err); - done(); - }); + push: { adapter: pushAdapter }, + }) + .then(() => { + return Parse.Object.saveAll(installations); + }) + .then(() => { + return pushController.sendPush(payload, {}, config, auth); + }) + .then(() => { + // Wait so the push is completed. + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + // Check we actually sent 15 pushes. + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('numSent')).toBe(15); + }) + .then(() => { + // Check that the installations were actually updated. + const query = new Parse.Query('_Installation'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + expect(results.length).toBe(15); + for (let i = 0; i < 15; i++) { + const installation = results[i]; + expect(installation.get('badge')).toBe( + parseInt(installation.get('originalBadge')) + 1 + ); + } + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); - it('properly increment badges by more than 1', (done) => { + it('properly increment badges by more than 1', done => { const pushAdapter = { send: function(body, installations) { const badge = body.data.badge; - installations.forEach((installation) => { + installations.forEach(installation => { expect(installation.badge).toEqual(badge); expect(installation.originalBadge + 3).toEqual(installation.badge); - }) + }); return successfulTransmissions(body, installations); }, getValidPushTypes: function() { - return ["ios", "android"]; - } - } - const payload = {data:{ - alert: "Hello World!", - badge: { __op: 'Increment', amount: 3 }, - }} + return ['ios', 'android']; + }, + }; + const payload = { + data: { + alert: 'Hello World!', + badge: { __op: 'Increment', amount: 3 }, + }, + }; const installations = []; - while(installations.length != 10) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } - while(installations.length != 15) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length); - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "android"); + while (installations.length != 15) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'android'); installations.push(installation); } const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const pushController = new PushController(); reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - return Parse.Object.saveAll(installations) - }).then(() => { - return pushController.sendPush(payload, {}, config, auth); - }).then(() => { - // Wait so the push is completed. - return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); - }).then(() => { - // Check we actually sent 15 pushes. - const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }) - }).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('numSent')).toBe(15); - }).then(() => { - // Check that the installations were actually updated. - const query = new Parse.Query('_Installation'); - return query.find({ useMasterKey: true }) - }).then((results) => { - expect(results.length).toBe(15); - for (let i = 0; i < 15; i++) { - const installation = results[i]; - expect(installation.get('badge')).toBe(parseInt(installation.get('originalBadge')) + 3); - } - done() - }).catch((err) => { - jfail(err); - done(); - }); + push: { adapter: pushAdapter }, + }) + .then(() => { + return Parse.Object.saveAll(installations); + }) + .then(() => { + return pushController.sendPush(payload, {}, config, auth); + }) + .then(() => { + // Wait so the push is completed. + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + // Check we actually sent 15 pushes. + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('numSent')).toBe(15); + }) + .then(() => { + // Check that the installations were actually updated. + const query = new Parse.Query('_Installation'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + expect(results.length).toBe(15); + for (let i = 0; i < 15; i++) { + const installation = results[i]; + expect(installation.get('badge')).toBe( + parseInt(installation.get('originalBadge')) + 3 + ); + } + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); - it('properly set badges to 1', (done) => { - + it('properly set badges to 1', done => { const pushAdapter = { send: function(body, installations) { const badge = body.data.badge; - installations.forEach((installation) => { + installations.forEach(installation => { expect(installation.badge).toEqual(badge); expect(1).toEqual(installation.badge); - }) + }); return successfulTransmissions(body, installations); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; - const payload = {data: { - alert: "Hello World!", - badge: 1, - }} + const payload = { + data: { + alert: 'Hello World!', + badge: 1, + }, + }; const installations = []; - while(installations.length != 10) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const pushController = new PushController(); reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - return Parse.Object.saveAll(installations) - }).then(() => { - return pushController.sendPush(payload, {}, config, auth); - }).then(() => { - // Wait so the push is completed. - return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); - }).then(() => { - // Check we actually sent the pushes. - const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }) - }).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('numSent')).toBe(10); - }).then(() => { - // Check that the installations were actually updated. - const query = new Parse.Query('_Installation'); - return query.find({ useMasterKey: true }) - }).then((results) => { - expect(results.length).toBe(10); - for (let i = 0; i < 10; i++) { - const installation = results[i]; - expect(installation.get('badge')).toBe(1); - } - done() - }).catch((err) => { - jfail(err); - done(); - }); + push: { adapter: pushAdapter }, + }) + .then(() => { + return Parse.Object.saveAll(installations); + }) + .then(() => { + return pushController.sendPush(payload, {}, config, auth); + }) + .then(() => { + // Wait so the push is completed. + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + // Check we actually sent the pushes. + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('numSent')).toBe(10); + }) + .then(() => { + // Check that the installations were actually updated. + const query = new Parse.Query('_Installation'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + expect(results.length).toBe(10); + for (let i = 0; i < 10; i++) { + const installation = results[i]; + expect(installation.get('badge')).toBe(1); + } + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); - it('properly set badges to 1 with complex query #2903 #3022', (done) => { - + it('properly set badges to 1 with complex query #2903 #3022', done => { const payload = { data: { - alert: "Hello World!", + alert: 'Hello World!', badge: 1, - } - } + }, + }; const installations = []; - while(installations.length != 10) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } let matchedInstallationsCount = 0; @@ -417,115 +477,133 @@ describe('PushController', () => { send: function(body, installations) { matchedInstallationsCount += installations.length; const badge = body.data.badge; - installations.forEach((installation) => { + installations.forEach(installation => { expect(installation.badge).toEqual(badge); expect(1).toEqual(installation.badge); - }) + }); return successfulTransmissions(body, installations); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const pushController = new PushController(); reconfigureServer({ push: { - adapter: pushAdapter - } - }).then(() => { - return Parse.Object.saveAll(installations) - }).then((installations) => { - const objectIds = installations.map(installation => { - return installation.id; - }) - const where = { - objectId: {'$in': objectIds.slice(0, 5)} - } - return pushController.sendPush(payload, where, config, auth); - }).then(() => { - return new Promise((res) => { - setTimeout(res, 300); + adapter: pushAdapter, + }, + }) + .then(() => { + return Parse.Object.saveAll(installations); + }) + .then(installations => { + const objectIds = installations.map(installation => { + return installation.id; + }); + const where = { + objectId: { $in: objectIds.slice(0, 5) }, + }; + return pushController.sendPush(payload, where, config, auth); + }) + .then(() => { + return new Promise(res => { + setTimeout(res, 300); + }); + }) + .then(() => { + expect(matchedInstallationsCount).toBe(5); + const query = new Parse.Query(Parse.Installation); + query.equalTo('badge', 1); + return query.find({ useMasterKey: true }); + }) + .then(installations => { + expect(installations.length).toBe(5); + done(); + }) + .catch(() => { + fail('should not fail'); + done(); }); - }).then(() => { - expect(matchedInstallationsCount).toBe(5); - const query = new Parse.Query(Parse.Installation); - query.equalTo('badge', 1); - return query.find({useMasterKey: true}); - }).then((installations) => { - expect(installations.length).toBe(5); - done(); - }).catch(() => { - fail("should not fail"); - done(); - }); }); - it('properly creates _PushStatus', (done) => { + it('properly creates _PushStatus', done => { const pushStatusAfterSave = { - handler: function() {} + handler: function() {}, }; const spy = spyOn(pushStatusAfterSave, 'handler').and.callThrough(); Parse.Cloud.afterSave('_PushStatus', pushStatusAfterSave.handler); const installations = []; - while(installations.length != 10) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } - while(installations.length != 15) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("deviceType", "android"); + while (installations.length != 15) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('deviceType', 'android'); installations.push(installation); } - const payload = {data: { - alert: "Hello World!", - badge: 1, - }} + const payload = { + data: { + alert: 'Hello World!', + badge: 1, + }, + }; const pushAdapter = { send: function(body, installations) { return successfulIOS(body, installations); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const pushController = new PushController(); reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - return Parse.Object.saveAll(installations); + push: { adapter: pushAdapter }, }) + .then(() => { + return Parse.Object.saveAll(installations); + }) .then(() => { return pushController.sendPush(payload, {}, config, auth); - }).then(() => { + }) + .then(() => { // it is enqueued so it can take time - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); - }).then(() => { + }) + .then(() => { const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}); - }).then((results) => { + return query.find({ useMasterKey: true }); + }) + .then(results => { expect(results.length).toBe(1); const result = results[0]; expect(result.createdAt instanceof Date).toBe(true); @@ -533,21 +611,22 @@ describe('PushController', () => { expect(result.id.length).toBe(10); expect(result.get('source')).toEqual('rest'); expect(result.get('query')).toEqual(JSON.stringify({})); - expect(typeof result.get('payload')).toEqual("string"); + expect(typeof result.get('payload')).toEqual('string'); expect(JSON.parse(result.get('payload'))).toEqual(payload.data); expect(result.get('status')).toEqual('succeeded'); expect(result.get('numSent')).toEqual(10); expect(result.get('sentPerType')).toEqual({ - 'ios': 10 // 10 ios + ios: 10, // 10 ios }); expect(result.get('numFailed')).toEqual(5); expect(result.get('failedPerType')).toEqual({ - 'android': 5 // android + android: 5, // android }); // Try to get it without masterKey const query = new Parse.Query('_PushStatus'); return query.find(); - }).catch((error) => { + }) + .catch(error => { expect(error.code).toBe(119); }) .then(() => { @@ -557,7 +636,7 @@ describe('PushController', () => { expect(spy).toHaveBeenCalled(); expect(spy.calls.count()).toBe(4); const allCalls = spy.calls.all(); - allCalls.forEach((call) => { + allCalls.forEach(call => { expect(call.args.length).toBe(1); const object = call.args[0].object; expect(object instanceof Parse.Object).toBe(true); @@ -571,335 +650,409 @@ describe('PushController', () => { // Those are updated from a nested . operation, this would // not render correctly before expect(getPushStatus(2).get('failedPerType')).toEqual({ - android: 5 + android: 5, }); expect(getPushStatus(2).get('sentPerType')).toEqual({ - ios: 10 + ios: 10, }); expect(getPushStatus(3).get('status')).toBe('succeeded'); }) - .then(done).catch(done.fail); + .then(done) + .catch(done.fail); }); - it('properly creates _PushStatus without serverURL', (done) => { + it('properly creates _PushStatus without serverURL', done => { const pushStatusAfterSave = { - handler: function() {} + handler: function() {}, }; Parse.Cloud.afterSave('_PushStatus', pushStatusAfterSave.handler); - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation"); - installation.set("deviceToken","device_token") - installation.set("badge", 0); - installation.set("originalBadge", 0); - installation.set("deviceType", "ios"); - - const payload = {data: { - alert: "Hello World!", - badge: 1, - }} + const installation = new Parse.Object('_Installation'); + installation.set('installationId', 'installation'); + installation.set('deviceToken', 'device_token'); + installation.set('badge', 0); + installation.set('originalBadge', 0); + installation.set('deviceType', 'ios'); + + const payload = { + data: { + alert: 'Hello World!', + badge: 1, + }, + }; const pushAdapter = { send: function(body, installations) { return successfulIOS(body, installations); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const pushController = new PushController(); - return installation.save().then(() => { - return reconfigureServer({ - serverURL: 'http://localhost:8378/', // server with borked URL - push: { adapter: pushAdapter } + return installation + .save() + .then(() => { + return reconfigureServer({ + serverURL: 'http://localhost:8378/', // server with borked URL + push: { adapter: pushAdapter }, + }); }) - }) .then(() => { return pushController.sendPush(payload, {}, config, auth); - }).then(() => { + }) + .then(() => { // it is enqueued so it can take time - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); - }).then(() => { + }) + .then(() => { Parse.serverURL = 'http://localhost:8378/1'; // GOOD url const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}); - }).then((results) => { + return query.find({ useMasterKey: true }); + }) + .then(results => { expect(results.length).toBe(1); }) - .then(done).catch(done.fail); + .then(done) + .catch(done.fail); }); - it('should properly report failures in _PushStatus', (done) => { + it('should properly report failures in _PushStatus', done => { const pushAdapter = { send: function(body, installations) { - return installations.map((installation) => { + return installations.map(installation => { return Promise.resolve({ - deviceType: installation.deviceType - }) - }) + deviceType: installation.deviceType, + }); + }); }, getValidPushTypes: function() { - return ["ios"]; - } - } - const where = { 'channels': { - '$ins': ['Giants', 'Mets'] - }}; - const payload = {data: { - alert: "Hello World!", - badge: 1, - }} + return ['ios']; + }, + }; + const where = { + channels: { + $ins: ['Giants', 'Mets'], + }, + }; + const payload = { + data: { + alert: 'Hello World!', + badge: 1, + }, + }; const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const pushController = new PushController(); reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - return pushController.sendPush(payload, where, config, auth) - }).then(() => { - fail('should not succeed'); - done(); - }).catch(() => { - const query = new Parse.Query('_PushStatus'); - query.find({useMasterKey: true}).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('status')).toBe('failed'); + push: { adapter: pushAdapter }, + }) + .then(() => { + return pushController.sendPush(payload, where, config, auth); + }) + .then(() => { + fail('should not succeed'); done(); + }) + .catch(() => { + const query = new Parse.Query('_PushStatus'); + query.find({ useMasterKey: true }).then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('status')).toBe('failed'); + done(); + }); }); - }); }); - it('should support full RESTQuery for increment', (done) => { - const payload = {data: { - alert: "Hello World!", - badge: 'Increment', - }} + it('should support full RESTQuery for increment', done => { + const payload = { + data: { + alert: 'Hello World!', + badge: 'Increment', + }, + }; const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const where = { - 'deviceToken': { - '$in': ['device_token_0', 'device_token_1', 'device_token_2'] - } - } + deviceToken: { + $in: ['device_token_0', 'device_token_1', 'device_token_2'], + }, + }; const pushController = new PushController(); reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - const installations = []; - while (installations.length != 5) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken", "device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); - installations.push(installation); - } - return Parse.Object.saveAll(installations); - }).then(() => { - return pushController.sendPush(payload, where, config, auth); - }).then(() => { - // Wait so the push is completed. - return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); - }).then(() => { - const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }) - }).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('numSent')).toBe(3); - done(); - }).catch((err) => { - jfail(err); - done(); - }); + push: { adapter: pushAdapter }, + }) + .then(() => { + const installations = []; + while (installations.length != 5) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set( + 'deviceToken', + 'device_token_' + installations.length + ); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); + installations.push(installation); + } + return Parse.Object.saveAll(installations); + }) + .then(() => { + return pushController.sendPush(payload, where, config, auth); + }) + .then(() => { + // Wait so the push is completed. + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('numSent')).toBe(3); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); - it('should support object type for alert', (done) => { - const payload = {data: { - alert: { - 'loc-key': 'hello_world', + it('should support object type for alert', done => { + const payload = { + data: { + alert: { + 'loc-key': 'hello_world', + }, }, - }} + }; const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const where = { - 'deviceType': 'ios' - } + deviceType: 'ios', + }; const pushController = new PushController(); reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - const installations = []; - while (installations.length != 5) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken", "device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); - installations.push(installation); - } - return Parse.Object.saveAll(installations); - }).then(() => { - return pushController.sendPush(payload, where, config, auth) - }).then(() => { - // Wait so the push is completed. - return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); - }).then(() => { - const query = new Parse.Query('_PushStatus'); - return query.find({ useMasterKey: true }) - }).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('numSent')).toBe(5); - done(); - }).catch(() => { - fail('should not fail'); - done(); - }); + push: { adapter: pushAdapter }, + }) + .then(() => { + const installations = []; + while (installations.length != 5) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set( + 'deviceToken', + 'device_token_' + installations.length + ); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); + installations.push(installation); + } + return Parse.Object.saveAll(installations); + }) + .then(() => { + return pushController.sendPush(payload, where, config, auth); + }) + .then(() => { + // Wait so the push is completed. + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }); + }) + .then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('numSent')).toBe(5); + done(); + }) + .catch(() => { + fail('should not fail'); + done(); + }); }); it('should flatten', () => { - const res = StatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]]) - expect(res).toEqual([1,2,3,4,5,6]); + const res = StatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]]); + expect(res).toEqual([1, 2, 3, 4, 5, 6]); }); it('properly transforms push time', () => { expect(PushController.getPushTime()).toBe(undefined); - expect(PushController.getPushTime({ - 'push_time': 1000 - }).date).toEqual(new Date(1000 * 1000)); - expect(PushController.getPushTime({ - 'push_time': '2017-01-01' - }).date).toEqual(new Date('2017-01-01')); - - expect(() => {PushController.getPushTime({ - 'push_time': 'gibberish-time' - })}).toThrow(); - expect(() => {PushController.getPushTime({ - 'push_time': Number.NaN - })}).toThrow(); - - expect(PushController.getPushTime({ - push_time: '2017-09-06T13:42:48.369Z' - })).toEqual({ + expect( + PushController.getPushTime({ + push_time: 1000, + }).date + ).toEqual(new Date(1000 * 1000)); + expect( + PushController.getPushTime({ + push_time: '2017-01-01', + }).date + ).toEqual(new Date('2017-01-01')); + + expect(() => { + PushController.getPushTime({ + push_time: 'gibberish-time', + }); + }).toThrow(); + expect(() => { + PushController.getPushTime({ + push_time: Number.NaN, + }); + }).toThrow(); + + expect( + PushController.getPushTime({ + push_time: '2017-09-06T13:42:48.369Z', + }) + ).toEqual({ date: new Date('2017-09-06T13:42:48.369Z'), isLocalTime: false, }); - expect(PushController.getPushTime({ - push_time: '2007-04-05T12:30-02:00', - })).toEqual({ + expect( + PushController.getPushTime({ + push_time: '2007-04-05T12:30-02:00', + }) + ).toEqual({ date: new Date('2007-04-05T12:30-02:00'), isLocalTime: false, }); - expect(PushController.getPushTime({ - push_time: '2007-04-05T12:30', - })).toEqual({ + expect( + PushController.getPushTime({ + push_time: '2007-04-05T12:30', + }) + ).toEqual({ date: new Date('2007-04-05T12:30'), isLocalTime: true, }); }); - it('should not schedule push when not configured', (done) => { + it('should not schedule push when not configured', done => { const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const pushController = new PushController(); const payload = { data: { alert: 'hello', }, - push_time: new Date().getTime() - } + push_time: new Date().getTime(), + }; const installations = []; - while(installations.length != 10) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - return Parse.Object.saveAll(installations).then(() => { - return pushController.sendPush(payload, {}, config, auth); - }).then(() => new Promise(resolve => setTimeout(resolve, 300))); - }).then(() => { - const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('status')).not.toBe('scheduled'); + push: { adapter: pushAdapter }, + }) + .then(() => { + return Parse.Object.saveAll(installations) + .then(() => { + return pushController.sendPush(payload, {}, config, auth); + }) + .then(() => new Promise(resolve => setTimeout(resolve, 300))); + }) + .then(() => { + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }).then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('status')).not.toBe('scheduled'); + done(); + }); + }) + .catch(err => { + console.error(err); + fail('should not fail'); done(); }); - }).catch((err) => { - console.error(err); - fail('should not fail'); - done(); - }); }); - it('should schedule push when configured', (done) => { + it('should schedule push when configured', done => { const auth = { - isMaster: true - } + isMaster: true, + }; const pushAdapter = { send: function(body, installations) { - const promises = installations.map((device) => { + const promises = installations.map(device => { if (!device.deviceToken) { // Simulate error when device token is not set return Promise.reject(); @@ -907,60 +1060,69 @@ describe('PushController', () => { return Promise.resolve({ transmitted: true, device: device, - }) + }); }); return Promise.all(promises); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const pushController = new PushController(); const payload = { data: { alert: 'hello', }, - push_time: new Date().getTime() / 1000 - } + push_time: new Date().getTime() / 1000, + }; const installations = []; - while(installations.length != 10) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } reconfigureServer({ push: { adapter: pushAdapter }, - scheduledPush: true - }).then(() => { - const config = Config.get(Parse.applicationId); - return Parse.Object.saveAll(installations).then(() => { - return pushController.sendPush(payload, {}, config, auth); - }).then(() => new Promise(resolve => setTimeout(resolve, 300))); - }).then(() => { - const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('status')).toBe('scheduled'); - }); - }).then(done).catch(done.err); + scheduledPush: true, + }) + .then(() => { + const config = Config.get(Parse.applicationId); + return Parse.Object.saveAll(installations) + .then(() => { + return pushController.sendPush(payload, {}, config, auth); + }) + .then(() => new Promise(resolve => setTimeout(resolve, 300))); + }) + .then(() => { + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }).then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('status')).toBe('scheduled'); + }); + }) + .then(done) + .catch(done.err); }); - it('should not enqueue push when device token is not set', (done) => { + it('should not enqueue push when device token is not set', done => { const auth = { - isMaster: true - } + isMaster: true, + }; const pushAdapter = { send: function(body, installations) { - const promises = installations.map((device) => { + const promises = installations.map(device => { if (!device.deviceToken) { // Simulate error when device token is not set return Promise.reject(); @@ -968,74 +1130,85 @@ describe('PushController', () => { return Promise.resolve({ transmitted: true, device: device, - }) + }); }); return Promise.all(promises); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const pushController = new PushController(); const payload = { data: { alert: 'hello', }, - push_time: new Date().getTime() / 1000 - } + push_time: new Date().getTime() / 1000, + }; const installations = []; - while(installations.length != 5) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 5) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('deviceToken', 'device_token_' + installations.length); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } - while(installations.length != 15) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 15) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - const config = Config.get(Parse.applicationId); - return Parse.Object.saveAll(installations).then(() => { - return pushController.sendPush(payload, {}, config, auth); - }).then(() => new Promise(resolve => setTimeout(resolve, 100))); - }).then(() => { - const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('numSent')).toBe(5); - expect(pushStatus.get('status')).toBe('succeeded'); + push: { adapter: pushAdapter }, + }) + .then(() => { + const config = Config.get(Parse.applicationId); + return Parse.Object.saveAll(installations) + .then(() => { + return pushController.sendPush(payload, {}, config, auth); + }) + .then(() => new Promise(resolve => setTimeout(resolve, 100))); + }) + .then(() => { + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }).then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('numSent')).toBe(5); + expect(pushStatus.get('status')).toBe('succeeded'); + done(); + }); + }) + .catch(err => { + console.error(err); + fail('should not fail'); done(); }); - }).catch((err) => { - console.error(err); - fail('should not fail'); - done(); - }); }); - it('should not mark the _PushStatus as failed when audience has no deviceToken', (done) => { + it('should not mark the _PushStatus as failed when audience has no deviceToken', done => { const auth = { - isMaster: true - } + isMaster: true, + }; const pushAdapter = { send: function(body, installations) { - const promises = installations.map((device) => { + const promises = installations.map(device => { if (!device.deviceToken) { // Simulate error when device token is not set return Promise.reject(); @@ -1043,299 +1216,368 @@ describe('PushController', () => { return Promise.resolve({ transmitted: true, device: device, - }) + }); }); return Promise.all(promises); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const pushController = new PushController(); const payload = { data: { alert: 'hello', }, - push_time: new Date().getTime() / 1000 - } + push_time: new Date().getTime() / 1000, + }; const installations = []; - while(installations.length != 5) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); + while (installations.length != 5) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); installations.push(installation); } reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - const config = Config.get(Parse.applicationId); - return Parse.Object.saveAll(installations).then(() => { - return pushController.sendPush(payload, {}, config, auth) - }).then(() => new Promise(resolve => setTimeout(resolve, 100))); - }).then(() => { - const query = new Parse.Query('_PushStatus'); - return query.find({useMasterKey: true}).then((results) => { - expect(results.length).toBe(1); - const pushStatus = results[0]; - expect(pushStatus.get('numSent')).toBe(0); - expect(pushStatus.get('status')).toBe('succeeded'); + push: { adapter: pushAdapter }, + }) + .then(() => { + const config = Config.get(Parse.applicationId); + return Parse.Object.saveAll(installations) + .then(() => { + return pushController.sendPush(payload, {}, config, auth); + }) + .then(() => new Promise(resolve => setTimeout(resolve, 100))); + }) + .then(() => { + const query = new Parse.Query('_PushStatus'); + return query.find({ useMasterKey: true }).then(results => { + expect(results.length).toBe(1); + const pushStatus = results[0]; + expect(pushStatus.get('numSent')).toBe(0); + expect(pushStatus.get('status')).toBe('succeeded'); + done(); + }); + }) + .catch(err => { + console.error(err); + fail('should not fail'); done(); }); - }).catch((err) => { - console.error(err); - fail('should not fail'); - done(); - }); }); - it('should support localized payload data', (done) => { - const payload = {data: { - alert: 'Hello!', - 'alert-fr': 'Bonjour', - 'alert-es': 'Ola' - }} + it('should support localized payload data', done => { + const payload = { + data: { + alert: 'Hello!', + 'alert-fr': 'Bonjour', + 'alert-es': 'Ola', + }, + }; const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; const where = { - 'deviceType': 'ios' - } + deviceType: 'ios', + }; spyOn(pushAdapter, 'send').and.callThrough(); const pushController = new PushController(); reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - const installations = []; - while (installations.length != 5) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken", "device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); - installations.push(installation); - } - installations[0].set('localeIdentifier', 'fr-CA'); - installations[1].set('localeIdentifier', 'fr-FR'); - installations[2].set('localeIdentifier', 'en-US'); - return Parse.Object.saveAll(installations); - }).then(() => { - return pushController.sendPush(payload, where, config, auth) - }).then(() => { - // Wait so the push is completed. - return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); - }).then(() => { - expect(pushAdapter.send.calls.count()).toBe(2); - const firstCall = pushAdapter.send.calls.first(); - expect(firstCall.args[0].data).toEqual({ - alert: 'Hello!' - }); - expect(firstCall.args[1].length).toBe(3); // 3 installations + push: { adapter: pushAdapter }, + }) + .then(() => { + const installations = []; + while (installations.length != 5) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set( + 'deviceToken', + 'device_token_' + installations.length + ); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); + installations.push(installation); + } + installations[0].set('localeIdentifier', 'fr-CA'); + installations[1].set('localeIdentifier', 'fr-FR'); + installations[2].set('localeIdentifier', 'en-US'); + return Parse.Object.saveAll(installations); + }) + .then(() => { + return pushController.sendPush(payload, where, config, auth); + }) + .then(() => { + // Wait so the push is completed. + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + expect(pushAdapter.send.calls.count()).toBe(2); + const firstCall = pushAdapter.send.calls.first(); + expect(firstCall.args[0].data).toEqual({ + alert: 'Hello!', + }); + expect(firstCall.args[1].length).toBe(3); // 3 installations - const lastCall = pushAdapter.send.calls.mostRecent(); - expect(lastCall.args[0].data).toEqual({ - alert: 'Bonjour' - }); - expect(lastCall.args[1].length).toBe(2); // 2 installations - // No installation is in es so only 1 call for fr, and another for default - done(); - }).catch(done.fail); + const lastCall = pushAdapter.send.calls.mostRecent(); + expect(lastCall.args[0].data).toEqual({ + alert: 'Bonjour', + }); + expect(lastCall.args[1].length).toBe(2); // 2 installations + // No installation is in es so only 1 call for fr, and another for default + done(); + }) + .catch(done.fail); }); - it('should update audiences', (done) => { + it('should update audiences', done => { const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { - return ["ios"]; - } - } + return ['ios']; + }, + }; const config = Config.get(Parse.applicationId); const auth = { - isMaster: true - } + isMaster: true, + }; let audienceId = null; const now = new Date(); let timesUsed = 0; const where = { - 'deviceType': 'ios' - } + deviceType: 'ios', + }; spyOn(pushAdapter, 'send').and.callThrough(); const pushController = new PushController(); reconfigureServer({ - push: { adapter: pushAdapter } - }).then(() => { - const installations = []; - while (installations.length != 5) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", installations.length); - installation.set("originalBadge", installations.length); - installation.set("deviceType", "ios"); - installations.push(installation); - } - return Parse.Object.saveAll(installations); - }).then(() => { - // Create an audience - const query = new Parse.Query("_Audience"); - query.descending("createdAt"); - query.equalTo("query", JSON.stringify(where)); - const parseResults = (results) => { - if (results.length > 0) { - audienceId = results[0].id; - timesUsed = results[0].get('timesUsed'); - if (!isFinite(timesUsed)) { - timesUsed = 0; - } + push: { adapter: pushAdapter }, + }) + .then(() => { + const installations = []; + while (installations.length != 5) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set( + 'deviceToken', + 'device_token_' + installations.length + ); + installation.set('badge', installations.length); + installation.set('originalBadge', installations.length); + installation.set('deviceType', 'ios'); + installations.push(installation); } - } - const audience = new Parse.Object("_Audience"); - audience.set("name", "testAudience") - audience.set("query", JSON.stringify(where)); - return Parse.Object.saveAll(audience).then(() => { - return query.find({ useMasterKey: true }).then(parseResults); - }); - }).then(() => { - const body = { - data: { alert: 'hello' }, - audience_id: audienceId - } - return pushController.sendPush(body, where, config, auth) - }).then(() => { - // Wait so the push is completed. - return new Promise((resolve) => { setTimeout(() => { resolve(); }, 1000); }); - }).then(() => { - expect(pushAdapter.send.calls.count()).toBe(1); - const firstCall = pushAdapter.send.calls.first(); - expect(firstCall.args[0].data).toEqual({ - alert: 'hello' - }); - expect(firstCall.args[1].length).toBe(5); - }).then(() => { - // Get the audience we used above. - const query = new Parse.Query("_Audience"); - query.equalTo("objectId", audienceId); - return query.find({ useMasterKey: true }) - }).then((results) => { - const audience = results[0]; - expect(audience.get('query')).toBe(JSON.stringify(where)); - expect(audience.get('timesUsed')).toBe(timesUsed + 1); - expect(audience.get('lastUsed')).not.toBeLessThan(now); - }).then(() => { - done(); - }).catch(done.fail); + return Parse.Object.saveAll(installations); + }) + .then(() => { + // Create an audience + const query = new Parse.Query('_Audience'); + query.descending('createdAt'); + query.equalTo('query', JSON.stringify(where)); + const parseResults = results => { + if (results.length > 0) { + audienceId = results[0].id; + timesUsed = results[0].get('timesUsed'); + if (!isFinite(timesUsed)) { + timesUsed = 0; + } + } + }; + const audience = new Parse.Object('_Audience'); + audience.set('name', 'testAudience'); + audience.set('query', JSON.stringify(where)); + return Parse.Object.saveAll(audience).then(() => { + return query.find({ useMasterKey: true }).then(parseResults); + }); + }) + .then(() => { + const body = { + data: { alert: 'hello' }, + audience_id: audienceId, + }; + return pushController.sendPush(body, where, config, auth); + }) + .then(() => { + // Wait so the push is completed. + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, 1000); + }); + }) + .then(() => { + expect(pushAdapter.send.calls.count()).toBe(1); + const firstCall = pushAdapter.send.calls.first(); + expect(firstCall.args[0].data).toEqual({ + alert: 'hello', + }); + expect(firstCall.args[1].length).toBe(5); + }) + .then(() => { + // Get the audience we used above. + const query = new Parse.Query('_Audience'); + query.equalTo('objectId', audienceId); + return query.find({ useMasterKey: true }); + }) + .then(results => { + const audience = results[0]; + expect(audience.get('query')).toBe(JSON.stringify(where)); + expect(audience.get('timesUsed')).toBe(timesUsed + 1); + expect(audience.get('lastUsed')).not.toBeLessThan(now); + }) + .then(() => { + done(); + }) + .catch(done.fail); }); describe('pushTimeHasTimezoneComponent', () => { it('should be accurate', () => { - expect(PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048Z')) - .toBe(true, 'UTC time'); - expect(PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30-02:00')) - .toBe(true, 'Timezone offset'); - expect(PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30:00.000Z-02:00')) - .toBe(true, 'Seconds + Milliseconds + Timezone offset'); - - expect(PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048')) - .toBe(false, 'No timezone'); - expect(PushController.pushTimeHasTimezoneComponent('2017-09-06')) - .toBe(false, 'YY-MM-DD'); + expect( + PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048Z') + ).toBe(true, 'UTC time'); + expect( + PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30-02:00') + ).toBe(true, 'Timezone offset'); + expect( + PushController.pushTimeHasTimezoneComponent( + '2007-04-05T12:30:00.000Z-02:00' + ) + ).toBe(true, 'Seconds + Milliseconds + Timezone offset'); + + expect( + PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048') + ).toBe(false, 'No timezone'); + expect(PushController.pushTimeHasTimezoneComponent('2017-09-06')).toBe( + false, + 'YY-MM-DD' + ); }); }); describe('formatPushTime', () => { it('should format as ISO string', () => { - expect(PushController.formatPushTime({ - date: new Date('2017-09-06T17:14:01.048Z'), - isLocalTime: false, - })).toBe('2017-09-06T17:14:01.048Z', 'UTC time'); - expect(PushController.formatPushTime({ - date: new Date('2007-04-05T12:30-02:00'), - isLocalTime: false - })).toBe('2007-04-05T14:30:00.000Z', 'Timezone offset'); - - const noTimezone = new Date('2017-09-06T17:14:01.048') + expect( + PushController.formatPushTime({ + date: new Date('2017-09-06T17:14:01.048Z'), + isLocalTime: false, + }) + ).toBe('2017-09-06T17:14:01.048Z', 'UTC time'); + expect( + PushController.formatPushTime({ + date: new Date('2007-04-05T12:30-02:00'), + isLocalTime: false, + }) + ).toBe('2007-04-05T14:30:00.000Z', 'Timezone offset'); + + const noTimezone = new Date('2017-09-06T17:14:01.048'); const expectedHour = 17 + noTimezone.getTimezoneOffset() / 60; - expect(PushController.formatPushTime({ - date: noTimezone, - isLocalTime: true, - })).toBe(`2017-09-06T${expectedHour}:14:01.048`, 'No timezone'); - expect(PushController.formatPushTime({ - date: new Date('2017-09-06'), - isLocalTime: true - })).toBe('2017-09-06T00:00:00.000', 'YY-MM-DD'); + expect( + PushController.formatPushTime({ + date: noTimezone, + isLocalTime: true, + }) + ).toBe(`2017-09-06T${expectedHour}:14:01.048`, 'No timezone'); + expect( + PushController.formatPushTime({ + date: new Date('2017-09-06'), + isLocalTime: true, + }) + ).toBe('2017-09-06T00:00:00.000', 'YY-MM-DD'); }); }); describe('Scheduling pushes in local time', () => { - it('should preserve the push time', (done) => { - const auth = {isMaster: true}; + it('should preserve the push time', done => { + const auth = { isMaster: true }; const pushAdapter = { send(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes() { - return ["ios"]; - } + return ['ios']; + }, }; const pushTime = '2017-09-06T17:14:01.048'; const expectedHour = 17 + new Date(pushTime).getTimezoneOffset() / 60; reconfigureServer({ - push: {adapter: pushAdapter}, - scheduledPush: true + push: { adapter: pushAdapter }, + scheduledPush: true, }) .then(() => { const config = Config.get(Parse.applicationId); return new Promise((resolve, reject) => { const pushController = new PushController(); - pushController.sendPush({ - data: { - alert: "Hello World!", - badge: "Increment", - }, - push_time: pushTime - }, {}, config, auth, resolve) + pushController + .sendPush( + { + data: { + alert: 'Hello World!', + badge: 'Increment', + }, + push_time: pushTime, + }, + {}, + config, + auth, + resolve + ) .catch(reject); - }) + }); }) - .then((pushStatusId) => { + .then(pushStatusId => { const q = new Parse.Query('_PushStatus'); - return q.get(pushStatusId, {useMasterKey: true}); + return q.get(pushStatusId, { useMasterKey: true }); }) - .then((pushStatus) => { + .then(pushStatus => { expect(pushStatus.get('status')).toBe('scheduled'); - expect(pushStatus.get('pushTime')).toBe(`2017-09-06T${expectedHour}:14:01.048`); + expect(pushStatus.get('pushTime')).toBe( + `2017-09-06T${expectedHour}:14:01.048` + ); }) .then(done, done.fail); }); }); describe('With expiration defined', () => { - const auth = {isMaster: true}; + const auth = { isMaster: true }; const pushController = new PushController(); let config = Config.get(Parse.applicationId); @@ -1347,13 +1589,13 @@ describe('PushController', () => { return successfulTransmissions(body, installations); }, getValidPushTypes() { - return ["ios"]; - } + return ['ios']; + }, }; - beforeEach((done) => { + beforeEach(done => { reconfigureServer({ - push: {adapter: pushAdapter}, + push: { adapter: pushAdapter }, }) .then(() => { config = Config.get(Parse.applicationId); @@ -1362,51 +1604,91 @@ describe('PushController', () => { }); it('should throw if both expiration_time and expiration_interval are set', () => { - expect(() => pushController.sendPush({ - expiration_time: '2017-09-25T13:21:20.841Z', - expiration_interval: 1000, - }, {}, config, auth)).toThrow() + expect(() => + pushController.sendPush( + { + expiration_time: '2017-09-25T13:21:20.841Z', + expiration_interval: 1000, + }, + {}, + config, + auth + ) + ).toThrow(); }); it('should throw on invalid expiration_interval', () => { - expect(() => pushController.sendPush({ - expiration_interval: -1 - }, {}, config, auth)).toThrow(); - expect(() => pushController.sendPush({ - expiration_interval: '', - }, {}, config, auth)).toThrow(); - expect(() => pushController.sendPush({ - expiration_time: {}, - }, {}, config, auth)).toThrow(); + expect(() => + pushController.sendPush( + { + expiration_interval: -1, + }, + {}, + config, + auth + ) + ).toThrow(); + expect(() => + pushController.sendPush( + { + expiration_interval: '', + }, + {}, + config, + auth + ) + ).toThrow(); + expect(() => + pushController.sendPush( + { + expiration_time: {}, + }, + {}, + config, + auth + ) + ).toThrow(); }); - describe('For immediate pushes',() => { - it('should transform the expiration_interval into an absolute time', (done) => { + describe('For immediate pushes', () => { + it('should transform the expiration_interval into an absolute time', done => { const now = new Date('2017-09-25T13:30:10.452Z'); reconfigureServer({ - push: {adapter: pushAdapter}, + push: { adapter: pushAdapter }, }) - .then(() => - new Promise((resolve) => { - pushController.sendPush({ - data: { - alert: 'immediate push', - }, - expiration_interval: 20 * 60, // twenty minutes - }, {}, Config.get(Parse.applicationId), auth, resolve, now) - })) - .then((pushStatusId) => { + .then( + () => + new Promise(resolve => { + pushController.sendPush( + { + data: { + alert: 'immediate push', + }, + expiration_interval: 20 * 60, // twenty minutes + }, + {}, + Config.get(Parse.applicationId), + auth, + resolve, + now + ); + }) + ) + .then(pushStatusId => { const p = new Parse.Object('_PushStatus'); p.id = pushStatusId; - return p.fetch({useMasterKey: true}); + return p.fetch({ useMasterKey: true }); }) - .then((pushStatus) => { + .then(pushStatus => { expect(pushStatus.get('expiry')).toBeDefined('expiry must be set'); - expect(pushStatus.get('expiry')) - .toEqual(new Date('2017-09-25T13:50:10.452Z').valueOf()); + expect(pushStatus.get('expiry')).toEqual( + new Date('2017-09-25T13:50:10.452Z').valueOf() + ); - expect(pushStatus.get('expiration_interval')).toBeDefined('expiration_interval must be defined'); + expect(pushStatus.get('expiration_interval')).toBeDefined( + 'expiration_interval must be defined' + ); expect(pushStatus.get('expiration_interval')).toBe(20 * 60); }) .then(done, done.fail); diff --git a/spec/PushQueue.spec.js b/spec/PushQueue.spec.js index 1bf282203d..8cf42e81a7 100644 --- a/spec/PushQueue.spec.js +++ b/spec/PushQueue.spec.js @@ -1,14 +1,14 @@ -const Config = require("../lib/Config"); -const {PushQueue} = require("../lib/Push/PushQueue"); +const Config = require('../lib/Config'); +const { PushQueue } = require('../lib/Push/PushQueue'); describe('PushQueue', () => { describe('With a defined channel', () => { - it('should be propagated to the PushWorker and PushQueue', (done) => { + it('should be propagated to the PushWorker and PushQueue', done => { reconfigureServer({ push: { queueOptions: { disablePushWorker: false, - channel: 'my-specific-channel' + channel: 'my-specific-channel', }, adapter: { send() { @@ -16,25 +16,31 @@ describe('PushQueue', () => { }, getValidPushTypes() { return []; - } - } - } + }, + }, + }, }) .then(() => { const config = Config.get(Parse.applicationId); - expect(config.pushWorker.channel).toEqual('my-specific-channel', 'pushWorker.channel'); - expect(config.pushControllerQueue.channel).toEqual('my-specific-channel', 'pushWorker.channel'); + expect(config.pushWorker.channel).toEqual( + 'my-specific-channel', + 'pushWorker.channel' + ); + expect(config.pushControllerQueue.channel).toEqual( + 'my-specific-channel', + 'pushWorker.channel' + ); }) .then(done, done.fail); }); }); describe('Default channel', () => { - it('should be prefixed with the applicationId', (done) => { + it('should be prefixed with the applicationId', done => { reconfigureServer({ push: { queueOptions: { - disablePushWorker: false + disablePushWorker: false, }, adapter: { send() { @@ -42,15 +48,19 @@ describe('PushQueue', () => { }, getValidPushTypes() { return []; - } - } - } + }, + }, + }, }) .then(() => { const config = Config.get(Parse.applicationId); - expect(PushQueue.defaultPushChannel()).toEqual('test-parse-server-push'); + expect(PushQueue.defaultPushChannel()).toEqual( + 'test-parse-server-push' + ); expect(config.pushWorker.channel).toEqual('test-parse-server-push'); - expect(config.pushControllerQueue.channel).toEqual('test-parse-server-push'); + expect(config.pushControllerQueue.channel).toEqual( + 'test-parse-server-push' + ); }) .then(done, done.fail); }); diff --git a/spec/PushRouter.spec.js b/spec/PushRouter.spec.js index 70781fc137..d6cf7d74c0 100644 --- a/spec/PushRouter.spec.js +++ b/spec/PushRouter.spec.js @@ -2,46 +2,45 @@ const PushRouter = require('../lib/Routers/PushRouter').PushRouter; const request = require('request'); describe('PushRouter', () => { - it('can get query condition when channels is set', (done) => { + it('can get query condition when channels is set', done => { // Make mock request const request = { body: { - channels: ['Giants', 'Mets'] - } - } + channels: ['Giants', 'Mets'], + }, + }; const where = PushRouter.getQueryCondition(request); expect(where).toEqual({ - 'channels': { - '$in': ['Giants', 'Mets'] - } + channels: { + $in: ['Giants', 'Mets'], + }, }); done(); }); - it('can get query condition when where is set', (done) => { + it('can get query condition when where is set', done => { // Make mock request const request = { body: { - 'where': { - 'injuryReports': true - } - } - } + where: { + injuryReports: true, + }, + }, + }; const where = PushRouter.getQueryCondition(request); expect(where).toEqual({ - 'injuryReports': true + injuryReports: true, }); done(); }); - it('can get query condition when nothing is set', (done) => { + it('can get query condition when nothing is set', done => { // Make mock request const request = { - body: { - } - } + body: {}, + }; expect(function() { PushRouter.getQueryCondition(request); @@ -49,18 +48,18 @@ describe('PushRouter', () => { done(); }); - it('can throw on getQueryCondition when channels and where are set', (done) => { + it('can throw on getQueryCondition when channels and where are set', done => { // Make mock request const request = { body: { - 'channels': { - '$in': ['Giants', 'Mets'] + channels: { + $in: ['Giants', 'Mets'], }, - 'where': { - 'injuryReports': true - } - } - } + where: { + injuryReports: true, + }, + }, + }; expect(function() { PushRouter.getQueryCondition(request); @@ -68,25 +67,28 @@ describe('PushRouter', () => { done(); }); - it('sends a push through REST', (done) => { - request.post({ - url: Parse.serverURL + "/push", - json: true, - body: { - 'channels': { - '$in': ['Giants', 'Mets'] - } + it('sends a push through REST', done => { + request.post( + { + url: Parse.serverURL + '/push', + json: true, + body: { + channels: { + $in: ['Giants', 'Mets'], + }, + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey, + }, }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - '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(); } - }, 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(); - }); + ); }); }); diff --git a/spec/PushWorker.spec.js b/spec/PushWorker.spec.js index 9234a28d77..8dc1068e5e 100644 --- a/spec/PushWorker.spec.js +++ b/spec/PushWorker.spec.js @@ -5,57 +5,71 @@ const { pushStatusHandler } = require('../lib/StatusHandler'); const rest = require('../lib/rest'); describe('PushWorker', () => { - it('should run with small batch', (done) => { + it('should run with small batch', done => { const batchSize = 3; let sendCount = 0; reconfigureServer({ push: { queueOptions: { disablePushWorker: true, - batchSize - } - } - }).then(() => { - expect(Config.get('test').pushWorker).toBeUndefined(); - new PushWorker({ - send: (body, installations) => { - expect(installations.length <= batchSize).toBe(true); - sendCount += installations.length; - return Promise.resolve(); + batchSize, }, - getValidPushTypes: function() { - return ['ios', 'android'] - } - }); - const installations = []; - while(installations.length != 10) { - const installation = new Parse.Object("_Installation"); - installation.set("installationId", "installation_" + installations.length); - installation.set("deviceToken","device_token_" + installations.length) - installation.set("badge", 1); - installation.set("deviceType", "ios"); - installations.push(installation); - } - return Parse.Object.saveAll(installations); - }).then(() => { - return Parse.Push.send({ - where: { - deviceType: 'ios' - }, - data: { - alert: 'Hello world!' + }, + }) + .then(() => { + expect(Config.get('test').pushWorker).toBeUndefined(); + new PushWorker({ + send: (body, installations) => { + expect(installations.length <= batchSize).toBe(true); + sendCount += installations.length; + return Promise.resolve(); + }, + getValidPushTypes: function() { + return ['ios', 'android']; + }, + }); + const installations = []; + while (installations.length != 10) { + const installation = new Parse.Object('_Installation'); + installation.set( + 'installationId', + 'installation_' + installations.length + ); + installation.set( + 'deviceToken', + 'device_token_' + installations.length + ); + installation.set('badge', 1); + installation.set('deviceType', 'ios'); + installations.push(installation); } - }, {useMasterKey: true}) - }).then(() => { - return new Promise((resolve) => { - setTimeout(resolve, 500); + return Parse.Object.saveAll(installations); + }) + .then(() => { + return Parse.Push.send( + { + where: { + deviceType: 'ios', + }, + data: { + alert: 'Hello world!', + }, + }, + { useMasterKey: true } + ); + }) + .then(() => { + return new Promise(resolve => { + setTimeout(resolve, 500); + }); + }) + .then(() => { + expect(sendCount).toBe(10); + done(); + }) + .catch(err => { + jfail(err); }); - }).then(() => { - expect(sendCount).toBe(10); - done(); - }).catch(err => { - jfail(err); - }) }); describe('localized push', () => { @@ -63,9 +77,9 @@ describe('PushWorker', () => { const locales = PushUtils.getLocalesFromPush({ data: { 'alert-fr': 'french', - 'alert': 'Yo!', + alert: 'Yo!', 'alert-en-US': 'English', - } + }, }); expect(locales).toEqual(['fr', 'en-US']); }); @@ -73,8 +87,8 @@ describe('PushWorker', () => { it('should return and empty array if no locale is set', () => { const locales = PushUtils.getLocalesFromPush({ data: { - 'alert': 'Yo!', - } + alert: 'Yo!', + }, }); expect(locales).toEqual([]); }); @@ -82,10 +96,10 @@ describe('PushWorker', () => { it('should deduplicate locales', () => { const locales = PushUtils.getLocalesFromPush({ data: { - 'alert': 'Yo!', + alert: 'Yo!', 'alert-fr': 'french', - 'title-fr': 'french' - } + 'title-fr': 'french', + }, }); expect(locales).toEqual(['fr']); }); @@ -95,273 +109,315 @@ describe('PushWorker', () => { }); it('transforms body appropriately', () => { - const cleanBody = PushUtils.transformPushBodyForLocale({ - data: { - alert: 'Yo!', - 'alert-fr': 'frenchy!', - 'alert-en': 'english', - } - }, 'fr'); + const cleanBody = PushUtils.transformPushBodyForLocale( + { + data: { + alert: 'Yo!', + 'alert-fr': 'frenchy!', + 'alert-en': 'english', + }, + }, + 'fr' + ); expect(cleanBody).toEqual({ data: { - alert: 'frenchy!' - } + alert: 'frenchy!', + }, }); }); it('transforms body appropriately', () => { - const cleanBody = PushUtils.transformPushBodyForLocale({ - data: { - alert: 'Yo!', - 'alert-fr': 'frenchy!', - 'alert-en': 'english', - 'title-fr': 'french title' - } - }, 'fr'); + const cleanBody = PushUtils.transformPushBodyForLocale( + { + data: { + alert: 'Yo!', + 'alert-fr': 'frenchy!', + 'alert-en': 'english', + 'title-fr': 'french title', + }, + }, + 'fr' + ); expect(cleanBody).toEqual({ data: { alert: 'frenchy!', - title: 'french title' - } + title: 'french title', + }, }); }); it('maps body on all provided locales', () => { - const bodies = PushUtils.bodiesPerLocales({ - data: { - alert: 'Yo!', - 'alert-fr': 'frenchy!', - 'alert-en': 'english', - 'title-fr': 'french title' - } - }, ['fr', 'en']); + const bodies = PushUtils.bodiesPerLocales( + { + data: { + alert: 'Yo!', + 'alert-fr': 'frenchy!', + 'alert-en': 'english', + 'title-fr': 'french title', + }, + }, + ['fr', 'en'] + ); expect(bodies).toEqual({ fr: { data: { alert: 'frenchy!', - title: 'french title' - } + title: 'french title', + }, }, en: { data: { alert: 'english', - } + }, }, default: { data: { - alert: 'Yo!' - } - } + alert: 'Yo!', + }, + }, }); }); it('should properly handle default cases', () => { expect(PushUtils.transformPushBodyForLocale({})).toEqual({}); expect(PushUtils.stripLocalesFromBody({})).toEqual({}); - expect(PushUtils.bodiesPerLocales({where: {}})).toEqual({default: {where: {}}}); - expect(PushUtils.groupByLocaleIdentifier([])).toEqual({default: []}); + expect(PushUtils.bodiesPerLocales({ where: {} })).toEqual({ + default: { where: {} }, + }); + expect(PushUtils.groupByLocaleIdentifier([])).toEqual({ default: [] }); }); }); describe('pushStatus', () => { - it('should remove invalid installations', (done) => { + it('should remove invalid installations', done => { const config = Config.get('test'); const handler = pushStatusHandler(config); - const spy = spyOn(config.database, "update").and.callFake(() => { + const spy = spyOn(config.database, 'update').and.callFake(() => { return Promise.resolve({}); }); - const toAwait = handler.trackSent([ - { - transmitted: false, - device: { - deviceToken: 1, - deviceType: 'ios', - }, - response: { error: 'Unregistered' } - }, - { - transmitted: true, - device: { - deviceToken: 10, - deviceType: 'ios', - }, - }, - { - transmitted: false, - device: { - deviceToken: 2, - deviceType: 'ios', - }, - response: { error: 'NotRegistered' } - }, - { - transmitted: false, - device: { - deviceToken: 3, - deviceType: 'ios', - }, - response: { error: 'InvalidRegistration' } - }, - { - transmitted: true, - device: { - deviceToken: 11, - deviceType: 'ios', - }, - }, - { - transmitted: false, - device: { - deviceToken: 4, - deviceType: 'ios', - }, - response: { error: 'InvalidRegistration' } - }, - { - transmitted: false, - device: { - deviceToken: 5, - deviceType: 'ios', - }, - response: { error: 'InvalidRegistration' } - }, - { // should not be deleted - transmitted: false, - device: { - deviceToken: 101, - deviceType: 'ios', - }, - response: { error: 'invalid error...' } - } - ], undefined, true); - expect(spy).toHaveBeenCalled(); - expect(spy.calls.count()).toBe(1); - const lastCall = spy.calls.mostRecent(); - expect(lastCall.args[0]).toBe('_Installation'); - expect(lastCall.args[1]).toEqual({ - deviceToken: { '$in': [1,2,3,4,5] } - }); - expect(lastCall.args[2]).toEqual({ - deviceToken: { '__op': "Delete" } - }); - toAwait.then(done).catch(done); - }); - - it('tracks push status per UTC offsets', (done) => { - const config = Config.get('test'); - const handler = pushStatusHandler(config); - const spy = spyOn(rest, "update").and.callThrough(); - const UTCOffset = 1; - handler.setInitial().then(() => { - return handler.trackSent([ + const toAwait = handler.trackSent( + [ { transmitted: false, device: { deviceToken: 1, deviceType: 'ios', }, + response: { error: 'Unregistered' }, }, { transmitted: true, device: { - deviceToken: 1, + deviceToken: 10, deviceType: 'ios', - } + }, }, - ], UTCOffset) - }).then(() => { - expect(spy).toHaveBeenCalled(); - const lastCall = spy.calls.mostRecent(); - expect(lastCall.args[2]).toBe(`_PushStatus`); - expect(lastCall.args[4]).toEqual({ - numSent: { __op: 'Increment', amount: 1 }, - numFailed: { __op: 'Increment', amount: 1 }, - 'sentPerType.ios': { __op: 'Increment', amount: 1 }, - 'failedPerType.ios': { __op: 'Increment', amount: 1 }, - [`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 }, - [`failedPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 }, - count: { __op: 'Increment', amount: -1 } - }); - const query = new Parse.Query('_PushStatus'); - return query.get(handler.objectId, { useMasterKey: true }); - }).then((pushStatus) => { - const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset'); - expect(sentPerUTCOffset['1']).toBe(1); - const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset'); - expect(failedPerUTCOffset['1']).toBe(1); - return handler.trackSent([ { transmitted: false, device: { - deviceToken: 1, + deviceToken: 2, deviceType: 'ios', }, + response: { error: 'NotRegistered' }, }, { - transmitted: true, + transmitted: false, device: { - deviceToken: 1, + deviceToken: 3, deviceType: 'ios', - } + }, + response: { error: 'InvalidRegistration' }, }, { transmitted: true, device: { - deviceToken: 1, + deviceToken: 11, deviceType: 'ios', - } + }, }, - ], UTCOffset) - }).then(() => { - const query = new Parse.Query('_PushStatus'); - return query.get(handler.objectId, { useMasterKey: true }); - }).then((pushStatus) => { - const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset'); - expect(sentPerUTCOffset['1']).toBe(3); - const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset'); - expect(failedPerUTCOffset['1']).toBe(2); - }).then(done).catch(done.fail); - }); - - it('tracks push status per UTC offsets with negative offsets', (done) => { - const config = Config.get('test'); - const handler = pushStatusHandler(config); - const spy = spyOn(rest, "update").and.callThrough(); - const UTCOffset = -6; - handler.setInitial().then(() => { - return handler.trackSent([ { transmitted: false, device: { - deviceToken: 1, + deviceToken: 4, deviceType: 'ios', }, - response: { error: 'Unregistered' } + response: { error: 'InvalidRegistration' }, }, { - transmitted: true, + transmitted: false, device: { - deviceToken: 1, + deviceToken: 5, deviceType: 'ios', }, - response: { error: 'Unregistered' } + response: { error: 'InvalidRegistration' }, }, - ], UTCOffset); - }).then(() => { - expect(spy).toHaveBeenCalled(); - const lastCall = spy.calls.mostRecent(); - expect(lastCall.args[2]).toBe('_PushStatus'); - expect(lastCall.args[4]).toEqual({ - numSent: { __op: 'Increment', amount: 1 }, - numFailed: { __op: 'Increment', amount: 1 }, - 'sentPerType.ios': { __op: 'Increment', amount: 1 }, - 'failedPerType.ios': { __op: 'Increment', amount: 1 }, - [`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 }, - [`failedPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 }, - count: { __op: 'Increment', amount: -1 } - }); - done(); + { + // should not be deleted + transmitted: false, + device: { + deviceToken: 101, + deviceType: 'ios', + }, + response: { error: 'invalid error...' }, + }, + ], + undefined, + true + ); + expect(spy).toHaveBeenCalled(); + expect(spy.calls.count()).toBe(1); + const lastCall = spy.calls.mostRecent(); + expect(lastCall.args[0]).toBe('_Installation'); + expect(lastCall.args[1]).toEqual({ + deviceToken: { $in: [1, 2, 3, 4, 5] }, + }); + expect(lastCall.args[2]).toEqual({ + deviceToken: { __op: 'Delete' }, }); + toAwait.then(done).catch(done); + }); + + it('tracks push status per UTC offsets', done => { + const config = Config.get('test'); + const handler = pushStatusHandler(config); + const spy = spyOn(rest, 'update').and.callThrough(); + const UTCOffset = 1; + handler + .setInitial() + .then(() => { + return handler.trackSent( + [ + { + transmitted: false, + device: { + deviceToken: 1, + deviceType: 'ios', + }, + }, + { + transmitted: true, + device: { + deviceToken: 1, + deviceType: 'ios', + }, + }, + ], + UTCOffset + ); + }) + .then(() => { + expect(spy).toHaveBeenCalled(); + const lastCall = spy.calls.mostRecent(); + expect(lastCall.args[2]).toBe(`_PushStatus`); + expect(lastCall.args[4]).toEqual({ + numSent: { __op: 'Increment', amount: 1 }, + numFailed: { __op: 'Increment', amount: 1 }, + 'sentPerType.ios': { __op: 'Increment', amount: 1 }, + 'failedPerType.ios': { __op: 'Increment', amount: 1 }, + [`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 }, + [`failedPerUTCOffset.${UTCOffset}`]: { + __op: 'Increment', + amount: 1, + }, + count: { __op: 'Increment', amount: -1 }, + }); + const query = new Parse.Query('_PushStatus'); + return query.get(handler.objectId, { useMasterKey: true }); + }) + .then(pushStatus => { + const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset'); + expect(sentPerUTCOffset['1']).toBe(1); + const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset'); + expect(failedPerUTCOffset['1']).toBe(1); + return handler.trackSent( + [ + { + transmitted: false, + device: { + deviceToken: 1, + deviceType: 'ios', + }, + }, + { + transmitted: true, + device: { + deviceToken: 1, + deviceType: 'ios', + }, + }, + { + transmitted: true, + device: { + deviceToken: 1, + deviceType: 'ios', + }, + }, + ], + UTCOffset + ); + }) + .then(() => { + const query = new Parse.Query('_PushStatus'); + return query.get(handler.objectId, { useMasterKey: true }); + }) + .then(pushStatus => { + const sentPerUTCOffset = pushStatus.get('sentPerUTCOffset'); + expect(sentPerUTCOffset['1']).toBe(3); + const failedPerUTCOffset = pushStatus.get('failedPerUTCOffset'); + expect(failedPerUTCOffset['1']).toBe(2); + }) + .then(done) + .catch(done.fail); + }); + + it('tracks push status per UTC offsets with negative offsets', done => { + const config = Config.get('test'); + const handler = pushStatusHandler(config); + const spy = spyOn(rest, 'update').and.callThrough(); + const UTCOffset = -6; + handler + .setInitial() + .then(() => { + return handler.trackSent( + [ + { + transmitted: false, + device: { + deviceToken: 1, + deviceType: 'ios', + }, + response: { error: 'Unregistered' }, + }, + { + transmitted: true, + device: { + deviceToken: 1, + deviceType: 'ios', + }, + response: { error: 'Unregistered' }, + }, + ], + UTCOffset + ); + }) + .then(() => { + expect(spy).toHaveBeenCalled(); + const lastCall = spy.calls.mostRecent(); + expect(lastCall.args[2]).toBe('_PushStatus'); + expect(lastCall.args[4]).toEqual({ + numSent: { __op: 'Increment', amount: 1 }, + numFailed: { __op: 'Increment', amount: 1 }, + 'sentPerType.ios': { __op: 'Increment', amount: 1 }, + 'failedPerType.ios': { __op: 'Increment', amount: 1 }, + [`sentPerUTCOffset.${UTCOffset}`]: { __op: 'Increment', amount: 1 }, + [`failedPerUTCOffset.${UTCOffset}`]: { + __op: 'Increment', + amount: 1, + }, + count: { __op: 'Increment', amount: -1 }, + }); + done(); + }); }); }); }); diff --git a/spec/QueryTools.spec.js b/spec/QueryTools.spec.js index 9c5acf1f73..d47b2c94b4 100644 --- a/spec/QueryTools.spec.js +++ b/spec/QueryTools.spec.js @@ -8,7 +8,6 @@ const matchesQuery = QueryTools.matchesQuery; const Item = Parse.Object.extend('Item'); describe('queryHash', function() { - it('should always hash a query to the same string', function() { const q = new Parse.Query(Item); q.equalTo('field', 'value'); @@ -91,7 +90,7 @@ describe('matchesQuery', function() { it('matches blanket queries', function() { const obj = { id: new Id('Klass', 'O1'), - value: 12 + value: 12, }; const q = new Parse.Query('Klass'); expect(matchesQuery(obj, q)).toBe(true); @@ -103,7 +102,7 @@ describe('matchesQuery', function() { it('matches existence queries', function() { const obj = { id: new Id('Item', 'O1'), - count: 15 + count: 15, }; const q = new Parse.Query('Item'); q.exists('count'); @@ -115,7 +114,7 @@ describe('matchesQuery', function() { it('matches queries with doesNotExist constraint', function() { const obj = { id: new Id('Item', 'O1'), - count: 15 + count: 15, }; let q = new Parse.Query('Item'); q.doesNotExist('name'); @@ -130,14 +129,14 @@ describe('matchesQuery', function() { const day = new Date(); const location = new Parse.GeoPoint({ latitude: 37.484815, - longitude: -122.148377 + longitude: -122.148377, }); const obj = { id: new Id('Person', 'O1'), score: 12, name: 'Bill', birthday: day, - lastLocation: location + lastLocation: location, }; let q = new Parse.Query('Person'); @@ -169,21 +168,30 @@ describe('matchesQuery', function() { expect(matchesQuery(obj, q)).toBe(false); q = new Parse.Query('Person'); - q.equalTo('lastLocation', new Parse.GeoPoint({ - latitude: 37.484815, - longitude: -122.148377 - })); + q.equalTo( + 'lastLocation', + new Parse.GeoPoint({ + latitude: 37.484815, + longitude: -122.148377, + }) + ); expect(matchesQuery(obj, q)).toBe(true); - q.equalTo('lastLocation', new Parse.GeoPoint({ - latitude: 37.4848, - longitude: -122.1483 - })); + q.equalTo( + 'lastLocation', + new Parse.GeoPoint({ + latitude: 37.4848, + longitude: -122.1483, + }) + ); expect(matchesQuery(obj, q)).toBe(false); - q.equalTo('lastLocation', new Parse.GeoPoint({ - latitude: 37.484815, - longitude: -122.148377 - })); + q.equalTo( + 'lastLocation', + new Parse.GeoPoint({ + latitude: 37.484815, + longitude: -122.148377, + }) + ); q.equalTo('score', 12); q.equalTo('name', 'Bill'); q.equalTo('birthday', day); @@ -194,7 +202,7 @@ describe('matchesQuery', function() { let img = { id: new Id('Image', 'I1'), - tags: ['nofilter', 'latergram', 'tbt'] + tags: ['nofilter', 'latergram', 'tbt'], }; q = new Parse.Query('Image'); @@ -219,8 +227,8 @@ describe('matchesQuery', function() { objectId: 'I1', owner: { className: '_User', - objectId: 'U2' - } + objectId: 'U2', + }, }; expect(matchesQuery(img, q)).toBe(true); @@ -234,10 +242,12 @@ describe('matchesQuery', function() { img = { className: 'Image', objectId: 'I1', - owners: [{ - className: '_User', - objectId: 'U2' - }] + owners: [ + { + className: '_User', + objectId: 'U2', + }, + ], }; expect(matchesQuery(img, q)).toBe(true); @@ -291,7 +301,7 @@ describe('matchesQuery', function() { const player = { id: new Id('Player', 'P1'), name: 'Player 1', - score: 12 + score: 12, }; const q = new Parse.Query('Player'); q.equalTo('name', 'Player 1'); @@ -307,7 +317,7 @@ describe('matchesQuery', function() { const player = { id: new Id('Player', 'P1'), name: 'Player 1', - score: 12 + score: 12, }; let q = new Parse.Query('Player'); @@ -358,14 +368,14 @@ describe('matchesQuery', function() { // With no max distance, any GeoPoint is 'near' const pt = { id: new Id('Checkin', 'C1'), - location: new Parse.GeoPoint(40, 40) + location: new Parse.GeoPoint(40, 40), }; const ptUndefined = { - id: new Id('Checkin', 'C1') + id: new Id('Checkin', 'C1'), }; const ptNull = { id: new Id('Checkin', 'C1'), - location: null + location: null, }; expect(matchesQuery(pt, q)).toBe(true); expect(matchesQuery(ptUndefined, q)).toBe(false); @@ -384,24 +394,24 @@ describe('matchesQuery', function() { const caltrainStation = { id: new Id('Checkin', 'C1'), location: new Parse.GeoPoint(37.776346, -122.394218), - name: 'Caltrain' + name: 'Caltrain', }; const santaClara = { id: new Id('Checkin', 'C2'), location: new Parse.GeoPoint(37.325635, -121.945753), - name: 'Santa Clara' + name: 'Santa Clara', }; const noLocation = { id: new Id('Checkin', 'C2'), - name: 'Santa Clara' + name: 'Santa Clara', }; const nullLocation = { id: new Id('Checkin', 'C2'), location: null, - name: 'Santa Clara' + name: 'Santa Clara', }; let q = new Parse.Query('Checkin').withinGeoBox( @@ -437,63 +447,62 @@ describe('matchesQuery', function() { it('matches on subobjects with dot notation', function() { const message = { id: new Id('Message', 'O1'), - text: "content", - status: {x: "read", y: "delivered"} + text: 'content', + status: { x: 'read', y: 'delivered' }, }; let q = new Parse.Query('Message'); - q.equalTo("status.x", "read"); + q.equalTo('status.x', 'read'); expect(matchesQuery(message, q)).toBe(true); q = new Parse.Query('Message'); - q.equalTo("status.z", "read"); + q.equalTo('status.z', 'read'); expect(matchesQuery(message, q)).toBe(false); q = new Parse.Query('Message'); - q.equalTo("status.x", "delivered"); + q.equalTo('status.x', 'delivered'); expect(matchesQuery(message, q)).toBe(false); q = new Parse.Query('Message'); - q.notEqualTo("status.x", "read"); + q.notEqualTo('status.x', 'read'); expect(matchesQuery(message, q)).toBe(false); q = new Parse.Query('Message'); - q.notEqualTo("status.z", "read"); + q.notEqualTo('status.z', 'read'); expect(matchesQuery(message, q)).toBe(true); q = new Parse.Query('Message'); - q.notEqualTo("status.x", "delivered"); + q.notEqualTo('status.x', 'delivered'); expect(matchesQuery(message, q)).toBe(true); q = new Parse.Query('Message'); - q.exists("status.x"); + q.exists('status.x'); expect(matchesQuery(message, q)).toBe(true); q = new Parse.Query('Message'); - q.exists("status.z"); + q.exists('status.z'); expect(matchesQuery(message, q)).toBe(false); q = new Parse.Query('Message'); - q.exists("nonexistent.x"); + q.exists('nonexistent.x'); expect(matchesQuery(message, q)).toBe(false); q = new Parse.Query('Message'); - q.doesNotExist("status.x"); + q.doesNotExist('status.x'); expect(matchesQuery(message, q)).toBe(false); q = new Parse.Query('Message'); - q.doesNotExist("status.z"); + q.doesNotExist('status.z'); expect(matchesQuery(message, q)).toBe(true); q = new Parse.Query('Message'); - q.doesNotExist("nonexistent.z"); + q.doesNotExist('nonexistent.z'); expect(matchesQuery(message, q)).toBe(true); q = new Parse.Query('Message'); - q.equalTo("status.x", "read"); - q.doesNotExist("status.y"); + q.equalTo('status.x', 'read'); + q.doesNotExist('status.y'); expect(matchesQuery(message, q)).toBe(false); - }); function pointer(className, objectId) { @@ -503,43 +512,51 @@ describe('matchesQuery', function() { it('should support containedIn with pointers', () => { const message = { id: new Id('Message', 'O1'), - profile: pointer('Profile', 'abc') + profile: pointer('Profile', 'abc'), }; let q = new Parse.Query('Message'); - q.containedIn('profile', [Parse.Object.fromJSON({ className: 'Profile', objectId: 'abc' }), - Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' })]); + q.containedIn('profile', [ + Parse.Object.fromJSON({ className: 'Profile', objectId: 'abc' }), + Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' }), + ]); expect(matchesQuery(message, q)).toBe(true); q = new Parse.Query('Message'); - q.containedIn('profile', [Parse.Object.fromJSON({ className: 'Profile', objectId: 'ghi' }), - Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' })]); + q.containedIn('profile', [ + Parse.Object.fromJSON({ className: 'Profile', objectId: 'ghi' }), + Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' }), + ]); expect(matchesQuery(message, q)).toBe(false); }); it('should support notContainedIn with pointers', () => { let message = { id: new Id('Message', 'O1'), - profile: pointer('Profile', 'abc') + profile: pointer('Profile', 'abc'), }; let q = new Parse.Query('Message'); - q.notContainedIn('profile', [Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' }), - Parse.Object.fromJSON({ className: 'Profile', objectId: 'ghi' })]); + q.notContainedIn('profile', [ + Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' }), + Parse.Object.fromJSON({ className: 'Profile', objectId: 'ghi' }), + ]); expect(matchesQuery(message, q)).toBe(true); message = { id: new Id('Message', 'O1'), - profile: pointer('Profile', 'def') + profile: pointer('Profile', 'def'), }; q = new Parse.Query('Message'); - q.notContainedIn('profile', [Parse.Object.fromJSON({ className: 'Profile', objectId: 'ghi' }), - Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' })]); + q.notContainedIn('profile', [ + Parse.Object.fromJSON({ className: 'Profile', objectId: 'ghi' }), + Parse.Object.fromJSON({ className: 'Profile', objectId: 'def' }), + ]); expect(matchesQuery(message, q)).toBe(false); }); it('should support containedIn queries with [objectId]', () => { let message = { id: new Id('Message', 'O1'), - profile: pointer('Profile', 'abc') + profile: pointer('Profile', 'abc'), }; let q = new Parse.Query('Message'); q.containedIn('profile', ['abc', 'def']); @@ -547,7 +564,7 @@ describe('matchesQuery', function() { message = { id: new Id('Message', 'O1'), - profile: pointer('Profile', 'ghi') + profile: pointer('Profile', 'ghi'), }; q = new Parse.Query('Message'); q.containedIn('profile', ['abc', 'def']); @@ -557,14 +574,14 @@ describe('matchesQuery', function() { it('should support notContainedIn queries with [objectId]', () => { let message = { id: new Id('Message', 'O1'), - profile: pointer('Profile', 'ghi') + profile: pointer('Profile', 'ghi'), }; let q = new Parse.Query('Message'); q.notContainedIn('profile', ['abc', 'def']); expect(matchesQuery(message, q)).toBe(true); message = { id: new Id('Message', 'O1'), - profile: pointer('Profile', 'ghi') + profile: pointer('Profile', 'ghi'), }; q = new Parse.Query('Message'); q.notContainedIn('profile', ['abc', 'def', 'ghi']); diff --git a/spec/ReadPreferenceOption.spec.js b/spec/ReadPreferenceOption.spec.js index bfbc1525fd..d386bc201a 100644 --- a/spec/ReadPreferenceOption.spec.js +++ b/spec/ReadPreferenceOption.spec.js @@ -1,13 +1,13 @@ -'use strict' +'use strict'; const Parse = require('parse/node'); const ReadPreference = require('mongodb').ReadPreference; const rp = require('request-promise'); -const Config = require("../lib/Config"); +const Config = require('../lib/Config'); describe_only_db('mongo')('Read preference option', () => { - it('should find in primary by default', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should find in primary by default', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -20,17 +20,21 @@ describe_only_db('mongo')('Read preference option', () => { const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = true; - expect(call.args[2].readPreference.preference).toBe(ReadPreference.PRIMARY); - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.PRIMARY + ); + } + }); expect(myObjectReadPreference).toBe(true); @@ -40,23 +44,27 @@ describe_only_db('mongo')('Read preference option', () => { }); it('should preserve the read preference set (#4831)', async () => { - const { MongoStorageAdapter } = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter'); + const { + MongoStorageAdapter, + } = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter'); const adapterOptions = { uri: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', mongoOptions: { readPreference: ReadPreference.NEAREST, - } + }, }; - await reconfigureServer({ databaseAdapter: new MongoStorageAdapter(adapterOptions) }); + await reconfigureServer({ + databaseAdapter: new MongoStorageAdapter(adapterOptions), + }); - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); const obj1 = new Parse.Object('MyObject'); obj1.set('boolKey', true); - await Parse.Object.saveAll([obj0, obj1]) + await Parse.Object.saveAll([obj0, obj1]); spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); const query = new Parse.Query('MyObject'); @@ -67,18 +75,20 @@ describe_only_db('mongo')('Read preference option', () => { expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { + databaseAdapter.database.serverConfig.cursor.calls.all().forEach(call => { if (call.args[0].indexOf('MyObject') >= 0) { myObjectReadPreference = true; - expect(call.args[2].readPreference.preference).toBe(ReadPreference.NEAREST); + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.NEAREST + ); } }); expect(myObjectReadPreference).toBe(true); }); - it('should change read preference in the beforeFind trigger', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference in the beforeFind trigger', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -88,23 +98,25 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; }); const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); @@ -113,8 +125,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should change read preference in the beforeFind trigger even changing query', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference in the beforeFind trigger even changing query', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -124,7 +136,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.query.equalTo('boolKey', true); req.readPreference = 'SECONDARY'; }); @@ -132,16 +144,18 @@ describe_only_db('mongo')('Read preference option', () => { const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(true); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); @@ -150,8 +164,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should change read preference in the beforeFind trigger even returning query', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference in the beforeFind trigger even returning query', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -161,7 +175,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; const otherQuery = new Parse.Query('MyObject'); @@ -172,16 +186,18 @@ describe_only_db('mongo')('Read preference option', () => { const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(true); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); @@ -190,8 +206,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should change read preference in the beforeFind trigger even returning promise', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference in the beforeFind trigger even returning promise', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -201,7 +217,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; const otherQuery = new Parse.Query('MyObject'); @@ -212,16 +228,18 @@ describe_only_db('mongo')('Read preference option', () => { const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(true); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); @@ -230,8 +248,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should change read preference to PRIMARY_PREFERRED', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference to PRIMARY_PREFERRED', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -241,33 +259,37 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'PRIMARY_PREFERRED'; }); const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[2].readPreference.preference; - } - }); - - expect(myObjectReadPreference).toEqual(ReadPreference.PRIMARY_PREFERRED); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual( + ReadPreference.PRIMARY_PREFERRED + ); done(); }); }); }); - it('should change read preference to SECONDARY_PREFERRED', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference to SECONDARY_PREFERRED', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -277,33 +299,37 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY_PREFERRED'; }); const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[2].readPreference.preference; - } - }); - - expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY_PREFERRED); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); + + expect(myObjectReadPreference).toEqual( + ReadPreference.SECONDARY_PREFERRED + ); done(); }); }); }); - it('should change read preference to NEAREST', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference to NEAREST', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -313,23 +339,25 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'NEAREST'; }); const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference).toEqual(ReadPreference.NEAREST); @@ -338,8 +366,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should change read preference for GET', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference for GET', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -349,21 +377,23 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; }); const query = new Parse.Query('MyObject'); - query.get(obj0.id).then((result) => { + query.get(obj0.id).then(result => { expect(result.get('boolKey')).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); @@ -372,8 +402,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should change read preference for GET using API', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference for GET using API', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -383,7 +413,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; }); @@ -392,18 +422,20 @@ describe_only_db('mongo')('Read preference option', () => { uri: 'http://localhost:8378/1/classes/MyObject/' + obj0.id, headers: { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }, json: true, }).then(body => { expect(body.boolKey).toBe(false); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject') >= 0) { - myObjectReadPreference = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject') >= 0) { + myObjectReadPreference = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); @@ -412,8 +444,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should change read preference for count', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change read preference for count', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject'); obj0.set('boolKey', false); @@ -423,20 +455,22 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'command').and.callThrough(); - Parse.Cloud.beforeFind('MyObject', (req) => { + Parse.Cloud.beforeFind('MyObject', req => { req.readPreference = 'SECONDARY'; }); const query = new Parse.Query('MyObject'); query.equalTo('boolKey', false); - query.count().then((result) => { + query.count().then(result => { expect(result).toBe(1); let myObjectReadPreference = null; - databaseAdapter.database.serverConfig.command.calls.all().forEach((call) => { - myObjectReadPreference = call.args[2].readPreference.preference; - }); + databaseAdapter.database.serverConfig.command.calls + .all() + .forEach(call => { + myObjectReadPreference = call.args[2].readPreference.preference; + }); expect(myObjectReadPreference).toEqual(ReadPreference.SECONDARY); @@ -445,8 +479,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should find includes in primary by default', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should find includes in primary by default', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); @@ -460,7 +494,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject2', (req) => { + Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY'; }); @@ -469,29 +503,40 @@ describe_only_db('mongo')('Read preference option', () => { query.include('myObject1'); query.include('myObject1.myObject0'); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); const firstResult = results[0]; expect(firstResult.get('boolKey')).toBe(false); expect(firstResult.get('myObject1').get('boolKey')).toBe(true); - expect(firstResult.get('myObject1').get('myObject0').get('boolKey')).toBe(false); + expect( + firstResult + .get('myObject1') + .get('myObject0') + .get('boolKey') + ).toBe(false); let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = true; - expect(call.args[2].readPreference.preference).toBe(ReadPreference.PRIMARY); - } - if (call.args[0].indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = true; - expect(call.args[2].readPreference.preference).toBe(ReadPreference.PRIMARY); - } - if (call.args[0].indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.PRIMARY + ); + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.PRIMARY + ); + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference0).toBe(true); expect(myObjectReadPreference1).toBe(true); @@ -502,8 +547,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should change includes read preference', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change includes read preference', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); @@ -517,7 +562,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject2', (req) => { + Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY_PREFERRED'; req.includeReadPreference = 'SECONDARY'; }); @@ -527,40 +572,48 @@ describe_only_db('mongo')('Read preference option', () => { query.include('myObject1'); query.include('myObject1.myObject0'); - query.find().then((results) => { + query.find().then(results => { expect(results.length).toBe(1); const firstResult = results[0]; expect(firstResult.get('boolKey')).toBe(false); expect(firstResult.get('myObject1').get('boolKey')).toBe(true); - expect(firstResult.get('myObject1').get('myObject0').get('boolKey')).toBe(false); - + expect( + firstResult + .get('myObject1') + .get('myObject0') + .get('boolKey') + ).toBe(false); let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[2].readPreference.preference; - } - if (call.args[0].indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[2].readPreference.preference; - } - if (call.args[0].indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference0).toEqual(ReadPreference.SECONDARY); expect(myObjectReadPreference1).toEqual(ReadPreference.SECONDARY); - expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY_PREFERRED); + expect(myObjectReadPreference2).toEqual( + ReadPreference.SECONDARY_PREFERRED + ); done(); }); }); }); - it('should find subqueries in primary by default', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should find subqueries in primary by default', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); @@ -574,7 +627,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject2', (req) => { + Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY'; }); @@ -587,26 +640,32 @@ describe_only_db('mongo')('Read preference option', () => { const query2 = new Parse.Query('MyObject2'); query2.matchesQuery('myObject1', query1); - query2.find().then((results) => { + query2.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = true; - expect(call.args[2].readPreference.preference).toBe(ReadPreference.PRIMARY); - } - if (call.args[0].indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = true; - expect(call.args[2].readPreference.preference).toBe(ReadPreference.PRIMARY); - } - if (call.args[0].indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.PRIMARY + ); + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = true; + expect(call.args[2].readPreference.preference).toBe( + ReadPreference.PRIMARY + ); + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference0).toBe(true); expect(myObjectReadPreference1).toBe(true); @@ -617,8 +676,8 @@ describe_only_db('mongo')('Read preference option', () => { }); }); - it('should change subqueries read preference when using matchesQuery', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change subqueries read preference when using matchesQuery', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); @@ -632,7 +691,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject2', (req) => { + Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY_PREFERRED'; req.subqueryReadPreference = 'SECONDARY'; }); @@ -646,36 +705,40 @@ describe_only_db('mongo')('Read preference option', () => { const query2 = new Parse.Query('MyObject2'); query2.matchesQuery('myObject1', query1); - query2.find().then((results) => { + query2.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[2].readPreference.preference; - } - if (call.args[0].indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[2].readPreference.preference; - } - if (call.args[0].indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference0).toEqual(ReadPreference.SECONDARY); expect(myObjectReadPreference1).toEqual(ReadPreference.SECONDARY); - expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY_PREFERRED); + expect(myObjectReadPreference2).toEqual( + ReadPreference.SECONDARY_PREFERRED + ); done(); }); }); }); - it('should change subqueries read preference when using doesNotMatchQuery', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change subqueries read preference when using doesNotMatchQuery', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); @@ -689,7 +752,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject2', (req) => { + Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY_PREFERRED'; req.subqueryReadPreference = 'SECONDARY'; }); @@ -703,36 +766,40 @@ describe_only_db('mongo')('Read preference option', () => { const query2 = new Parse.Query('MyObject2'); query2.doesNotMatchQuery('myObject1', query1); - query2.find().then((results) => { + query2.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[2].readPreference.preference; - } - if (call.args[0].indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[2].readPreference.preference; - } - if (call.args[0].indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference0).toEqual(ReadPreference.SECONDARY); expect(myObjectReadPreference1).toEqual(ReadPreference.SECONDARY); - expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY_PREFERRED); + expect(myObjectReadPreference2).toEqual( + ReadPreference.SECONDARY_PREFERRED + ); done(); }); }); }); - it('should change subqueries read preference when using matchesKeyInQuery and doesNotMatchKeyInQuery', (done) => { - const databaseAdapter = (Config.get(Parse.applicationId)).database.adapter; + it('should change subqueries read preference when using matchesKeyInQuery and doesNotMatchKeyInQuery', done => { + const databaseAdapter = Config.get(Parse.applicationId).database.adapter; const obj0 = new Parse.Object('MyObject0'); obj0.set('boolKey', false); @@ -746,7 +813,7 @@ describe_only_db('mongo')('Read preference option', () => { Parse.Object.saveAll([obj0, obj1, obj2]).then(() => { spyOn(databaseAdapter.database.serverConfig, 'cursor').and.callThrough(); - Parse.Cloud.beforeFind('MyObject2', (req) => { + Parse.Cloud.beforeFind('MyObject2', req => { req.readPreference = 'SECONDARY_PREFERRED'; req.subqueryReadPreference = 'SECONDARY'; }); @@ -761,28 +828,32 @@ describe_only_db('mongo')('Read preference option', () => { query2.matchesKeyInQuery('boolKey', 'boolKey', query0); query2.doesNotMatchKeyInQuery('boolKey', 'boolKey', query1); - query2.find().then((results) => { + query2.find().then(results => { expect(results.length).toBe(1); expect(results[0].get('boolKey')).toBe(false); let myObjectReadPreference0 = null; let myObjectReadPreference1 = null; let myObjectReadPreference2 = null; - databaseAdapter.database.serverConfig.cursor.calls.all().forEach((call) => { - if (call.args[0].indexOf('MyObject0') >= 0) { - myObjectReadPreference0 = call.args[2].readPreference.preference; - } - if (call.args[0].indexOf('MyObject1') >= 0) { - myObjectReadPreference1 = call.args[2].readPreference.preference; - } - if (call.args[0].indexOf('MyObject2') >= 0) { - myObjectReadPreference2 = call.args[2].readPreference.preference; - } - }); + databaseAdapter.database.serverConfig.cursor.calls + .all() + .forEach(call => { + if (call.args[0].indexOf('MyObject0') >= 0) { + myObjectReadPreference0 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject1') >= 0) { + myObjectReadPreference1 = call.args[2].readPreference.preference; + } + if (call.args[0].indexOf('MyObject2') >= 0) { + myObjectReadPreference2 = call.args[2].readPreference.preference; + } + }); expect(myObjectReadPreference0).toEqual(ReadPreference.SECONDARY); expect(myObjectReadPreference1).toEqual(ReadPreference.SECONDARY); - expect(myObjectReadPreference2).toEqual(ReadPreference.SECONDARY_PREFERRED); + expect(myObjectReadPreference2).toEqual( + ReadPreference.SECONDARY_PREFERRED + ); done(); }); diff --git a/spec/RedisCacheAdapter.spec.js b/spec/RedisCacheAdapter.spec.js index 956e342701..aa759cce4a 100644 --- a/spec/RedisCacheAdapter.spec.js +++ b/spec/RedisCacheAdapter.spec.js @@ -1,4 +1,5 @@ -const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default; +const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter') + .default; /* To run this test part of the complete suite set PARSE_SERVER_TEST_CACHE='redis' @@ -13,44 +14,47 @@ describe_only(() => { function wait(sleep) { return new Promise(function(resolve) { setTimeout(resolve, sleep); - }) + }); } - it('should get/set/clear', (done) => { + it('should get/set/clear', done => { const cache = new RedisCacheAdapter({ - ttl: NaN + ttl: NaN, }); - cache.put(KEY, VALUE) + cache + .put(KEY, VALUE) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(VALUE)) + .then(value => expect(value).toEqual(VALUE)) .then(() => cache.clear()) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(null)) + .then(value => expect(value).toEqual(null)) .then(done); }); - it('should expire after ttl', (done) => { + it('should expire after ttl', done => { const cache = new RedisCacheAdapter(null, 1); - cache.put(KEY, VALUE) + cache + .put(KEY, VALUE) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(VALUE)) + .then(value => expect(value).toEqual(VALUE)) .then(wait.bind(null, 2)) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(null)) + .then(value => expect(value).toEqual(null)) .then(done); }); - it('should find un-expired records', (done) => { + it('should find un-expired records', done => { const cache = new RedisCacheAdapter(null, 5); - cache.put(KEY, VALUE) + cache + .put(KEY, VALUE) .then(() => cache.get(KEY)) - .then((value) => expect(value).toEqual(VALUE)) + .then(value => expect(value).toEqual(VALUE)) .then(wait.bind(null, 1)) .then(() => cache.get(KEY)) - .then((value) => expect(value).not.toEqual(null)) + .then(value => expect(value).not.toEqual(null)) .then(done); }); }); diff --git a/spec/RedisPubSub.spec.js b/spec/RedisPubSub.spec.js index 8530a95c0c..1263b14a7d 100644 --- a/spec/RedisPubSub.spec.js +++ b/spec/RedisPubSub.spec.js @@ -1,7 +1,6 @@ const RedisPubSub = require('../lib/Adapters/PubSub/RedisPubSub').RedisPubSub; describe('RedisPubSub', function() { - beforeEach(function(done) { // Mock redis const createClient = jasmine.createSpy('createClient'); @@ -10,17 +9,21 @@ describe('RedisPubSub', function() { }); it('can create publisher', function() { - RedisPubSub.createPublisher({redisURL: 'redisAddress'}); + RedisPubSub.createPublisher({ redisURL: 'redisAddress' }); const redis = require('redis'); - expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true }); + expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { + no_ready_check: true, + }); }); it('can create subscriber', function() { - RedisPubSub.createSubscriber({redisURL: 'redisAddress'}); + RedisPubSub.createSubscriber({ redisURL: 'redisAddress' }); const redis = require('redis'); - expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { no_ready_check: true }); + expect(redis.createClient).toHaveBeenCalledWith('redisAddress', { + no_ready_check: true, + }); }); afterEach(function() { diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 1b39af2ba8..dd3c261227 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -1,4 +1,4 @@ -'use strict' +'use strict'; // These tests check the "find" functionality of the REST API. const auth = require('../lib/Auth'); const Config = require('../lib/Config'); @@ -12,257 +12,332 @@ let database; const nobody = auth.nobody(config); describe('rest query', () => { - beforeEach(() => { config = Config.get('test'); database = config.database; }); - it('basic query', (done) => { - rest.create(config, nobody, 'TestObject', {}).then(() => { - return rest.find(config, nobody, 'TestObject', {}); - }).then((response) => { - expect(response.results.length).toEqual(1); - done(); - }); + it('basic query', done => { + rest + .create(config, nobody, 'TestObject', {}) + .then(() => { + return rest.find(config, nobody, 'TestObject', {}); + }) + .then(response => { + expect(response.results.length).toEqual(1); + done(); + }); }); - it('query with limit', (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: 1}); - }).then((response) => { - expect(response.results.length).toEqual(1); - expect(response.results[0].foo).toBeTruthy(); - done(); - }); + it('query with limit', 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: 1 }); + }) + .then(response => { + expect(response.results.length).toEqual(1); + expect(response.results[0].foo).toBeTruthy(); + done(); + }); }); const 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) => { - const 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 without masterKey has them stripped from results', + done => { + database + .create('_User', data) + .then(() => { + return rest.find(config, nobody, '_User'); + }) + .then(result => { + const 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) => { - const 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 => { + const 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_exclude_dbs(['postgres'])('query with include', (done) => { + it_exclude_dbs(['postgres'])('query with include', done => { let photo = { - foo: 'bar' + foo: 'bar', }; let user = { username: 'aUsername', - password: 'aPassword' + password: 'aPassword', }; const activity = { type: 'comment', photo: { __type: 'Pointer', className: 'TestPhoto', - objectId: '' + objectId: '', }, fromUser: { __type: 'Pointer', className: '_User', - objectId: '' - } + objectId: '', + }, }; const queryWhere = { photo: { __type: 'Pointer', className: 'TestPhoto', - objectId: '' + objectId: '', }, - type: 'comment' + type: 'comment', }; const queryOptions = { include: 'fromUser', order: 'createdAt', - limit: 30 + limit: 30, }; - rest.create(config, nobody, 'TestPhoto', photo - ).then((p) => { - photo = p; - return rest.create(config, nobody, '_User', user); - }).then((u) => { - user = u.response; - activity.photo.objectId = photo.objectId; - activity.fromUser.objectId = user.objectId; - return rest.create(config, nobody, - 'TestActivity', activity); - }).then(() => { - queryWhere.photo.objectId = photo.objectId; - return rest.find(config, nobody, - 'TestActivity', queryWhere, queryOptions); - }).then((response) => { - const results = response.results; - expect(results.length).toEqual(1); - expect(typeof results[0].objectId).toEqual('string'); - expect(typeof results[0].photo).toEqual('object'); - expect(typeof results[0].fromUser).toEqual('object'); - expect(typeof results[0].fromUser.username).toEqual('string'); - done(); - }).catch((error) => { console.log(error); }); - }); - - it('query non-existent class when disabled client class creation', (done) => { - const customConfig = Object.assign({}, config, {allowClientClassCreation: false}); - rest.find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}) + rest + .create(config, nobody, 'TestPhoto', photo) + .then(p => { + photo = p; + return rest.create(config, nobody, '_User', user); + }) + .then(u => { + user = u.response; + activity.photo.objectId = photo.objectId; + activity.fromUser.objectId = user.objectId; + return rest.create(config, nobody, 'TestActivity', activity); + }) .then(() => { - fail('Should throw an error'); - done(); - }, (err) => { - expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(err.message).toEqual('This user is not allowed to access ' + - 'non-existent class: ClientClassCreation'); + queryWhere.photo.objectId = photo.objectId; + return rest.find( + config, + nobody, + 'TestActivity', + queryWhere, + queryOptions + ); + }) + .then(response => { + const results = response.results; + expect(results.length).toEqual(1); + expect(typeof results[0].objectId).toEqual('string'); + expect(typeof results[0].photo).toEqual('object'); + expect(typeof results[0].fromUser).toEqual('object'); + expect(typeof results[0].fromUser.username).toEqual('string'); done(); + }) + .catch(error => { + console.log(error); }); }); - it('query existent class when disabled client class creation', (done) => { - const customConfig = Object.assign({}, config, {allowClientClassCreation: false}); - config.database.loadSchema() + it('query non-existent class when disabled client class creation', done => { + const customConfig = Object.assign({}, config, { + allowClientClassCreation: false, + }); + rest + .find(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}) + .then( + () => { + fail('Should throw an error'); + done(); + }, + err => { + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(err.message).toEqual( + 'This user is not allowed to access ' + + 'non-existent class: ClientClassCreation' + ); + done(); + } + ); + }); + + it('query existent class when disabled client class creation', done => { + const 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', {}); + return rest.find( + customConfig, + auth.nobody(customConfig), + 'ClientClassCreation', + {} + ); }) - .then((result) => { - expect(result.results.length).toEqual(0); - done(); - }, () => { - fail('Should not throw error') - }); + .then( + result => { + expect(result.results.length).toEqual(0); + done(); + }, + () => { + fail('Should not throw error'); + } + ); }); - it('query with wrongly encoded parameter', (done) => { - rest.create(config, nobody, 'TestParameterEncode', {foo: 'bar'} - ).then(() => { - return rest.create(config, nobody, - 'TestParameterEncode', {foo: 'baz'}); - }).then(() => { - const headers = { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' - }; - - const p0 = rp.get({ - headers: headers, - url: 'http://localhost:8378/1/classes/TestParameterEncode?' + querystring.stringify({ - where: '{"foo":{"$ne": "baz"}}', - limit: 1 - }).replace('=', '%3D'), - }) - .then(fail, (response) => { - const error = response.error; - const b = JSON.parse(error); - expect(b.code).toEqual(Parse.Error.INVALID_QUERY); + it('query with wrongly encoded parameter', done => { + rest + .create(config, nobody, 'TestParameterEncode', { foo: 'bar' }) + .then(() => { + return rest.create(config, nobody, 'TestParameterEncode', { + foo: 'baz', }); + }) + .then(() => { + const headers = { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }; + + const p0 = rp + .get({ + headers: headers, + url: + 'http://localhost:8378/1/classes/TestParameterEncode?' + + querystring + .stringify({ + where: '{"foo":{"$ne": "baz"}}', + limit: 1, + }) + .replace('=', '%3D'), + }) + .then(fail, response => { + const error = response.error; + const b = JSON.parse(error); + expect(b.code).toEqual(Parse.Error.INVALID_QUERY); + }); - const p1 = rp.get({ - headers: headers, - url: 'http://localhost:8378/1/classes/TestParameterEncode?' + querystring.stringify({ - limit: 1 - }).replace('=', '%3D'), + const p1 = rp + .get({ + headers: headers, + url: + 'http://localhost:8378/1/classes/TestParameterEncode?' + + querystring + .stringify({ + limit: 1, + }) + .replace('=', '%3D'), + }) + .then(fail, response => { + const error = response.error; + const b = JSON.parse(error); + expect(b.code).toEqual(Parse.Error.INVALID_QUERY); + }); + return Promise.all([p0, p1]); }) - .then(fail, (response) => { - const error = response.error; - const b = JSON.parse(error); - expect(b.code).toEqual(Parse.Error.INVALID_QUERY); - }); - return Promise.all([p0, p1]); - }).then(done).catch((err) => { - jfail(err); - fail('should not fail'); - done(); - }) + .then(done) + .catch(err => { + jfail(err); + fail('should not fail'); + 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('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('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(); - }); + it('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(); + }); }); it('makes sure null pointers are handed correctly #2189', done => { const object = new Parse.Object('AnObject'); const anotherObject = new Parse.Object('AnotherObject'); - anotherObject.save().then(() => { - object.set('values', [null, null, anotherObject]); - return object.save(); - }).then(() => { - const query = new Parse.Query('AnObject'); - query.include('values'); - return query.first(); - }).then((result) => { - const values = result.get('values'); - expect(values.length).toBe(3); - let anotherObjectFound = false; - let nullCounts = 0; - for(const value of values) { - if (value === null) { - nullCounts++; - } else if (value instanceof Parse.Object) { - anotherObjectFound = true; + anotherObject + .save() + .then(() => { + object.set('values', [null, null, anotherObject]); + return object.save(); + }) + .then(() => { + const query = new Parse.Query('AnObject'); + query.include('values'); + return query.first(); + }) + .then( + result => { + const values = result.get('values'); + expect(values.length).toBe(3); + let anotherObjectFound = false; + let nullCounts = 0; + for (const value of values) { + if (value === null) { + nullCounts++; + } else if (value instanceof Parse.Object) { + anotherObjectFound = true; + } + } + expect(nullCounts).toBe(2); + expect(anotherObjectFound).toBeTruthy(); + done(); + }, + err => { + console.error(err); + fail(err); + done(); } - } - expect(nullCounts).toBe(2); - expect(anotherObjectFound).toBeTruthy(); - done(); - }, (err) => { - console.error(err); - fail(err); - done(); - }); + ); }); }); diff --git a/spec/RevocableSessionsUpgrade.spec.js b/spec/RevocableSessionsUpgrade.spec.js index 8b546695a8..2cf5a2736e 100644 --- a/spec/RevocableSessionsUpgrade.spec.js +++ b/spec/RevocableSessionsUpgrade.spec.js @@ -9,14 +9,13 @@ function createUser() { objectId: '1234567890', username: 'hello', password: 'pass', - _session_token: sessionToken - } + _session_token: sessionToken, + }; return config.database.create('_User', user); } describe_only_db('mongo')('revocable sessions', () => { - - beforeEach((done) => { + beforeEach(done => { // Create 1 user with the legacy createUser().then(done); }); @@ -25,48 +24,71 @@ describe_only_db('mongo')('revocable sessions', () => { const user = Parse.Object.fromJSON({ className: '_User', objectId: '1234567890', - sessionToken: sessionToken - }); - user._upgradeToRevocableSession().then((res) => { - expect(res.getSessionToken().indexOf('r:')).toBe(0); - const config = Config.get(Parse.applicationId); - // use direct access to the DB to make sure we're not - // getting the session token stripped - return config.database.loadSchema().then(schemaController => { - return schemaController.getOneSchema('_User', true) - }).then((schema) => { - return config.database.adapter.find('_User', schema, {objectId: '1234567890'}, {}) - }).then((results) => { - expect(results.length).toBe(1); - expect(results[0].sessionToken).toBeUndefined(); - }); - }).then(() => { - done(); - }, (err) => { - jfail(err); - done(); + sessionToken: sessionToken, }); + user + ._upgradeToRevocableSession() + .then(res => { + expect(res.getSessionToken().indexOf('r:')).toBe(0); + const config = Config.get(Parse.applicationId); + // use direct access to the DB to make sure we're not + // getting the session token stripped + return config.database + .loadSchema() + .then(schemaController => { + return schemaController.getOneSchema('_User', true); + }) + .then(schema => { + return config.database.adapter.find( + '_User', + schema, + { objectId: '1234567890' }, + {} + ); + }) + .then(results => { + expect(results.length).toBe(1); + expect(results[0].sessionToken).toBeUndefined(); + }); + }) + .then( + () => { + done(); + }, + err => { + jfail(err); + done(); + } + ); }); it('should be able to become with revocable session token', done => { const user = Parse.Object.fromJSON({ className: '_User', objectId: '1234567890', - sessionToken: sessionToken - }); - user._upgradeToRevocableSession().then((res) => { - expect(res.getSessionToken().indexOf('r:')).toBe(0); - return Parse.User.logOut().then(() => { - return Parse.User.become(res.getSessionToken()) - }).then((user) => { - expect(user.id).toEqual('1234567890'); - }); - }).then(() => { - done(); - }, (err) => { - jfail(err); - done(); + sessionToken: sessionToken, }); + user + ._upgradeToRevocableSession() + .then(res => { + expect(res.getSessionToken().indexOf('r:')).toBe(0); + return Parse.User.logOut() + .then(() => { + return Parse.User.become(res.getSessionToken()); + }) + .then(user => { + expect(user.id).toEqual('1234567890'); + }); + }) + .then( + () => { + done(); + }, + err => { + jfail(err); + done(); + } + ); }); it('should not upgrade bad legacy session token', done => { @@ -75,19 +97,24 @@ describe_only_db('mongo')('revocable sessions', () => { headers: { 'X-Parse-Application-Id': Parse.applicationId, 'X-Parse-Rest-API-Key': 'rest', - 'X-Parse-Session-Token': 'badSessionToken' + 'X-Parse-Session-Token': 'badSessionToken', }, - json: true - }).then(() => { - fail('should not be able to upgrade a bad token'); - }, (response) => { - expect(response.statusCode).toBe(400); - expect(response.error).not.toBeUndefined(); - expect(response.error.code).toBe(Parse.Error.INVALID_SESSION_TOKEN); - expect(response.error.error).toEqual('invalid legacy session token'); - }).then(() => { - done(); - }); + json: true, + }) + .then( + () => { + fail('should not be able to upgrade a bad token'); + }, + response => { + expect(response.statusCode).toBe(400); + expect(response.error).not.toBeUndefined(); + expect(response.error.code).toBe(Parse.Error.INVALID_SESSION_TOKEN); + expect(response.error.error).toEqual('invalid legacy session token'); + } + ) + .then(() => { + done(); + }); }); it('should not crash without session token #2720', done => { @@ -95,18 +122,23 @@ describe_only_db('mongo')('revocable sessions', () => { url: Parse.serverURL + '/upgradeToRevocableSession', headers: { 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Rest-API-Key': 'rest' + 'X-Parse-Rest-API-Key': 'rest', }, - json: true - }).then(() => { - fail('should not be able to upgrade a bad token'); - }, (response) => { - expect(response.statusCode).toBe(404); - expect(response.error).not.toBeUndefined(); - expect(response.error.code).toBe(Parse.Error.OBJECT_NOT_FOUND); - expect(response.error.error).toEqual('invalid session'); - }).then(() => { - done(); - }); + json: true, + }) + .then( + () => { + fail('should not be able to upgrade a bad token'); + }, + response => { + expect(response.statusCode).toBe(404); + expect(response.error).not.toBeUndefined(); + expect(response.error.code).toBe(Parse.Error.OBJECT_NOT_FOUND); + expect(response.error.error).toEqual('invalid session'); + } + ) + .then(() => { + done(); + }); }); -}) +}); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index e308234fc3..1d2703b9c4 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -12,10 +12,13 @@ const hasAllPODobject = () => { obj.set('aString', 'string'); obj.set('aBool', true); obj.set('aDate', new Date()); - obj.set('aObject', {k1: 'value', k2: true, k3: 5}); + obj.set('aObject', { k1: 'value', k2: true, k3: 5 }); obj.set('aArray', ['contents', true, 5]); - obj.set('aGeoPoint', new Parse.GeoPoint({latitude: 0, longitude: 0})); - obj.set('aFile', new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' })); + obj.set('aGeoPoint', new Parse.GeoPoint({ latitude: 0, longitude: 0 })); + obj.set( + 'aFile', + new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' }) + ); return obj; }; @@ -24,197 +27,280 @@ describe('SchemaController', () => { config = Config.get('test'); }); - it('can validate one object', (done) => { - config.database.loadSchema().then((schema) => { - return schema.validateObject('TestObject', {a: 1, b: 'yo', c: false}); - }).then(() => { - done(); - }, (error) => { - jfail(error); - done(); - }); + it('can validate one object', done => { + config.database + .loadSchema() + .then(schema => { + return schema.validateObject('TestObject', { a: 1, b: 'yo', c: false }); + }) + .then( + () => { + done(); + }, + error => { + jfail(error); + done(); + } + ); }); - it('can validate one object with dot notation', (done) => { - config.database.loadSchema().then((schema) => { - return schema.validateObject('TestObjectWithSubDoc', {x: false, y: 'YY', z: 1, 'aObject.k1': 'newValue'}); - }).then(() => { - done(); - }, (error) => { - jfail(error); - done(); - }); + it('can validate one object with dot notation', done => { + config.database + .loadSchema() + .then(schema => { + return schema.validateObject('TestObjectWithSubDoc', { + x: false, + y: 'YY', + z: 1, + 'aObject.k1': 'newValue', + }); + }) + .then( + () => { + done(); + }, + error => { + jfail(error); + done(); + } + ); }); - it('can validate two objects in a row', (done) => { - config.database.loadSchema().then((schema) => { - return schema.validateObject('Foo', {x: true, y: 'yyy', z: 0}); - }).then((schema) => { - return schema.validateObject('Foo', {x: false, y: 'YY', z: 1}); - }).then(() => { - done(); - }); + it('can validate two objects in a row', done => { + config.database + .loadSchema() + .then(schema => { + return schema.validateObject('Foo', { x: true, y: 'yyy', z: 0 }); + }) + .then(schema => { + return schema.validateObject('Foo', { x: false, y: 'YY', z: 1 }); + }) + .then(() => { + done(); + }); }); - it('can validate Relation object', (done) => { - config.database.loadSchema().then((schema) => { - return schema.validateObject('Stuff', {aRelation: {__type:'Relation',className:'Stuff'}}); - }).then((schema) => { - return schema.validateObject('Stuff', {aRelation: {__type:'Pointer',className:'Stuff'}}) - .then(() => { - fail('expected invalidity'); + it('can validate Relation object', done => { + config.database + .loadSchema() + .then(schema => { + return schema.validateObject('Stuff', { + aRelation: { __type: 'Relation', className: 'Stuff' }, + }); + }) + .then( + schema => { + return schema + .validateObject('Stuff', { + aRelation: { __type: 'Pointer', className: 'Stuff' }, + }) + .then(() => { + fail('expected invalidity'); + done(); + }, done); + }, + err => { + fail(err); done(); - }, done); - }, (err) => { - fail(err); - done(); - }); + } + ); }); - it('rejects inconsistent types', (done) => { - config.database.loadSchema().then((schema) => { - return schema.validateObject('Stuff', {bacon: 7}); - }).then((schema) => { - return schema.validateObject('Stuff', {bacon: 'z'}); - }).then(() => { - fail('expected invalidity'); - done(); - }, done); + it('rejects inconsistent types', done => { + config.database + .loadSchema() + .then(schema => { + return schema.validateObject('Stuff', { bacon: 7 }); + }) + .then(schema => { + return schema.validateObject('Stuff', { bacon: 'z' }); + }) + .then(() => { + fail('expected invalidity'); + done(); + }, done); }); - it('updates when new fields are added', (done) => { - config.database.loadSchema().then((schema) => { - return schema.validateObject('Stuff', {bacon: 7}); - }).then((schema) => { - return schema.validateObject('Stuff', {sausage: 8}); - }).then((schema) => { - return schema.validateObject('Stuff', {sausage: 'ate'}); - }).then(() => { - fail('expected invalidity'); - done(); - }, done); + it('updates when new fields are added', done => { + config.database + .loadSchema() + .then(schema => { + return schema.validateObject('Stuff', { bacon: 7 }); + }) + .then(schema => { + return schema.validateObject('Stuff', { sausage: 8 }); + }) + .then(schema => { + return schema.validateObject('Stuff', { sausage: 'ate' }); + }) + .then(() => { + fail('expected invalidity'); + done(); + }, done); }); - it('class-level permissions test find', (done) => { - config.database.loadSchema().then((schema) => { - // Just to create a valid class - return schema.validateObject('Stuff', {foo: 'bar'}); - }).then((schema) => { - return schema.setPermissions('Stuff', { - 'find': {} - }); - }).then(() => { - const query = new Parse.Query('Stuff'); - return query.find(); - }).then(() => { - fail('Class permissions should have rejected this query.'); - done(); - }, () => { - done(); - }); + it('class-level permissions test find', done => { + config.database + .loadSchema() + .then(schema => { + // Just to create a valid class + return schema.validateObject('Stuff', { foo: 'bar' }); + }) + .then(schema => { + return schema.setPermissions('Stuff', { + find: {}, + }); + }) + .then(() => { + const query = new Parse.Query('Stuff'); + return query.find(); + }) + .then( + () => { + fail('Class permissions should have rejected this query.'); + done(); + }, + () => { + done(); + } + ); }); - it('class-level permissions test user', (done) => { + it('class-level permissions test user', done => { let user; - 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) => { - const find = {}; - find[user.id] = true; - return schema.setPermissions('Stuff', { - 'find': find - }); - }).then(() => { - const query = new Parse.Query('Stuff'); - return query.find(); - }).then(() => { - done(); - }, () => { - fail('Class permissions should have allowed this query.'); - done(); - }); + 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 => { + const find = {}; + find[user.id] = true; + return schema.setPermissions('Stuff', { + find: find, + }); + }) + .then(() => { + const query = new Parse.Query('Stuff'); + return query.find(); + }) + .then( + () => { + done(); + }, + () => { + fail('Class permissions should have allowed this query.'); + done(); + } + ); }); - it('class-level permissions test get', (done) => { + it('class-level permissions test get', done => { let obj; - createTestUser() - .then(user => { - return config.database.loadSchema() - // Create a valid class - .then(schema => schema.validateObject('Stuff', {foo: 'bar'})) + createTestUser().then(user => { + return ( + config.database + .loadSchema() + // Create a valid class + .then(schema => schema.validateObject('Stuff', { foo: 'bar' })) .then(schema => { const find = {}; const get = {}; get[user.id] = true; return schema.setPermissions('Stuff', { - 'create': {'*': true}, - 'find': find, - 'get': get + create: { '*': true }, + find: find, + get: get, }); - }).then(() => { + }) + .then(() => { obj = new Parse.Object('Stuff'); obj.set('foo', 'bar'); return obj.save(); - }).then((o) => { + }) + .then(o => { obj = o; const query = new Parse.Query('Stuff'); return query.find(); - }).then(() => { - fail('Class permissions should have rejected this query.'); - done(); - }, () => { - const query = new Parse.Query('Stuff'); - return query.get(obj.id).then(() => { - done(); - }, () => { - fail('Class permissions should have allowed this get query'); + }) + .then( + () => { + fail('Class permissions should have rejected this query.'); done(); - }); - }); - }); + }, + () => { + const query = new Parse.Query('Stuff'); + return query.get(obj.id).then( + () => { + done(); + }, + () => { + fail('Class permissions should have allowed this get query'); + done(); + } + ); + } + ) + ); + }); }); - it('class-level permissions test count', (done) => { + it('class-level permissions test count', done => { let obj; - return config.database.loadSchema() - // Create a valid class - .then(schema => schema.validateObject('Stuff', {foo: 'bar'})) - .then(schema => { - const count = {}; - return schema.setPermissions('Stuff', { - 'create': {'*': true}, - 'find': {'*': true}, - 'count': count + return ( + config.database + .loadSchema() + // Create a valid class + .then(schema => schema.validateObject('Stuff', { foo: 'bar' })) + .then(schema => { + const count = {}; + return schema.setPermissions('Stuff', { + create: { '*': true }, + find: { '*': true }, + count: count, + }); }) - }).then(() => { - obj = new Parse.Object('Stuff'); - obj.set('foo', 'bar'); - return obj.save(); - }).then((o) => { - obj = o; - const query = new Parse.Query('Stuff'); - return query.find(); - }).then((results) => { - expect(results.length).toBe(1); - const query = new Parse.Query('Stuff'); - return query.count(); - }).then(() => { - fail('Class permissions should have rejected this query.'); - }, (err) => { - expect(err.message).toEqual('Permission denied for action count on class Stuff.'); - done(); - }); + .then(() => { + obj = new Parse.Object('Stuff'); + obj.set('foo', 'bar'); + return obj.save(); + }) + .then(o => { + obj = o; + const query = new Parse.Query('Stuff'); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(1); + const query = new Parse.Query('Stuff'); + return query.count(); + }) + .then( + () => { + fail('Class permissions should have rejected this query.'); + }, + err => { + expect(err.message).toEqual( + 'Permission denied for action count on class Stuff.' + ); + done(); + } + ) + ); }); it('can add classes without needing an object', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 'String'}, - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + foo: { type: 'String' }, + }) + ) .then(actualSchema => { const expectedSchema = { className: 'NewClass', @@ -233,7 +319,7 @@ describe('SchemaController', () => { delete: { '*': true }, addField: { '*': true }, }, - } + }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); }) @@ -251,145 +337,161 @@ describe('SchemaController', () => { delete: { '*': true }, addField: { '*': true }, }; - config.database.loadSchema() - .then(schema => { - schema.validateObject('NewClass', { foo: 2 }) - .then(() => schema.reloadData()) - .then(() => schema.updateClass('NewClass', { - fooOne: {type: 'Number'}, - fooTwo: {type: 'Array'}, - fooThree: {type: 'Date'}, - fooFour: {type: 'Object'}, - fooFive: {type: 'Relation', targetClass: '_User' }, - fooSix: {type: 'String'}, - fooSeven: {type: 'Object' }, - fooEight: {type: 'String'}, - fooNine: {type: 'String'}, - fooTeen: {type: 'Number' }, - fooEleven: {type: 'String'}, - fooTwelve: {type: 'String'}, - fooThirteen: {type: 'String'}, - fooFourteen: {type: 'String'}, - fooFifteen: {type: 'String'}, - fooSixteen: {type: 'String'}, - fooEighteen: {type: 'String'}, - fooNineteen: {type: 'String'}, - }, levelPermissions, {}, config.database)) - .then(actualSchema => { - const expectedSchema = { - className: 'NewClass', - fields: { - objectId: { type: 'String' }, - updatedAt: { type: 'Date' }, - createdAt: { type: 'Date' }, - ACL: { type: 'ACL' }, - foo: { type: 'Number' }, - fooOne: {type: 'Number'}, - fooTwo: {type: 'Array'}, - fooThree: {type: 'Date'}, - fooFour: {type: 'Object'}, - fooFive: {type: 'Relation', targetClass: '_User' }, - fooSix: {type: 'String'}, - fooSeven: {type: 'Object' }, - fooEight: {type: 'String'}, - fooNine: {type: 'String'}, - fooTeen: {type: 'Number' }, - fooEleven: {type: 'String'}, - fooTwelve: {type: 'String'}, - fooThirteen: {type: 'String'}, - fooFourteen: {type: 'String'}, - fooFifteen: {type: 'String'}, - fooSixteen: {type: 'String'}, - fooEighteen: {type: 'String'}, - fooNineteen: {type: 'String'}, - }, - classLevelPermissions: { ...levelPermissions }, - indexes: { - _id_: { _id: 1 } - } - }; - - expect(dd(actualSchema, expectedSchema)).toEqual(undefined); - done(); - }) - .catch(error => { - console.trace(error); - done(); - fail('Error creating class: ' + JSON.stringify(error)); - }); - }); + config.database.loadSchema().then(schema => { + schema + .validateObject('NewClass', { foo: 2 }) + .then(() => schema.reloadData()) + .then(() => + schema.updateClass( + 'NewClass', + { + fooOne: { type: 'Number' }, + fooTwo: { type: 'Array' }, + fooThree: { type: 'Date' }, + fooFour: { type: 'Object' }, + fooFive: { type: 'Relation', targetClass: '_User' }, + fooSix: { type: 'String' }, + fooSeven: { type: 'Object' }, + fooEight: { type: 'String' }, + fooNine: { type: 'String' }, + fooTeen: { type: 'Number' }, + fooEleven: { type: 'String' }, + fooTwelve: { type: 'String' }, + fooThirteen: { type: 'String' }, + fooFourteen: { type: 'String' }, + fooFifteen: { type: 'String' }, + fooSixteen: { type: 'String' }, + fooEighteen: { type: 'String' }, + fooNineteen: { type: 'String' }, + }, + levelPermissions, + {}, + config.database + ) + ) + .then(actualSchema => { + const expectedSchema = { + className: 'NewClass', + fields: { + objectId: { type: 'String' }, + updatedAt: { type: 'Date' }, + createdAt: { type: 'Date' }, + ACL: { type: 'ACL' }, + foo: { type: 'Number' }, + fooOne: { type: 'Number' }, + fooTwo: { type: 'Array' }, + fooThree: { type: 'Date' }, + fooFour: { type: 'Object' }, + fooFive: { type: 'Relation', targetClass: '_User' }, + fooSix: { type: 'String' }, + fooSeven: { type: 'Object' }, + fooEight: { type: 'String' }, + fooNine: { type: 'String' }, + fooTeen: { type: 'Number' }, + fooEleven: { type: 'String' }, + fooTwelve: { type: 'String' }, + fooThirteen: { type: 'String' }, + fooFourteen: { type: 'String' }, + fooFifteen: { type: 'String' }, + fooSixteen: { type: 'String' }, + fooEighteen: { type: 'String' }, + fooNineteen: { type: 'String' }, + }, + classLevelPermissions: { ...levelPermissions }, + indexes: { + _id_: { _id: 1 }, + }, + }; + + expect(dd(actualSchema, expectedSchema)).toEqual(undefined); + done(); + }) + .catch(error => { + console.trace(error); + done(); + fail('Error creating class: ' + JSON.stringify(error)); + }); + }); }); it('will fail to create a class if that class was already created by an object', done => { - config.database.loadSchema() - .then(schema => { - schema.validateObject('NewClass', { foo: 7 }) - .then(() => schema.reloadData()) - .then(() => schema.addClassIfNotExists('NewClass', { - foo: { type: 'String' } - })) - .catch(error => { - expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(error.message).toEqual('Class NewClass already exists.'); - done(); - }); - }); + config.database.loadSchema().then(schema => { + schema + .validateObject('NewClass', { foo: 7 }) + .then(() => schema.reloadData()) + .then(() => + schema.addClassIfNotExists('NewClass', { + foo: { type: 'String' }, + }) + ) + .catch(error => { + expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(error.message).toEqual('Class NewClass already exists.'); + done(); + }); + }); }); it('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 => { - const p1 = schema.addClassIfNotExists('NewClass', {foo: {type: 'String'}}); - const p2 = schema.addClassIfNotExists('NewClass', {foo: {type: '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 => { - expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(error.message).toEqual('Class NewClass already exists.'); - done(); - }); + config.database.loadSchema().then(schema => { + const p1 = schema.addClassIfNotExists('NewClass', { + foo: { type: 'String' }, + }); + const p2 = schema.addClassIfNotExists('NewClass', { + foo: { type: '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 => { + expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(error.message).toEqual('Class NewClass already exists.'); + done(); + }); + }); }); it('refuses to create classes with invalid names', done => { - config.database.loadSchema() - .then(schema => { - schema.addClassIfNotExists('_InvalidName', {foo: {type: 'String'}}) - .catch(error => { - expect(error.error).toEqual( - 'Invalid classname: _InvalidName, classnames can only have alphanumeric characters and _, and must start with an alpha character ' - ); - done(); - }); - }); + config.database.loadSchema().then(schema => { + schema + .addClassIfNotExists('_InvalidName', { foo: { type: 'String' } }) + .catch(error => { + expect(error.error).toEqual( + 'Invalid classname: _InvalidName, classnames can only have alphanumeric characters and _, and must start with an alpha character ' + ); + done(); + }); + }); }); it('refuses to add fields with invalid names', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', {'0InvalidName': {type: 'String'}})) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + '0InvalidName': { type: 'String' }, + }) + ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME); expect(error.error).toEqual('invalid field name: 0InvalidName'); @@ -398,8 +500,11 @@ describe('SchemaController', () => { }); it('refuses to explicitly create the default fields for custom classes', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', {objectId: {type: 'String'}})) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { objectId: { type: 'String' } }) + ) .catch(error => { expect(error.code).toEqual(136); expect(error.error).toEqual('field objectId cannot be added'); @@ -408,8 +513,13 @@ describe('SchemaController', () => { }); it('refuses to explicitly create the default fields for non-custom classes', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('_Installation', {localeIdentifier: {type: 'String'}})) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('_Installation', { + localeIdentifier: { type: 'String' }, + }) + ) .catch(error => { expect(error.code).toEqual(136); expect(error.error).toEqual('field localeIdentifier cannot be added'); @@ -418,10 +528,13 @@ describe('SchemaController', () => { }); it('refuses to add fields with invalid types', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 7} - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + foo: { type: 7 }, + }) + ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); expect(error.error).toEqual('invalid JSON'); @@ -430,10 +543,13 @@ describe('SchemaController', () => { }); it('refuses to add fields with invalid pointer types', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 'Pointer'} - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + foo: { type: 'Pointer' }, + }) + ) .catch(error => { expect(error.code).toEqual(135); expect(error.error).toEqual('type Pointer needs a class name'); @@ -442,10 +558,13 @@ describe('SchemaController', () => { }); it('refuses to add fields with invalid pointer target', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 'Pointer', targetClass: 7}, - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + foo: { type: 'Pointer', targetClass: 7 }, + }) + ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); expect(error.error).toEqual('invalid JSON'); @@ -454,10 +573,13 @@ describe('SchemaController', () => { }); it('refuses to add fields with invalid Relation type', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 'Relation', uselessKey: 7}, - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + foo: { type: 'Relation', uselessKey: 7 }, + }) + ) .catch(error => { expect(error.code).toEqual(135); expect(error.error).toEqual('type Relation needs a class name'); @@ -466,10 +588,13 @@ describe('SchemaController', () => { }); it('refuses to add fields with invalid relation target', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 'Relation', targetClass: 7}, - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + foo: { type: 'Relation', targetClass: 7 }, + }) + ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); expect(error.error).toEqual('invalid JSON'); @@ -478,34 +603,47 @@ describe('SchemaController', () => { }); it('refuses to add fields with uncreatable pointer target class', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 'Pointer', targetClass: 'not a valid class name'}, - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + foo: { type: 'Pointer', targetClass: 'not a valid class name' }, + }) + ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(error.error).toEqual('Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character '); + expect(error.error).toEqual( + 'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character ' + ); done(); }); }); it('refuses to add fields with uncreatable relation target class', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 'Relation', targetClass: 'not a valid class name'}, - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + foo: { type: 'Relation', targetClass: 'not a valid class name' }, + }) + ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(error.error).toEqual('Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character '); + expect(error.error).toEqual( + 'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character ' + ); done(); }); }); it('refuses to add fields with unknown types', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - foo: {type: 'Unknown'}, - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + foo: { type: 'Unknown' }, + }) + ) .catch(error => { expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); expect(error.error).toEqual('invalid field type: Unknown'); @@ -514,21 +652,27 @@ describe('SchemaController', () => { }); it('will create classes', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - 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'}, - aBytes: {type: 'Bytes'}, - aPolygon: {type: 'Polygon'}, - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + 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' }, + aBytes: { type: 'Bytes' }, + aPolygon: { type: 'Polygon' }, + }) + ) .then(actualSchema => { const expectedSchema = { className: 'NewClass', @@ -545,10 +689,13 @@ describe('SchemaController', () => { aArray: { type: 'Array' }, aGeoPoint: { type: 'GeoPoint' }, aFile: { type: 'File' }, - aPointer: { type: 'Pointer', targetClass: 'ThisClassDoesNotExistYet' }, + aPointer: { + type: 'Pointer', + targetClass: 'ThisClassDoesNotExistYet', + }, aRelation: { type: 'Relation', targetClass: 'NewClass' }, - aBytes: {type: 'Bytes'}, - aPolygon: {type: 'Polygon'}, + aBytes: { type: 'Bytes' }, + aPolygon: { type: 'Polygon' }, }, classLevelPermissions: { find: { '*': true }, @@ -558,17 +705,20 @@ describe('SchemaController', () => { delete: { '*': true }, addField: { '*': true }, }, - } + }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); }); }); it('creates the default fields for non-custom classes', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('_Installation', { - foo: {type: 'Number'}, - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('_Installation', { + foo: { type: 'Number' }, + }) + ) .then(actualSchema => { const expectedSchema = { className: '_Installation', @@ -600,15 +750,16 @@ describe('SchemaController', () => { delete: { '*': true }, addField: { '*': true }, }, - } + }; expect(dd(actualSchema, expectedSchema)).toEqual(undefined); done(); }); }); it('creates non-custom classes which include relation field', done => { - config.database.loadSchema() - //as `_Role` is always created by default, we only get it here + config.database + .loadSchema() + //as `_Role` is always created by default, we only get it here .then(schema => schema.getOneSchema('_Role')) .then(actualSchema => { const expectedSchema = { @@ -637,7 +788,8 @@ describe('SchemaController', () => { }); it('creates non-custom classes which include pointer field', done => { - config.database.loadSchema() + config.database + .loadSchema() .then(schema => schema.addClassIfNotExists('_Session', {})) .then(actualSchema => { const expectedSchema = { @@ -669,31 +821,40 @@ describe('SchemaController', () => { }); it('refuses to create two geopoints', done => { - config.database.loadSchema() - .then(schema => schema.addClassIfNotExists('NewClass', { - geo1: {type: 'GeoPoint'}, - geo2: {type: 'GeoPoint'} - })) + config.database + .loadSchema() + .then(schema => + schema.addClassIfNotExists('NewClass', { + geo1: { type: 'GeoPoint' }, + geo2: { type: 'GeoPoint' }, + }) + ) .catch(error => { expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); - expect(error.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding geo2 when geo1 already exists.'); + expect(error.error).toEqual( + 'currently, only one GeoPoint field may exist in an object. Adding geo2 when geo1 already exists.' + ); done(); }); }); it('can check if a class exists', done => { - config.database.loadSchema() + config.database + .loadSchema() .then(schema => { - return schema.addClassIfNotExists('NewClass', {}) + return schema + .addClassIfNotExists('NewClass', {}) .then(() => { - schema.hasClass('NewClass') + schema + .hasClass('NewClass') .then(hasClass => { expect(hasClass).toEqual(true); done(); }) .catch(fail); - schema.hasClass('NonexistantClass') + schema + .hasClass('NonexistantClass') .then(hasClass => { expect(hasClass).toEqual(false); done(); @@ -701,15 +862,16 @@ describe('SchemaController', () => { .catch(fail); }) .catch(error => { - fail('Couldn\'t create class'); + fail("Couldn't create class"); jfail(error); }); }) - .catch(() => fail('Couldn\'t load schema')); + .catch(() => fail("Couldn't load schema")); }); it('refuses to delete fields from invalid class names', done => { - config.database.loadSchema() + config.database + .loadSchema() .then(schema => schema.deleteField('fieldName', 'invalid class name')) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); @@ -718,8 +880,11 @@ describe('SchemaController', () => { }); it('refuses to delete invalid fields', done => { - config.database.loadSchema() - .then(schema => schema.deleteField('invalid field name', 'ValidClassName')) + config.database + .loadSchema() + .then(schema => + schema.deleteField('invalid field name', 'ValidClassName') + ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME); done(); @@ -727,7 +892,8 @@ describe('SchemaController', () => { }); it('refuses to delete the default fields', done => { - config.database.loadSchema() + config.database + .loadSchema() .then(schema => schema.deleteField('installationId', '_Installation')) .catch(error => { expect(error.code).toEqual(136); @@ -737,7 +903,8 @@ describe('SchemaController', () => { }); it('refuses to delete fields from nonexistant classes', done => { - config.database.loadSchema() + config.database + .loadSchema() .then(schema => schema.deleteField('field', 'NoClass')) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); @@ -747,19 +914,23 @@ describe('SchemaController', () => { }); it('refuses to delete fields that dont exist', done => { - hasAllPODobject().save() + hasAllPODobject() + .save() .then(() => config.database.loadSchema()) .then(schema => schema.deleteField('missingField', 'HasAllPOD')) .catch(error => { expect(error.code).toEqual(255); - expect(error.message).toEqual('Field missingField does not exist, cannot delete.'); + expect(error.message).toEqual( + 'Field missingField does not exist, cannot delete.' + ); done(); }); }); it('drops related collection when deleting relation field', done => { const obj1 = hasAllPODobject(); - obj1.save() + obj1 + .save() .then(savedObj1 => { const obj2 = new Parse.Object('HasPointersAndRelations'); obj2.set('aPointer', savedObj1); @@ -767,77 +938,102 @@ describe('SchemaController', () => { relation.add(obj1); return obj2.save(); }) - .then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations')) + .then(() => + config.database.collectionExists( + '_Join:aRelation:HasPointersAndRelations' + ) + ) .then(exists => { if (!exists) { - fail('Relation collection ' + - 'should exist after save.'); + fail('Relation collection ' + 'should exist after save.'); } }) .then(() => config.database.loadSchema()) - .then(schema => schema.deleteField('aRelation', 'HasPointersAndRelations', config.database)) - .then(() => config.database.collectionExists('_Join:aRelation:HasPointersAndRelations')) - .then(exists => { - if (exists) { - fail('Relation collection should not exist after deleting relation field.'); + .then(schema => + schema.deleteField( + 'aRelation', + 'HasPointersAndRelations', + config.database + ) + ) + .then(() => + config.database.collectionExists( + '_Join:aRelation:HasPointersAndRelations' + ) + ) + .then( + exists => { + if (exists) { + fail( + 'Relation collection should not exist after deleting relation field.' + ); + } + done(); + }, + error => { + jfail(error); + done(); } - done(); - }, error => { - jfail(error); - done(); - }); + ); }); it('can delete relation field when related _Join collection not exist', done => { - config.database.loadSchema() - .then(schema => { - schema.addClassIfNotExists('NewClass', { - relationField: {type: 'Relation', targetClass: '_User'} + config.database.loadSchema().then(schema => { + schema + .addClassIfNotExists('NewClass', { + relationField: { type: 'Relation', targetClass: '_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 => { - on_db('postgres', () => { - // We create the table when creating the column - expect(exist).toEqual(true); - }, () => { - expect(exist).toEqual(false); - }); - - }) - .then(() => schema.deleteField('relationField', 'NewClass', config.database)) - .then(() => schema.reloadData()) - .then(() => { - const expectedSchema = { + .then(actualSchema => { + const expectedSchema = { + className: 'NewClass', + fields: { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, ACL: { type: 'ACL' }, - }; - expect(dd(schema.data.NewClass, expectedSchema)).toEqual(undefined); - done(); - }); - }); + 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 => { + on_db( + 'postgres', + () => { + // We create the table when creating the column + expect(exist).toEqual(true); + }, + () => { + expect(exist).toEqual(false); + } + ); + }) + .then(() => + schema.deleteField('relationField', 'NewClass', config.database) + ) + .then(() => schema.reloadData()) + .then(() => { + 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 => { @@ -846,14 +1042,20 @@ describe('SchemaController', () => { const obj2 = hasAllPODobject(); Parse.Object.saveAll([obj1, obj2]) .then(() => config.database.loadSchema()) - .then(schema => schema.deleteField('aString', 'HasAllPOD', config.database)) + .then(schema => + schema.deleteField('aString', 'HasAllPOD', config.database) + ) .then(() => new Parse.Query('HasAllPOD').get(obj1.id)) .then(obj1Reloaded => { expect(obj1Reloaded.get('aString')).toEqual(undefined); obj1Reloaded.set('aString', ['not a string', 'this time']); - obj1Reloaded.save() + obj1Reloaded + .save() .then(obj1reloadedAgain => { - expect(obj1reloadedAgain.get('aString')).toEqual(['not a string', 'this time']); + expect(obj1reloadedAgain.get('aString')).toEqual([ + 'not a string', + 'this time', + ]); return new Parse.Query('HasAllPOD').get(obj2.id); }) .then(obj2reloaded => { @@ -871,7 +1073,8 @@ describe('SchemaController', () => { it('can delete pointer fields and resave as string', done => { Parse.Object.disableSingleInstance(); const obj1 = new Parse.Object('NewClass'); - obj1.save() + obj1 + .save() .then(() => { obj1.set('aPointer', obj1); return obj1.save(); @@ -880,7 +1083,9 @@ describe('SchemaController', () => { expect(obj1.get('aPointer').id).toEqual(obj1.id); }) .then(() => config.database.loadSchema()) - .then(schema => schema.deleteField('aPointer', 'NewClass', config.database)) + .then(schema => + schema.deleteField('aPointer', 'NewClass', config.database) + ) .then(() => new Parse.Query('NewClass').get(obj1.id)) .then(obj1 => { expect(obj1.get('aPointer')).toEqual(undefined); @@ -895,45 +1100,60 @@ describe('SchemaController', () => { }); it('can merge schemas', done => { - expect(SchemaController.buildMergedSchemaObject({ - _id: 'SomeClass', - someType: { type: 'Number' } - }, { - newType: {type: 'Number'} - })).toEqual({ - someType: {type: 'Number'}, - newType: {type: 'Number'}, + expect( + SchemaController.buildMergedSchemaObject( + { + _id: 'SomeClass', + someType: { type: 'Number' }, + }, + { + newType: { type: 'Number' }, + } + ) + ).toEqual({ + someType: { type: 'Number' }, + newType: { type: 'Number' }, }); done(); }); it('can merge deletions', done => { - expect(SchemaController.buildMergedSchemaObject({ - _id: 'SomeClass', + expect( + SchemaController.buildMergedSchemaObject( + { + _id: 'SomeClass', + someType: { type: 'Number' }, + outDatedType: { type: 'String' }, + }, + { + newType: { type: 'GeoPoint' }, + outDatedType: { __op: 'Delete' }, + } + ) + ).toEqual({ someType: { type: 'Number' }, - outDatedType: { type: 'String' }, - },{ - newType: {type: 'GeoPoint'}, - outDatedType: {__op: 'Delete'}, - })).toEqual({ - someType: {type: 'Number'}, - newType: {type: 'GeoPoint'}, + newType: { type: 'GeoPoint' }, }); done(); }); it('ignore default field when merge with system class', done => { - expect(SchemaController.buildMergedSchemaObject({ - _id: '_User', - username: { type: 'String' }, - password: { type: 'String' }, - email: { type: 'String' }, - emailVerified: { type: 'Boolean' }, - },{ - emailVerified: { type: 'String' }, + expect( + SchemaController.buildMergedSchemaObject( + { + _id: '_User', + username: { type: 'String' }, + password: { type: 'String' }, + email: { type: 'String' }, + emailVerified: { type: 'Boolean' }, + }, + { + emailVerified: { type: 'String' }, + customField: { type: 'String' }, + } + ) + ).toEqual({ customField: { type: 'String' }, - })).toEqual({ - customField: { type: 'String' } }); done(); }); @@ -942,58 +1162,82 @@ describe('SchemaController', () => { const anObject = new Parse.Object('AnObject'); const anotherObject = new Parse.Object('AnotherObject'); const someObject = new Parse.Object('SomeObject'); - Parse.Object.saveAll([anObject, anotherObject, someObject]).then(() => { - anObject.set('pointer', anotherObject); - return anObject.save(); - }).then(() => { - anObject.set('pointer', someObject); - return anObject.save(); - }).then(() => { - fail('shoud not save correctly'); - done(); - }, (err) => { - expect(err instanceof Parse.Error).toBeTruthy(); - expect(err.message).toEqual('schema mismatch for AnObject.pointer; expected Pointer but got Pointer') - done(); - }); + Parse.Object.saveAll([anObject, anotherObject, someObject]) + .then(() => { + anObject.set('pointer', anotherObject); + return anObject.save(); + }) + .then(() => { + anObject.set('pointer', someObject); + return anObject.save(); + }) + .then( + () => { + fail('shoud not save correctly'); + done(); + }, + err => { + expect(err instanceof Parse.Error).toBeTruthy(); + expect(err.message).toEqual( + 'schema mismatch for AnObject.pointer; expected Pointer but got Pointer' + ); + done(); + } + ); }); it('yields a proper schema mismatch error bis (#2661)', done => { const anObject = new Parse.Object('AnObject'); const someObject = new Parse.Object('SomeObject'); - Parse.Object.saveAll([anObject, someObject]).then(() => { - anObject.set('number', 1); - return anObject.save(); - }).then(() => { - anObject.set('number', someObject); - return anObject.save(); - }).then(() => { - fail('shoud not save correctly'); - done(); - }, (err) => { - expect(err instanceof Parse.Error).toBeTruthy(); - expect(err.message).toEqual('schema mismatch for AnObject.number; expected Number but got Pointer') - done(); - }); + Parse.Object.saveAll([anObject, someObject]) + .then(() => { + anObject.set('number', 1); + return anObject.save(); + }) + .then(() => { + anObject.set('number', someObject); + return anObject.save(); + }) + .then( + () => { + fail('shoud not save correctly'); + done(); + }, + err => { + expect(err instanceof Parse.Error).toBeTruthy(); + expect(err.message).toEqual( + 'schema mismatch for AnObject.number; expected Number but got Pointer' + ); + done(); + } + ); }); it('yields a proper schema mismatch error ter (#2661)', done => { const anObject = new Parse.Object('AnObject'); const someObject = new Parse.Object('SomeObject'); - Parse.Object.saveAll([anObject, someObject]).then(() => { - anObject.set('pointer', someObject); - return anObject.save(); - }).then(() => { - anObject.set('pointer', 1); - return anObject.save(); - }).then(() => { - fail('shoud not save correctly'); - done(); - }, (err) => { - expect(err instanceof Parse.Error).toBeTruthy(); - expect(err.message).toEqual('schema mismatch for AnObject.pointer; expected Pointer but got Number') - done(); - }); + Parse.Object.saveAll([anObject, someObject]) + .then(() => { + anObject.set('pointer', someObject); + return anObject.save(); + }) + .then(() => { + anObject.set('pointer', 1); + return anObject.save(); + }) + .then( + () => { + fail('shoud not save correctly'); + done(); + }, + err => { + expect(err instanceof Parse.Error).toBeTruthy(); + expect(err.message).toEqual( + 'schema mismatch for AnObject.pointer; expected Pointer but got Number' + ); + done(); + } + ); }); it('properly handles volatile _Schemas', done => { @@ -1014,297 +1258,392 @@ describe('SchemaController', () => { }); } let schema; - config.database.loadSchema().then(s => { - schema = s; - return schema.getOneSchema('_User', false); - }).then(userSchema => { - validateSchemaStructure(userSchema); - validateSchemaDataStructure(schema.data); - return schema.getOneSchema('_PushStatus', true); - }).then(pushStatusSchema => { - validateSchemaStructure(pushStatusSchema); - validateSchemaDataStructure(schema.data); - done(); - }); + config.database + .loadSchema() + .then(s => { + schema = s; + return schema.getOneSchema('_User', false); + }) + .then(userSchema => { + validateSchemaStructure(userSchema); + validateSchemaDataStructure(schema.data); + return schema.getOneSchema('_PushStatus', true); + }) + .then(pushStatusSchema => { + validateSchemaStructure(pushStatusSchema); + validateSchemaDataStructure(schema.data); + done(); + }); }); }); describe('Class Level Permissions for requiredAuth', () => { - beforeEach(() => { config = Config.get('test'); }); function createUser() { - const user = new Parse.User(); - user.set("username", "hello"); - user.set("password", "world"); + const user = new Parse.User(); + user.set('username', 'hello'); + user.set('password', 'world'); return user.signUp(null); } - it('required auth test find', (done) => { - config.database.loadSchema().then((schema) => { - // Just to create a valid class - return schema.validateObject('Stuff', {foo: 'bar'}); - }).then((schema) => { - return schema.setPermissions('Stuff', { - 'find': { - 'requiresAuthentication': true + it('required auth test find', done => { + config.database + .loadSchema() + .then(schema => { + // Just to create a valid class + return schema.validateObject('Stuff', { foo: 'bar' }); + }) + .then(schema => { + return schema.setPermissions('Stuff', { + find: { + requiresAuthentication: true, + }, + }); + }) + .then(() => { + const query = new Parse.Query('Stuff'); + return query.find(); + }) + .then( + () => { + fail('Class permissions should have rejected this query.'); + done(); + }, + e => { + expect(e.message).toEqual( + 'Permission denied, user needs to be authenticated.' + ); + done(); } - }); - }).then(() => { - const query = new Parse.Query('Stuff'); - return query.find(); - }).then(() => { - fail('Class permissions should have rejected this query.'); - done(); - }, (e) => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); - done(); - }); + ); }); - it('required auth test find authenticated', (done) => { - config.database.loadSchema().then((schema) => { - // Just to create a valid class - return schema.validateObject('Stuff', {foo: 'bar'}); - }).then((schema) => { - return schema.setPermissions('Stuff', { - 'find': { - 'requiresAuthentication': true + it('required auth test find authenticated', done => { + config.database + .loadSchema() + .then(schema => { + // Just to create a valid class + return schema.validateObject('Stuff', { foo: 'bar' }); + }) + .then(schema => { + return schema.setPermissions('Stuff', { + find: { + requiresAuthentication: true, + }, + }); + }) + .then(() => { + return createUser(); + }) + .then(() => { + const query = new Parse.Query('Stuff'); + return query.find(); + }) + .then( + results => { + expect(results.length).toEqual(0); + done(); + }, + e => { + console.error(e); + fail('Should not have failed'); + done(); } - }); - }).then(() => { - return createUser(); - }).then(() => { - const query = new Parse.Query('Stuff'); - return query.find(); - }).then((results) => { - expect(results.length).toEqual(0); - done(); - }, (e) => { - console.error(e); - fail("Should not have failed"); - done(); - }); + ); }); - it('required auth should allow create authenticated', (done) => { - config.database.loadSchema().then((schema) => { - // Just to create a valid class - return schema.validateObject('Stuff', {foo: 'bar'}); - }).then((schema) => { - return schema.setPermissions('Stuff', { - 'create': { - 'requiresAuthentication': true + it('required auth should allow create authenticated', done => { + config.database + .loadSchema() + .then(schema => { + // Just to create a valid class + return schema.validateObject('Stuff', { foo: 'bar' }); + }) + .then(schema => { + return schema.setPermissions('Stuff', { + create: { + requiresAuthentication: true, + }, + }); + }) + .then(() => { + return createUser(); + }) + .then(() => { + const stuff = new Parse.Object('Stuff'); + stuff.set('foo', 'bar'); + return stuff.save(); + }) + .then( + () => { + done(); + }, + e => { + console.error(e); + fail('Should not have failed'); + done(); } - }); - }).then(() => { - return createUser(); - }).then(() => { - const stuff = new Parse.Object('Stuff'); - stuff.set('foo', 'bar'); - return stuff.save(); - }).then(() => { - done(); - }, (e) => { - console.error(e); - fail("Should not have failed"); - done(); - }); + ); }); - it('required auth should reject create when not authenticated', (done) => { - config.database.loadSchema().then((schema) => { - // Just to create a valid class - return schema.validateObject('Stuff', {foo: 'bar'}); - }).then((schema) => { - return schema.setPermissions('Stuff', { - 'create': { - 'requiresAuthentication': true + it('required auth should reject create when not authenticated', done => { + config.database + .loadSchema() + .then(schema => { + // Just to create a valid class + return schema.validateObject('Stuff', { foo: 'bar' }); + }) + .then(schema => { + return schema.setPermissions('Stuff', { + create: { + requiresAuthentication: true, + }, + }); + }) + .then(() => { + const stuff = new Parse.Object('Stuff'); + stuff.set('foo', 'bar'); + return stuff.save(); + }) + .then( + () => { + fail('Class permissions should have rejected this query.'); + done(); + }, + e => { + expect(e.message).toEqual( + 'Permission denied, user needs to be authenticated.' + ); + done(); } - }); - }).then(() => { - const stuff = new Parse.Object('Stuff'); - stuff.set('foo', 'bar'); - return stuff.save(); - }).then(() => { - fail('Class permissions should have rejected this query.'); - done(); - }, (e) => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); - done(); - }); + ); }); - it('required auth test create/get/update/delete authenticated', (done) => { - config.database.loadSchema().then((schema) => { - // Just to create a valid class - return schema.validateObject('Stuff', {foo: 'bar'}); - }).then((schema) => { - return schema.setPermissions('Stuff', { - 'create': { - 'requiresAuthentication': true - }, - 'get': { - 'requiresAuthentication': true - }, - 'delete': { - 'requiresAuthentication': true + it('required auth test create/get/update/delete authenticated', done => { + config.database + .loadSchema() + .then(schema => { + // Just to create a valid class + return schema.validateObject('Stuff', { foo: 'bar' }); + }) + .then(schema => { + return schema.setPermissions('Stuff', { + create: { + requiresAuthentication: true, + }, + get: { + requiresAuthentication: true, + }, + delete: { + requiresAuthentication: true, + }, + update: { + requiresAuthentication: true, + }, + }); + }) + .then(() => { + return createUser(); + }) + .then(() => { + const stuff = new Parse.Object('Stuff'); + stuff.set('foo', 'bar'); + return stuff.save().then(() => { + const query = new Parse.Query('Stuff'); + return query.get(stuff.id); + }); + }) + .then(gotStuff => { + return gotStuff.save({ foo: 'baz' }).then(() => { + return gotStuff.destroy(); + }); + }) + .then( + () => { + done(); }, - 'update': { - 'requiresAuthentication': true + e => { + console.error(e); + fail('Should not have failed'); + done(); } - }); - }).then(() => { - return createUser(); - }).then(() => { - const stuff = new Parse.Object('Stuff'); - stuff.set('foo', 'bar'); - return stuff.save().then(() => { - const query = new Parse.Query('Stuff'); - return query.get(stuff.id); - }); - }).then((gotStuff) => { - return gotStuff.save({'foo': 'baz'}).then(() => { - return gotStuff.destroy(); - }) - }).then(() => { - done(); - }, (e) => { - console.error(e); - fail("Should not have failed"); - done(); - }); + ); }); - it('required auth test create/get/update/delete not authenitcated', (done) => { - config.database.loadSchema().then((schema) => { - // Just to create a valid class - return schema.validateObject('Stuff', {foo: 'bar'}); - }).then((schema) => { - return schema.setPermissions('Stuff', { - 'get': { - 'requiresAuthentication': true - }, - 'delete': { - 'requiresAuthentication': true - }, - 'update': { - 'requiresAuthentication': true + it('required auth test create/get/update/delete not authenitcated', done => { + config.database + .loadSchema() + .then(schema => { + // Just to create a valid class + return schema.validateObject('Stuff', { foo: 'bar' }); + }) + .then(schema => { + return schema.setPermissions('Stuff', { + get: { + requiresAuthentication: true, + }, + delete: { + requiresAuthentication: true, + }, + update: { + requiresAuthentication: true, + }, + create: { + '*': true, + }, + }); + }) + .then(() => { + const stuff = new Parse.Object('Stuff'); + stuff.set('foo', 'bar'); + return stuff.save().then(() => { + const query = new Parse.Query('Stuff'); + return query.get(stuff.id); + }); + }) + .then( + () => { + fail('Should not succeed!'); + done(); }, - 'create': { - '*': true + e => { + expect(e.message).toEqual( + 'Permission denied, user needs to be authenticated.' + ); + done(); } - }); - }).then(() => { - const stuff = new Parse.Object('Stuff'); - stuff.set('foo', 'bar'); - return stuff.save().then(() => { - const query = new Parse.Query('Stuff'); - return query.get(stuff.id); - }); - }).then(() => { - fail("Should not succeed!"); - done(); - }, (e) => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); - done(); - }); + ); }); - it('required auth test create/get/update/delete not authenitcated', (done) => { - config.database.loadSchema().then((schema) => { - // Just to create a valid class - return schema.validateObject('Stuff', {foo: 'bar'}); - }).then((schema) => { - return schema.setPermissions('Stuff', { - 'find': { - 'requiresAuthentication': true - }, - 'delete': { - 'requiresAuthentication': true - }, - 'update': { - 'requiresAuthentication': true - }, - 'create': { - '*': true - }, - 'get': { - '*': true - } - }); - }).then(() => { - const stuff = new Parse.Object('Stuff'); - stuff.set('foo', 'bar'); - return stuff.save().then(() => { + it('required auth test create/get/update/delete not authenitcated', done => { + config.database + .loadSchema() + .then(schema => { + // Just to create a valid class + return schema.validateObject('Stuff', { foo: 'bar' }); + }) + .then(schema => { + return schema.setPermissions('Stuff', { + find: { + requiresAuthentication: true, + }, + delete: { + requiresAuthentication: true, + }, + update: { + requiresAuthentication: true, + }, + create: { + '*': true, + }, + get: { + '*': true, + }, + }); + }) + .then(() => { + const stuff = new Parse.Object('Stuff'); + stuff.set('foo', 'bar'); + return stuff.save().then(() => { + const query = new Parse.Query('Stuff'); + return query.get(stuff.id); + }); + }) + .then(result => { + expect(result.get('foo')).toEqual('bar'); const query = new Parse.Query('Stuff'); - return query.get(stuff.id); + return query.find(); }) - }).then((result) => { - expect(result.get('foo')).toEqual('bar'); - const query = new Parse.Query('Stuff'); - return query.find(); - }).then(() => { - fail("Should not succeed!"); - done(); - }, (e) => { - expect(e.message).toEqual('Permission denied, user needs to be authenticated.'); - done(); - }); + .then( + () => { + fail('Should not succeed!'); + done(); + }, + e => { + expect(e.message).toEqual( + 'Permission denied, user needs to be authenticated.' + ); + done(); + } + ); }); - it('required auth test create/get/update/delete with roles (#3753)', (done) => { + it('required auth test create/get/update/delete with roles (#3753)', done => { let user; - config.database.loadSchema().then((schema) => { - // Just to create a valid class - return schema.validateObject('Stuff', {foo: 'bar'}); - }).then((schema) => { - return schema.setPermissions('Stuff', { - 'find': { - 'requiresAuthentication': true, - 'role:admin': true + config.database + .loadSchema() + .then(schema => { + // Just to create a valid class + return schema.validateObject('Stuff', { foo: 'bar' }); + }) + .then(schema => { + return schema.setPermissions('Stuff', { + find: { + requiresAuthentication: true, + 'role:admin': true, + }, + create: { 'role:admin': true }, + update: { 'role:admin': true }, + delete: { 'role:admin': true }, + get: { + requiresAuthentication: true, + 'role:admin': true, + }, + }); + }) + .then(() => { + const stuff = new Parse.Object('Stuff'); + stuff.set('foo', 'bar'); + return stuff + .save(null, { useMasterKey: true }) + .then(() => { + const query = new Parse.Query('Stuff'); + return query + .get(stuff.id) + .then( + () => { + done.fail('should not succeed'); + }, + () => { + return new Parse.Query('Stuff').find(); + } + ) + .then( + () => { + done.fail('should not succeed'); + }, + () => { + return Promise.resolve(); + } + ); + }) + .then(() => { + return Parse.User.signUp('user', 'password').then(signedUpUser => { + user = signedUpUser; + const query = new Parse.Query('Stuff'); + return query.get(stuff.id, { + sessionToken: user.getSessionToken(), + }); + }); + }); + }) + .then(result => { + expect(result.get('foo')).toEqual('bar'); + const query = new Parse.Query('Stuff'); + return query.find({ sessionToken: user.getSessionToken() }); + }) + .then( + results => { + expect(results.length).toBe(1); + done(); }, - 'create': { 'role:admin': true }, - 'update': { 'role:admin': true }, - 'delete': { 'role:admin': true }, - 'get': { - 'requiresAuthentication': true, - 'role:admin': true + e => { + console.error(e); + done.fail(e); } - }); - }).then(() => { - const stuff = new Parse.Object('Stuff'); - stuff.set('foo', 'bar'); - return stuff.save(null, {useMasterKey: true}).then(() => { - const query = new Parse.Query('Stuff'); - return query.get(stuff.id).then(() => { - done.fail('should not succeed'); - }, () => { - return new Parse.Query('Stuff').find(); - }).then(() => { - done.fail('should not succeed'); - }, () => { - return Promise.resolve(); - }); - }).then(() => { - return Parse.User.signUp('user', 'password').then((signedUpUser) => { - user = signedUpUser; - const query = new Parse.Query('Stuff'); - return query.get(stuff.id, {sessionToken: user.getSessionToken()}); - }); - }); - }).then((result) => { - expect(result.get('foo')).toEqual('bar'); - const query = new Parse.Query('Stuff'); - return query.find({sessionToken: user.getSessionToken()}); - }).then((results) => { - expect(results.length).toBe(1); - done(); - }, (e) => { - console.error(e); - done.fail(e); - }); + ); }); -}) +}); diff --git a/spec/SchemaCache.spec.js b/spec/SchemaCache.spec.js index 7d4e54b09c..a0ed509f2f 100644 --- a/spec/SchemaCache.spec.js +++ b/spec/SchemaCache.spec.js @@ -1,5 +1,7 @@ -const CacheController = require('../lib/Controllers/CacheController.js').default; -const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter').default; +const CacheController = require('../lib/Controllers/CacheController.js') + .default; +const InMemoryCacheAdapter = require('../lib/Adapters/Cache/InMemoryCacheAdapter') + .default; const SchemaCache = require('../lib/Controllers/SchemaCache').default; describe('SchemaCache', () => { @@ -10,56 +12,65 @@ describe('SchemaCache', () => { cacheController = new CacheController(cacheAdapter, 'appId'); }); - it('can retrieve a single schema after all schemas stored', (done) => { + it('can retrieve a single schema after all schemas stored', done => { const schemaCache = new SchemaCache(cacheController); - const allSchemas = [{ - className: 'Class1' - }, { - className: 'Class2' - }]; - schemaCache.setAllClasses(allSchemas).then(() => { - return schemaCache.getOneSchema('Class2'); - }).then((schema) => { - expect(schema).not.toBeNull(); - done(); - }); + const allSchemas = [ + { + className: 'Class1', + }, + { + className: 'Class2', + }, + ]; + schemaCache + .setAllClasses(allSchemas) + .then(() => { + return schemaCache.getOneSchema('Class2'); + }) + .then(schema => { + expect(schema).not.toBeNull(); + done(); + }); }); - it('does not return all schemas after a single schema is stored', (done) => { + it('does not return all schemas after a single schema is stored', done => { const schemaCache = new SchemaCache(cacheController); const schema = { - className: 'Class1' + className: 'Class1', }; - schemaCache.setOneSchema(schema.className, schema).then(() => { - return schemaCache.getAllClasses(); - }).then((allSchemas) => { - expect(allSchemas).toBeNull(); - done(); - }); + schemaCache + .setOneSchema(schema.className, schema) + .then(() => { + return schemaCache.getAllClasses(); + }) + .then(allSchemas => { + expect(allSchemas).toBeNull(); + done(); + }); }); - it('doesn\'t persist cached data by default', (done) => { + it("doesn't persist cached data by default", done => { const schemaCache = new SchemaCache(cacheController); const schema = { - className: 'Class1' + className: 'Class1', }; schemaCache.setOneSchema(schema.className, schema).then(() => { const anotherSchemaCache = new SchemaCache(cacheController); - return anotherSchemaCache.getOneSchema(schema.className).then((schema) => { + return anotherSchemaCache.getOneSchema(schema.className).then(schema => { expect(schema).toBeNull(); done(); }); }); }); - it('can persist cached data', (done) => { + it('can persist cached data', done => { const schemaCache = new SchemaCache(cacheController, 5000, true); const schema = { - className: 'Class1' + className: 'Class1', }; schemaCache.setOneSchema(schema.className, schema).then(() => { const anotherSchemaCache = new SchemaCache(cacheController, 5000, true); - return anotherSchemaCache.getOneSchema(schema.className).then((schema) => { + return anotherSchemaCache.getOneSchema(schema.className).then(schema => { expect(schema).not.toBeNull(); done(); }); diff --git a/spec/SessionTokenCache.spec.js b/spec/SessionTokenCache.spec.js index 584a2c094d..990903d3ff 100644 --- a/spec/SessionTokenCache.spec.js +++ b/spec/SessionTokenCache.spec.js @@ -1,16 +1,20 @@ -const SessionTokenCache = require('../lib/LiveQuery/SessionTokenCache').SessionTokenCache; +const SessionTokenCache = require('../lib/LiveQuery/SessionTokenCache') + .SessionTokenCache; describe('SessionTokenCache', function() { - beforeEach(function(done) { const Parse = require('parse/node'); - spyOn(Parse, "Query").and.returnValue({ - first: jasmine.createSpy("first").and.returnValue(Promise.resolve(new Parse.Object("_Session", { - user: new Parse.User({id:"userId"}) - }))), - equalTo: function(){} - }) + spyOn(Parse, 'Query').and.returnValue({ + first: jasmine.createSpy('first').and.returnValue( + Promise.resolve( + new Parse.Object('_Session', { + user: new Parse.User({ id: 'userId' }), + }) + ) + ), + equalTo: function() {}, + }); done(); }); @@ -18,20 +22,22 @@ describe('SessionTokenCache', function() { it('can get undefined userId', function(done) { const sessionTokenCache = new SessionTokenCache(); - sessionTokenCache.getUserId(undefined).then(() => { - }, (error) => { - expect(error).not.toBeNull(); - done(); - }); + sessionTokenCache.getUserId(undefined).then( + () => {}, + error => { + expect(error).not.toBeNull(); + done(); + } + ); }); it('can get existing userId', function(done) { const sessionTokenCache = new SessionTokenCache(); const sessionToken = 'sessionToken'; - const userId = 'userId' + const userId = 'userId'; sessionTokenCache.cache.set(sessionToken, userId); - sessionTokenCache.getUserId(sessionToken).then((userIdFromCache) => { + sessionTokenCache.getUserId(sessionToken).then(userIdFromCache => { expect(userIdFromCache).toBe(userId); done(); }); @@ -40,11 +46,10 @@ describe('SessionTokenCache', function() { it('can get new userId', function(done) { const sessionTokenCache = new SessionTokenCache(); - sessionTokenCache.getUserId('sessionToken').then((userIdFromCache) => { + sessionTokenCache.getUserId('sessionToken').then(userIdFromCache => { expect(userIdFromCache).toBe('userId'); expect(sessionTokenCache.cache.length).toBe(1); done(); }); }); - }); diff --git a/spec/Subscription.spec.js b/spec/Subscription.spec.js index 0d06ef304f..368b493fa8 100644 --- a/spec/Subscription.spec.js +++ b/spec/Subscription.spec.js @@ -1,36 +1,51 @@ const Subscription = require('../lib/LiveQuery/Subscription').Subscription; let logger; describe('Subscription', function() { - beforeEach(function() { logger = require('../lib/logger').logger; spyOn(logger, 'error').and.callThrough(); }); it('can be initialized', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); expect(subscription.className).toBe('className'); - expect(subscription.query).toEqual({ key : 'value' }); + expect(subscription.query).toEqual({ key: 'value' }); expect(subscription.hash).toBe('hash'); expect(subscription.clientRequestIds.size).toBe(0); }); it('can check it has subscribing clients', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); expect(subscription.hasSubscribingClient()).toBe(false); }); it('can check it does not have subscribing clients', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); subscription.addClientSubscription(1, 1); expect(subscription.hasSubscribingClient()).toBe(true); }); it('can add one request for one client', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); subscription.addClientSubscription(1, 1); expect(subscription.clientRequestIds.size).toBe(1); @@ -38,7 +53,11 @@ describe('Subscription', function() { }); it('can add requests for one client', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); subscription.addClientSubscription(1, 1); subscription.addClientSubscription(1, 2); @@ -47,7 +66,11 @@ describe('Subscription', function() { }); it('can add requests for clients', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); subscription.addClientSubscription(1, 1); subscription.addClientSubscription(1, 2); subscription.addClientSubscription(2, 2); @@ -59,14 +82,22 @@ describe('Subscription', function() { }); it('can delete requests for nonexistent client', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); subscription.deleteClientSubscription(1, 1); expect(logger.error).toHaveBeenCalled(); }); it('can delete nonexistent request for one client', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); subscription.addClientSubscription(1, 1); subscription.deleteClientSubscription(1, 2); @@ -76,7 +107,11 @@ describe('Subscription', function() { }); it('can delete some requests for one client', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); subscription.addClientSubscription(1, 1); subscription.addClientSubscription(1, 2); subscription.deleteClientSubscription(1, 2); @@ -87,7 +122,11 @@ describe('Subscription', function() { }); it('can delete all requests for one client', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); subscription.addClientSubscription(1, 1); subscription.addClientSubscription(1, 2); subscription.deleteClientSubscription(1, 1); @@ -98,7 +137,11 @@ describe('Subscription', function() { }); it('can delete requests for multiple clients', function() { - const subscription = new Subscription('className', { key : 'value' }, 'hash'); + const subscription = new Subscription( + 'className', + { key: 'value' }, + 'hash' + ); subscription.addClientSubscription(1, 1); subscription.addClientSubscription(1, 2); subscription.addClientSubscription(2, 1); diff --git a/spec/TwitterAuth.spec.js b/spec/TwitterAuth.spec.js index 4845bde7ad..ce55542a2b 100644 --- a/spec/TwitterAuth.spec.js +++ b/spec/TwitterAuth.spec.js @@ -3,59 +3,92 @@ const twitter = require('../lib/Adapters/Auth/twitter'); describe('Twitter Auth', () => { it('should use the proper configuration', () => { // Multiple options, consumer_key found - expect(twitter.handleMultipleConfigurations({ - consumer_key: 'hello', - }, [{ - consumer_key: 'hello' - }, { - consumer_key: 'world' - }]).consumer_key).toEqual('hello'); + expect( + twitter.handleMultipleConfigurations( + { + consumer_key: 'hello', + }, + [ + { + consumer_key: 'hello', + }, + { + consumer_key: 'world', + }, + ] + ).consumer_key + ).toEqual('hello'); // Multiple options, consumer_key not found - expect(function(){ - twitter.handleMultipleConfigurations({ - consumer_key: 'some', - }, [{ - consumer_key: 'hello' - }, { - consumer_key: 'world' - }]); + expect(function() { + twitter.handleMultipleConfigurations( + { + consumer_key: 'some', + }, + [ + { + consumer_key: 'hello', + }, + { + consumer_key: 'world', + }, + ] + ); }).toThrow(); // Multiple options, consumer_key not found - expect(function(){ - twitter.handleMultipleConfigurations({ - auth_token: 'token', - }, [{ - consumer_key: 'hello' - }, { - consumer_key: 'world' - }]); + expect(function() { + twitter.handleMultipleConfigurations( + { + auth_token: 'token', + }, + [ + { + consumer_key: 'hello', + }, + { + consumer_key: 'world', + }, + ] + ); }).toThrow(); // Single configuration and consumer_key set - expect(twitter.handleMultipleConfigurations({ - consumer_key: 'hello', - }, { - consumer_key: 'hello' - }).consumer_key).toEqual('hello'); + expect( + twitter.handleMultipleConfigurations( + { + consumer_key: 'hello', + }, + { + consumer_key: 'hello', + } + ).consumer_key + ).toEqual('hello'); // General case, only 1 config, no consumer_key set - expect(twitter.handleMultipleConfigurations({ - auth_token: 'token', - }, { - consumer_key: 'hello' - }).consumer_key).toEqual('hello'); + expect( + twitter.handleMultipleConfigurations( + { + auth_token: 'token', + }, + { + consumer_key: 'hello', + } + ).consumer_key + ).toEqual('hello'); }); - it("Should fail with missing options", (done) => { + it('Should fail with missing options', done => { try { - twitter.validateAuthData({ - consumer_key: 'key', - consumer_secret: 'secret', - auth_token: 'token', - auth_token_secret: 'secret' - }, undefined); + twitter.validateAuthData( + { + consumer_key: 'key', + consumer_secret: 'secret', + auth_token: 'token', + auth_token_secret: 'secret', + }, + undefined + ); } catch (error) { jequal(error.message, 'Twitter auth configuration missing'); done(); diff --git a/spec/Uniqueness.spec.js b/spec/Uniqueness.spec.js index 25394cafcf..73dfad34cb 100644 --- a/spec/Uniqueness.spec.js +++ b/spec/Uniqueness.spec.js @@ -1,45 +1,62 @@ 'use strict'; -const Parse = require("parse/node"); +const Parse = require('parse/node'); const Config = require('../lib/Config'); describe('Uniqueness', function() { it('fail when create duplicate value in unique field', done => { const obj = new Parse.Object('UniqueField'); obj.set('unique', 'value'); - obj.save().then(() => { - expect(obj.id).not.toBeUndefined(); - const config = Config.get('test'); - return config.database.adapter.ensureUniqueness('UniqueField', { fields: { unique: { __type: 'String' } } }, ['unique']) - }) + obj + .save() + .then(() => { + expect(obj.id).not.toBeUndefined(); + const config = Config.get('test'); + return config.database.adapter.ensureUniqueness( + 'UniqueField', + { fields: { unique: { __type: 'String' } } }, + ['unique'] + ); + }) .then(() => { const 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(); - }); + return obj.save(); + }) + .then( + () => { + fail('Saving duplicate field should have failed'); + done(); + }, + error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + done(); + } + ); }); it('unique indexing works on pointer fields', done => { const obj = new Parse.Object('UniquePointer'); - obj.save({ string: 'who cares' }) + obj + .save({ string: 'who cares' }) .then(() => obj.save({ ptr: obj })) .then(() => { const config = Config.get('test'); - return config.database.adapter.ensureUniqueness('UniquePointer', { fields: { - string: { __type: 'String' }, - ptr: { __type: 'Pointer', targetClass: 'UniquePointer' } - } }, ['ptr']); + return config.database.adapter.ensureUniqueness( + 'UniquePointer', + { + fields: { + string: { __type: 'String' }, + ptr: { __type: 'Pointer', targetClass: 'UniquePointer' }, + }, + }, + ['ptr'] + ); }) .then(() => { - const newObj = new Parse.Object('UniquePointer') - newObj.set('ptr', obj) - return newObj.save() + const newObj = new Parse.Object('UniquePointer'); + newObj.set('ptr', obj); + return newObj.save(); }) .then(() => { fail('save should have failed due to duplicate value'); @@ -59,7 +76,11 @@ describe('Uniqueness', function() { Parse.Object.saveAll([o1, o2]) .then(() => { const config = Config.get('test'); - return config.database.adapter.ensureUniqueness('UniqueFail', { fields: { key: { __type: 'String' } } }, ['key']); + return config.database.adapter.ensureUniqueness( + 'UniqueFail', + { fields: { key: { __type: 'String' } } }, + ['key'] + ); }) .catch(error => { expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); @@ -69,7 +90,12 @@ describe('Uniqueness', function() { it_exclude_dbs(['postgres'])('can do compound uniqueness', done => { const config = Config.get('test'); - config.database.adapter.ensureUniqueness('CompoundUnique', { fields: { k1: { __type: 'String' }, k2: { __type: 'String' } } }, ['k1', 'k2']) + config.database.adapter + .ensureUniqueness( + 'CompoundUnique', + { fields: { k1: { __type: 'String' }, k2: { __type: 'String' } } }, + ['k1', 'k2'] + ) .then(() => { const o1 = new Parse.Object('CompoundUnique'); o1.set('k1', 'v1'); diff --git a/spec/UserController.spec.js b/spec/UserController.spec.js index 0b55346d40..1e4d5e48c3 100644 --- a/spec/UserController.spec.js +++ b/spec/UserController.spec.js @@ -1,59 +1,68 @@ -const UserController = require('../lib/Controllers/UserController').UserController; -const emailAdapter = require('./MockEmailAdapter') +const UserController = require('../lib/Controllers/UserController') + .UserController; +const emailAdapter = require('./MockEmailAdapter'); const AppCache = require('../lib/cache').AppCache; describe('UserController', () => { const user = { _email_verify_token: 'testToken', username: 'testUser', - email: 'test@example.com' - } + email: 'test@example.com', + }; describe('sendVerificationEmail', () => { describe('parseFrameURL not provided', () => { - it('uses publicServerURL', (done) => { + it('uses publicServerURL', done => { + AppCache.put( + defaultConfiguration.appId, + Object.assign({}, defaultConfiguration, { + publicServerURL: 'http://www.example.com', + customPages: { + parseFrameURL: undefined, + }, + }) + ); - AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, { - publicServerURL: 'http://www.example.com', - customPages: { - parseFrameURL: undefined - } - })) - - emailAdapter.sendVerificationEmail = (options) => { - expect(options.link).toEqual('http://www.example.com/apps/test/verify_email?token=testToken&username=testUser') - done() - } + emailAdapter.sendVerificationEmail = options => { + expect(options.link).toEqual( + 'http://www.example.com/apps/test/verify_email?token=testToken&username=testUser' + ); + done(); + }; const userController = new UserController(emailAdapter, 'test', { - verifyUserEmails: true - }) + verifyUserEmails: true, + }); - userController.sendVerificationEmail(user) - }) - }) + userController.sendVerificationEmail(user); + }); + }); describe('parseFrameURL provided', () => { - it('uses parseFrameURL and includes the destination in the link parameter', (done) => { - - AppCache.put(defaultConfiguration.appId, Object.assign({}, defaultConfiguration, { - publicServerURL: 'http://www.example.com', - customPages: { - parseFrameURL: 'http://someother.example.com/handle-parse-iframe' - } - })) + it('uses parseFrameURL and includes the destination in the link parameter', done => { + AppCache.put( + defaultConfiguration.appId, + Object.assign({}, defaultConfiguration, { + publicServerURL: 'http://www.example.com', + customPages: { + parseFrameURL: 'http://someother.example.com/handle-parse-iframe', + }, + }) + ); - emailAdapter.sendVerificationEmail = (options) => { - expect(options.link).toEqual('http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser') - done() - } + emailAdapter.sendVerificationEmail = options => { + expect(options.link).toEqual( + 'http://someother.example.com/handle-parse-iframe?link=%2Fapps%2Ftest%2Fverify_email&token=testToken&username=testUser' + ); + done(); + }; const userController = new UserController(emailAdapter, 'test', { - verifyUserEmails: true - }) + verifyUserEmails: true, + }); - userController.sendVerificationEmail(user) - }) - }) - }) + userController.sendVerificationEmail(user); + }); + }); + }); }); diff --git a/spec/UserPII.spec.js b/spec/UserPII.spec.js index 64822cd404..6aca847291 100644 --- a/spec/UserPII.spec.js +++ b/spec/UserPII.spec.js @@ -14,84 +14,91 @@ describe('Personally Identifiable Information', () => { beforeEach(done => { return Parse.User.signUp('tester', 'abc') - .then(loggedInUser => user = loggedInUser) + .then(loggedInUser => (user = loggedInUser)) .then(() => Parse.User.logIn(user.get('username'), 'abc')) - .then(() => user - .set('email', EMAIL) - .set('zip', ZIP) - .set('ssn', SSN) - .save()) + .then(() => + user + .set('email', EMAIL) + .set('zip', ZIP) + .set('ssn', SSN) + .save() + ) .then(() => done()); }); - it('should be able to get own PII via API with object', (done) => { - const userObj = new (Parse.Object.extend(Parse.User)); + it('should be able to get own PII via API with object', done => { + const userObj = new (Parse.Object.extend(Parse.User))(); userObj.id = user.id; - userObj.fetch().then( - fetchedUser => { - expect(fetchedUser.get('email')).toBe(EMAIL); - }, e => console.error('error', e)) - .then(done).catch(done.fail); + userObj + .fetch() + .then( + fetchedUser => { + expect(fetchedUser.get('email')).toBe(EMAIL); + }, + e => console.error('error', e) + ) + .then(done) + .catch(done.fail); }); - it('should not be able to get PII via API with object', (done) => { - Parse.User.logOut() - .then(() => { - const userObj = new (Parse.Object.extend(Parse.User)); - userObj.id = user.id; - userObj.fetch().then( - fetchedUser => { - expect(fetchedUser.get('email')).toBe(undefined); - done(); - }) - .catch(e => { - done.fail(JSON.stringify(e)); - }) - .then(done).catch(done.fail); - }); + it('should not be able to get PII via API with object', done => { + Parse.User.logOut().then(() => { + const userObj = new (Parse.Object.extend(Parse.User))(); + userObj.id = user.id; + userObj + .fetch() + .then(fetchedUser => { + expect(fetchedUser.get('email')).toBe(undefined); + done(); + }) + .catch(e => { + done.fail(JSON.stringify(e)); + }) + .then(done) + .catch(done.fail); + }); }); - it('should be able to get PII via API with object using master key', (done) => { - Parse.User.logOut() - .then(() => { - const userObj = new (Parse.Object.extend(Parse.User)); - userObj.id = user.id; - userObj.fetch({ useMasterKey: true }).then( + it('should be able to get PII via API with object using master key', done => { + Parse.User.logOut().then(() => { + const userObj = new (Parse.Object.extend(Parse.User))(); + userObj.id = user.id; + userObj + .fetch({ useMasterKey: true }) + .then( fetchedUser => { expect(fetchedUser.get('email')).toBe(EMAIL); - }, e => console.error('error', e)) - .then(done).catch(done.fail); - }); + }, + e => console.error('error', e) + ) + .then(done) + .catch(done.fail); + }); }); + it('should be able to get own PII via API with Find', done => { + new Parse.Query(Parse.User).first().then(fetchedUser => { + expect(fetchedUser.get('email')).toBe(EMAIL); + expect(fetchedUser.get('zip')).toBe(ZIP); + expect(fetchedUser.get('ssn')).toBe(SSN); + done(); + }); + }); - it('should be able to get own PII via API with Find', (done) => { - new Parse.Query(Parse.User) - .first() - .then(fetchedUser => { - expect(fetchedUser.get('email')).toBe(EMAIL); + it('should not get PII via API with Find', done => { + Parse.User.logOut().then(() => + new Parse.Query(Parse.User).first().then(fetchedUser => { + expect(fetchedUser.get('email')).toBe(undefined); expect(fetchedUser.get('zip')).toBe(ZIP); expect(fetchedUser.get('ssn')).toBe(SSN); done(); - }); - }); - - it('should not get PII via API with Find', (done) => { - Parse.User.logOut() - .then(() => new Parse.Query(Parse.User) - .first() - .then(fetchedUser => { - expect(fetchedUser.get('email')).toBe(undefined); - expect(fetchedUser.get('zip')).toBe(ZIP); - expect(fetchedUser.get('ssn')).toBe(SSN); - done(); - }) - ); + }) + ); }); - it('should get PII via API with Find using master key', (done) => { - Parse.User.logOut() - .then(() => new Parse.Query(Parse.User) + it('should get PII via API with Find using master key', done => { + Parse.User.logOut().then(() => + new Parse.Query(Parse.User) .first({ useMasterKey: true }) .then(fetchedUser => { expect(fetchedUser.get('email')).toBe(EMAIL); @@ -99,37 +106,32 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.get('ssn')).toBe(SSN); done(); }) - ); + ); }); + it('should be able to get own PII via API with Get', done => { + new Parse.Query(Parse.User).get(user.id).then(fetchedUser => { + expect(fetchedUser.get('email')).toBe(EMAIL); + expect(fetchedUser.get('zip')).toBe(ZIP); + expect(fetchedUser.get('ssn')).toBe(SSN); + done(); + }); + }); - it('should be able to get own PII via API with Get', (done) => { - new Parse.Query(Parse.User) - .get(user.id) - .then(fetchedUser => { - expect(fetchedUser.get('email')).toBe(EMAIL); + it('should not get PII via API with Get', done => { + Parse.User.logOut().then(() => + new Parse.Query(Parse.User).get(user.id).then(fetchedUser => { + expect(fetchedUser.get('email')).toBe(undefined); expect(fetchedUser.get('zip')).toBe(ZIP); expect(fetchedUser.get('ssn')).toBe(SSN); done(); - }); - }); - - it('should not get PII via API with Get', (done) => { - Parse.User.logOut() - .then(() => new Parse.Query(Parse.User) - .get(user.id) - .then(fetchedUser => { - expect(fetchedUser.get('email')).toBe(undefined); - expect(fetchedUser.get('zip')).toBe(ZIP); - expect(fetchedUser.get('ssn')).toBe(SSN); - done(); - }) - ); + }) + ); }); - it('should get PII via API with Get using master key', (done) => { - Parse.User.logOut() - .then(() => new Parse.Query(Parse.User) + it('should get PII via API with Get using master key', done => { + Parse.User.logOut().then(() => + new Parse.Query(Parse.User) .get(user.id, { useMasterKey: true }) .then(fetchedUser => { expect(fetchedUser.get('email')).toBe(EMAIL); @@ -137,18 +139,19 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.get('ssn')).toBe(SSN); done(); }) - ); + ); }); - it('should not get PII via REST', (done) => { - request.get({ - url: 'http://localhost:8378/1/classes/_User', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test' - } - }) + it('should not get PII via REST', done => { + request + .get({ + url: 'http://localhost:8378/1/classes/_User', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }, + }) .then( result => { const fetchedUser = result.results[0]; @@ -156,19 +159,21 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(undefined); }, e => console.error('error', e.message) - ).done(() => done()); + ) + .done(() => done()); }); - it('should get PII via REST with self credentials', (done) => { - request.get({ - url: 'http://localhost:8378/1/classes/_User', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test', - 'X-Parse-Session-Token': user.getSessionToken() - } - }) + it('should get PII via REST with self credentials', done => { + request + .get({ + url: 'http://localhost:8378/1/classes/_User', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + }, + }) .then( result => { const fetchedUser = result.results[0]; @@ -176,18 +181,20 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(EMAIL); }, e => console.error('error', e.message) - ).done(() => done()); + ) + .done(() => done()); }); - it('should get PII via REST using master key', (done) => { - request.get({ - url: 'http://localhost:8378/1/classes/_User', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test' - } - }) + it('should get PII via REST using master key', done => { + request + .get({ + url: 'http://localhost:8378/1/classes/_User', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }) .then( result => { const fetchedUser = result.results[0]; @@ -195,18 +202,20 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(EMAIL); }, e => console.error('error', e.message) - ).done(() => done()); + ) + .done(() => done()); }); - it('should not get PII via REST by ID', (done) => { - request.get({ - url: `http://localhost:8378/1/classes/_User/${user.id}`, - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test' - } - }) + it('should not get PII via REST by ID', done => { + request + .get({ + url: `http://localhost:8378/1/classes/_User/${user.id}`, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }, + }) .then( result => { const fetchedUser = result; @@ -214,19 +223,21 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(undefined); }, e => console.error('error', e.message) - ).done(() => done()); + ) + .done(() => done()); }); - it('should get PII via REST by ID with self credentials', (done) => { - request.get({ - url: `http://localhost:8378/1/classes/_User/${user.id}`, - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test', - 'X-Parse-Session-Token': user.getSessionToken() - } - }) + it('should get PII via REST by ID with self credentials', done => { + request + .get({ + url: `http://localhost:8378/1/classes/_User/${user.id}`, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + }, + }) .then( result => { const fetchedUser = result; @@ -234,19 +245,21 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(EMAIL); }, e => console.error('error', e.message) - ).done(() => done()); + ) + .done(() => done()); }); - it('should get PII via REST by ID with master key', (done) => { - request.get({ - url: `http://localhost:8378/1/classes/_User/${user.id}`, - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test', - 'X-Parse-Master-Key': 'test', - } - }) + it('should get PII via REST by ID with master key', done => { + request + .get({ + url: `http://localhost:8378/1/classes/_User/${user.id}`, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + 'X-Parse-Master-Key': 'test', + }, + }) .then( result => { const fetchedUser = result; @@ -254,17 +267,19 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(EMAIL); }, e => console.error('error', e.message) - ).done(() => done()); + ) + .done(() => done()); }); describe('with configured sensitive fields', () => { - beforeEach((done) => { - reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] }) - .then(() => done()); + beforeEach(done => { + reconfigureServer({ userSensitiveFields: ['ssn', 'zip'] }).then(() => + done() + ); }); - it('should be able to get own PII via API with object', (done) => { - const userObj = new (Parse.Object.extend(Parse.User)); + it('should be able to get own PII via API with object', done => { + const userObj = new (Parse.Object.extend(Parse.User))(); userObj.id = user.id; userObj.fetch().then( fetchedUser => { @@ -272,67 +287,72 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.get('zip')).toBe(ZIP); expect(fetchedUser.get('ssn')).toBe(SSN); done(); - }, e => done.fail(e)); + }, + e => done.fail(e) + ); }); - it('should not be able to get PII via API with object', (done) => { - Parse.User.logOut() - .then(() => { - const userObj = new (Parse.Object.extend(Parse.User)); - userObj.id = user.id; - userObj.fetch().then( + it('should not be able to get PII via API with object', done => { + Parse.User.logOut().then(() => { + const userObj = new (Parse.Object.extend(Parse.User))(); + userObj.id = user.id; + userObj + .fetch() + .then( fetchedUser => { expect(fetchedUser.get('email')).toBe(undefined); expect(fetchedUser.get('zip')).toBe(undefined); expect(fetchedUser.get('ssn')).toBe(undefined); - }, e => console.error('error', e)) - .then(done).catch(done.fail); - }); + }, + e => console.error('error', e) + ) + .then(done) + .catch(done.fail); + }); }); - it('should be able to get PII via API with object using master key', (done) => { - Parse.User.logOut() - .then(() => { - const userObj = new (Parse.Object.extend(Parse.User)); - userObj.id = user.id; - userObj.fetch({ useMasterKey: true }).then( + it('should be able to get PII via API with object using master key', done => { + Parse.User.logOut().then(() => { + const userObj = new (Parse.Object.extend(Parse.User))(); + userObj.id = user.id; + userObj + .fetch({ useMasterKey: true }) + .then( fetchedUser => { expect(fetchedUser.get('email')).toBe(EMAIL); expect(fetchedUser.get('zip')).toBe(ZIP); expect(fetchedUser.get('ssn')).toBe(SSN); - }, e => console.error('error', e)) - .then(done).catch(done.fail); - }); + }, + e => console.error('error', e) + ) + .then(done) + .catch(done.fail); + }); }); - - it('should be able to get own PII via API with Find', (done) => { - new Parse.Query(Parse.User) - .first() - .then(fetchedUser => { - expect(fetchedUser.get('email')).toBe(EMAIL); - expect(fetchedUser.get('zip')).toBe(ZIP); - expect(fetchedUser.get('ssn')).toBe(SSN); - done(); - }); + it('should be able to get own PII via API with Find', done => { + new Parse.Query(Parse.User).first().then(fetchedUser => { + expect(fetchedUser.get('email')).toBe(EMAIL); + expect(fetchedUser.get('zip')).toBe(ZIP); + expect(fetchedUser.get('ssn')).toBe(SSN); + done(); + }); }); - it('should not get PII via API with Find', (done) => { - Parse.User.logOut() - .then(() => new Parse.Query(Parse.User) - .first() - .then(fetchedUser => { - expect(fetchedUser.get('email')).toBe(undefined); - expect(fetchedUser.get('zip')).toBe(undefined); - expect(fetchedUser.get('ssn')).toBe(undefined); - done(); - }) - ); + it('should not get PII via API with Find', done => { + Parse.User.logOut().then(() => + new Parse.Query(Parse.User).first().then(fetchedUser => { + expect(fetchedUser.get('email')).toBe(undefined); + expect(fetchedUser.get('zip')).toBe(undefined); + expect(fetchedUser.get('ssn')).toBe(undefined); + done(); + }) + ); }); - it('should get PII via API with Find using master key', (done) => { - Parse.User.logOut() - .then(() => new Parse.Query(Parse.User) + it('should get PII via API with Find using master key', done => { + Parse.User.logOut().then(() => + new Parse.Query(Parse.User) .first({ useMasterKey: true }) .then(fetchedUser => { expect(fetchedUser.get('email')).toBe(EMAIL); @@ -340,37 +360,32 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.get('ssn')).toBe(SSN); done(); }) - ); + ); }); - - it('should be able to get own PII via API with Get', (done) => { - new Parse.Query(Parse.User) - .get(user.id) - .then(fetchedUser => { - expect(fetchedUser.get('email')).toBe(EMAIL); - expect(fetchedUser.get('zip')).toBe(ZIP); - expect(fetchedUser.get('ssn')).toBe(SSN); - done(); - }); + it('should be able to get own PII via API with Get', done => { + new Parse.Query(Parse.User).get(user.id).then(fetchedUser => { + expect(fetchedUser.get('email')).toBe(EMAIL); + expect(fetchedUser.get('zip')).toBe(ZIP); + expect(fetchedUser.get('ssn')).toBe(SSN); + done(); + }); }); - it('should not get PII via API with Get', (done) => { - Parse.User.logOut() - .then(() => new Parse.Query(Parse.User) - .get(user.id) - .then(fetchedUser => { - expect(fetchedUser.get('email')).toBe(undefined); - expect(fetchedUser.get('zip')).toBe(undefined); - expect(fetchedUser.get('ssn')).toBe(undefined); - done(); - }) - ); + it('should not get PII via API with Get', done => { + Parse.User.logOut().then(() => + new Parse.Query(Parse.User).get(user.id).then(fetchedUser => { + expect(fetchedUser.get('email')).toBe(undefined); + expect(fetchedUser.get('zip')).toBe(undefined); + expect(fetchedUser.get('ssn')).toBe(undefined); + done(); + }) + ); }); - it('should get PII via API with Get using master key', (done) => { - Parse.User.logOut() - .then(() => new Parse.Query(Parse.User) + it('should get PII via API with Get using master key', done => { + Parse.User.logOut().then(() => + new Parse.Query(Parse.User) .get(user.id, { useMasterKey: true }) .then(fetchedUser => { expect(fetchedUser.get('email')).toBe(EMAIL); @@ -378,18 +393,19 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.get('ssn')).toBe(SSN); done(); }) - ); + ); }); - it('should not get PII via REST', (done) => { - request.get({ - url: 'http://localhost:8378/1/classes/_User', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test' - } - }) + it('should not get PII via REST', done => { + request + .get({ + url: 'http://localhost:8378/1/classes/_User', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }, + }) .then( result => { const fetchedUser = result.results[0]; @@ -398,19 +414,22 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(undefined); }, e => console.error('error', e.message) - ).then(done).catch(done.fail); + ) + .then(done) + .catch(done.fail); }); - it('should get PII via REST with self credentials', (done) => { - request.get({ - url: 'http://localhost:8378/1/classes/_User', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test', - 'X-Parse-Session-Token': user.getSessionToken() - } - }) + it('should get PII via REST with self credentials', done => { + request + .get({ + url: 'http://localhost:8378/1/classes/_User', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + }, + }) .then( result => { const fetchedUser = result.results[0]; @@ -419,18 +438,21 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.ssn).toBe(SSN); }, e => console.error('error', e.message) - ).then(done).catch(done.fail); + ) + .then(done) + .catch(done.fail); }); - it('should get PII via REST using master key', (done) => { - request.get({ - url: 'http://localhost:8378/1/classes/_User', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test' - } - }) + it('should get PII via REST using master key', done => { + request + .get({ + url: 'http://localhost:8378/1/classes/_User', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + }) .then( result => { const fetchedUser = result.results[0]; @@ -439,18 +461,21 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.ssn).toBe(SSN); }, e => console.error('error', e.message) - ).then(done).catch(done.fail); + ) + .then(done) + .catch(done.fail); }); - it('should not get PII via REST by ID', (done) => { - request.get({ - url: `http://localhost:8378/1/classes/_User/${user.id}`, - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test' - } - }) + it('should not get PII via REST by ID', done => { + request + .get({ + url: `http://localhost:8378/1/classes/_User/${user.id}`, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + }, + }) .then( result => { const fetchedUser = result; @@ -458,19 +483,22 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(undefined); }, e => console.error('error', e.message) - ).then(done).catch(done.fail); + ) + .then(done) + .catch(done.fail); }); - it('should get PII via REST by ID with self credentials', (done) => { - request.get({ - url: `http://localhost:8378/1/classes/_User/${user.id}`, - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test', - 'X-Parse-Session-Token': user.getSessionToken() - } - }) + it('should get PII via REST by ID with self credentials', done => { + request + .get({ + url: `http://localhost:8378/1/classes/_User/${user.id}`, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + 'X-Parse-Session-Token': user.getSessionToken(), + }, + }) .then( result => { const fetchedUser = result; @@ -478,19 +506,22 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(EMAIL); }, e => console.error('error', e.message) - ).then(done).catch(done.fail); + ) + .then(done) + .catch(done.fail); }); - it('should get PII via REST by ID with master key', (done) => { - request.get({ - url: `http://localhost:8378/1/classes/_User/${user.id}`, - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Javascript-Key': 'test', - 'X-Parse-Master-Key': 'test', - } - }) + it('should get PII via REST by ID with master key', done => { + request + .get({ + url: `http://localhost:8378/1/classes/_User/${user.id}`, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Javascript-Key': 'test', + 'X-Parse-Master-Key': 'test', + }, + }) .then( result => { const fetchedUser = result; @@ -498,7 +529,9 @@ describe('Personally Identifiable Information', () => { expect(fetchedUser.email).toBe(EMAIL); }, e => console.error('error', e.message) - ).then(done).catch(done.fail); + ) + .then(done) + .catch(done.fail); }); }); }); diff --git a/spec/ValidationAndPasswordsReset.spec.js b/spec/ValidationAndPasswordsReset.spec.js index bd82122f89..cafe60752a 100644 --- a/spec/ValidationAndPasswordsReset.spec.js +++ b/spec/ValidationAndPasswordsReset.spec.js @@ -1,118 +1,122 @@ -"use strict"; +'use strict'; const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); const request = require('request'); -const Config = require("../lib/Config"); +const Config = require('../lib/Config'); -describe("Custom Pages, Email Verification, Password Reset", () => { - it("should set the custom pages", (done) => { +describe('Custom Pages, Email Verification, Password Reset', () => { + it('should set the custom pages', done => { reconfigureServer({ appName: 'unused', customPages: { - invalidLink: "myInvalidLink", - verifyEmailSuccess: "myVerifyEmailSuccess", - choosePassword: "myChoosePassword", - passwordResetSuccess: "myPasswordResetSuccess", - parseFrameURL: "http://example.com/handle-parse-iframe" + invalidLink: 'myInvalidLink', + verifyEmailSuccess: 'myVerifyEmailSuccess', + choosePassword: 'myChoosePassword', + passwordResetSuccess: 'myPasswordResetSuccess', + parseFrameURL: 'http://example.com/handle-parse-iframe', }, - publicServerURL: "https://my.public.server.com/1" - }) - .then(() => { - const config = Config.get("test"); - expect(config.invalidLinkURL).toEqual("myInvalidLink"); - expect(config.verifyEmailSuccessURL).toEqual("myVerifyEmailSuccess"); - expect(config.choosePasswordURL).toEqual("myChoosePassword"); - expect(config.passwordResetSuccessURL).toEqual("myPasswordResetSuccess"); - expect(config.parseFrameURL).toEqual("http://example.com/handle-parse-iframe"); - expect(config.verifyEmailURL).toEqual("https://my.public.server.com/1/apps/test/verify_email"); - expect(config.requestResetPasswordURL).toEqual("https://my.public.server.com/1/apps/test/request_password_reset"); - done(); - }); + publicServerURL: 'https://my.public.server.com/1', + }).then(() => { + const config = Config.get('test'); + expect(config.invalidLinkURL).toEqual('myInvalidLink'); + expect(config.verifyEmailSuccessURL).toEqual('myVerifyEmailSuccess'); + expect(config.choosePasswordURL).toEqual('myChoosePassword'); + expect(config.passwordResetSuccessURL).toEqual('myPasswordResetSuccess'); + expect(config.parseFrameURL).toEqual( + 'http://example.com/handle-parse-iframe' + ); + expect(config.verifyEmailURL).toEqual( + 'https://my.public.server.com/1/apps/test/verify_email' + ); + expect(config.requestResetPasswordURL).toEqual( + 'https://my.public.server.com/1/apps/test/request_password_reset' + ); + done(); + }); }); it('sends verification email if email verification is enabled', done => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }) - .then(async () => { - spyOn(emailAdapter, 'sendVerificationEmail'); - const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.setEmail('testIfEnabled@parse.com'); - await user.signUp(); - expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); - user.fetch() - .then(() => { - expect(user.get('emailVerified')).toEqual(false); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }).then(async () => { + spyOn(emailAdapter, 'sendVerificationEmail'); + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.setEmail('testIfEnabled@parse.com'); + await user.signUp(); + expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); + user.fetch().then(() => { + expect(user.get('emailVerified')).toEqual(false); + done(); }); + }); }); it('does not send verification email when verification is enabled and email is not set', done => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }) - .then(async () => { - spyOn(emailAdapter, 'sendVerificationEmail'); - const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - await user.signUp(); - expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); - user.fetch() - .then(() => { - expect(user.get('emailVerified')).toEqual(undefined); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }).then(async () => { + spyOn(emailAdapter, 'sendVerificationEmail'); + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + await user.signUp(); + expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); + user.fetch().then(() => { + expect(user.get('emailVerified')).toEqual(undefined); + done(); }); + }); }); it('does send a validation email when updating the email', done => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }).then(async () => { spyOn(emailAdapter, 'sendVerificationEmail'); const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); + user.setPassword('asdf'); + user.setUsername('zxcv'); await user.signUp(); expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); - user.fetch() - .then((user) => { - user.set("email", "testWhenUpdating@parse.com"); + user + .fetch() + .then(user => { + user.set('email', 'testWhenUpdating@parse.com'); return user.save(); - }).then((user) => { + }) + .then(user => { return user.fetch(); - }).then(() => { + }) + .then(() => { expect(user.get('emailVerified')).toEqual(false); // Wait as on update email, we need to fetch the username - setTimeout(function(){ + setTimeout(function() { expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); done(); }, 200); @@ -124,31 +128,31 @@ describe("Custom Pages, Email Verification, Password Reset", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; await reconfigureServer({ appName: 'unused', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }) - spyOn(emailAdapter, 'sendVerificationEmail').and.callFake((options) => { + 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(); }); const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); + user.setPassword('asdf'); + user.setUsername('zxcv'); await user.signUp(); expect(emailAdapter.sendVerificationEmail).not.toHaveBeenCalled(); - await user.fetch() - user.set("email", "testValidLinkWhenUpdating@parse.com"); + await user.fetch(); + user.set('email', 'testValidLinkWhenUpdating@parse.com'); await user.save(); await user.fetch(); expect(user.get('emailVerified')).toEqual(false); // Wait as on update email, we need to fetch the username - setTimeout(function(){ + setTimeout(function() { expect(emailAdapter.sendVerificationEmail).toHaveBeenCalled(); done(); }, 200); @@ -157,10 +161,12 @@ describe("Custom Pages, Email Verification, Password Reset", () => { it('does send with a simple adapter', done => { let calls = 0; const emailAdapter = { - sendMail: function(options){ + sendMail: function(options) { 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.subject).toEqual( + 'Please verify your e-mail for My Cool App' + ); expect(options.text.match(/verify_email/)).not.toBe(null); } else if (calls == 1) { expect(options.subject).toEqual('Password Reset for My Cool App'); @@ -168,34 +174,38 @@ describe("Custom Pages, Email Verification, Password Reset", () => { } calls++; return Promise.resolve(); - } - } + }, + }; reconfigureServer({ appName: 'My Cool App', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }) - .then(async () => { - const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set("email", "testSendSimpleAdapter@parse.com"); - await user.signUp(); - expect(calls).toBe(1); - user.fetch() - .then((user) => { - return user.save(); - }).then(() => { - return Parse.User.requestPasswordReset("testSendSimpleAdapter@parse.com").catch(() => { - fail('Should not fail requesting a password'); - done(); - }) - }).then(() => { - expect(calls).toBe(2); + publicServerURL: 'http://localhost:8378/1', + }).then(async () => { + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'testSendSimpleAdapter@parse.com'); + await user.signUp(); + expect(calls).toBe(1); + user + .fetch() + .then(user => { + return user.save(); + }) + .then(() => { + return Parse.User.requestPasswordReset( + 'testSendSimpleAdapter@parse.com' + ).catch(() => { + fail('Should not fail requesting a password'); done(); }); - }); + }) + .then(() => { + expect(calls).toBe(2); + done(); + }); + }); }); it('prevents user from login if email is not verified but preventLoginWithUnverifiedEmail is set to true', done => { @@ -212,21 +222,25 @@ describe("Custom Pages, Email Verification, Password Reset", () => { }) .then(() => { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set("email", "testInvalidConfig@parse.com"); - user.signUp(null) - .then((user) => { + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'testInvalidConfig@parse.com'); + user + .signUp(null) + .then(user => { expect(user.getSessionToken()).toBe(undefined); - return Parse.User.logIn("zxcv", "asdf"); + return Parse.User.logIn('zxcv', 'asdf'); }) - .then(() => { - fail('login should have failed'); - done(); - }, error => { - expect(error.message).toEqual('User email is not verified.') - done(); - }); + .then( + () => { + fail('login should have failed'); + done(); + }, + error => { + expect(error.message).toEqual('User email is not verified.'); + done(); + } + ); }) .catch(error => { fail(JSON.stringify(error)); @@ -242,49 +256,63 @@ describe("Custom Pages, Email Verification, Password Reset", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailing app', verifyUserEmails: true, preventLoginWithUnverifiedEmail: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setPassword("other-password"); - user.setUsername("user"); + user.setPassword('other-password'); + user.setUsername('user'); user.set('email', 'user@parse.com'); return user.signUp(); - }).then(() => { + }) + .then(() => { expect(sendEmailOptions).not.toBeUndefined(); - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user'); - user.fetch() - .then(() => { - expect(user.get('emailVerified')).toEqual(true); - - Parse.User.logIn("user", "other-password") - .then(user => { - expect(typeof user).toBe('object'); - expect(user.get('emailVerified')).toBe(true); - done(); - }, () => { - fail('login should have succeeded'); + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' + ); + user + .fetch() + .then( + () => { + expect(user.get('emailVerified')).toEqual(true); + + Parse.User.logIn('user', 'other-password').then( + user => { + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(true); + done(); + }, + () => { + fail('login should have succeeded'); + done(); + } + ); + }, + err => { + jfail(err); + fail('this should not fail'); done(); - }); - }, (err) => { - jfail(err); - fail("this should not fail"); - done(); - }).catch((err) => { - jfail(err); - done(); - }) - }); + } + ) + .catch(err => { + jfail(err); + done(); + }); + } + ); }); }); @@ -302,19 +330,23 @@ describe("Custom Pages, Email Verification, Password Reset", () => { }) .then(() => { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set("email", "testInvalidConfig@parse.com"); - user.signUp(null) - .then(() => Parse.User.logIn("zxcv", "asdf")) - .then(user => { - expect(typeof user).toBe('object'); - expect(user.get('emailVerified')).toBe(false); - done(); - }, () => { - fail('login should have succeeded'); - done(); - }); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'testInvalidConfig@parse.com'); + user + .signUp(null) + .then(() => Parse.User.logIn('zxcv', 'asdf')) + .then( + user => { + expect(typeof user).toBe('object'); + expect(user.get('emailVerified')).toBe(false); + done(); + }, + () => { + fail('login should have succeeded'); + done(); + } + ); }) .catch(error => { fail(JSON.stringify(error)); @@ -334,19 +366,27 @@ describe("Custom Pages, Email Verification, Password Reset", () => { }) .then(() => { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set("email", "testInvalidConfig@parse.com"); - user.signUp(null) - .then(() => 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 and email verification functionality.') - done(); - }); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'testInvalidConfig@parse.com'); + user + .signUp(null) + .then(() => + 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 and email verification functionality.' + ); + done(); + } + ); }) .catch(error => { fail(JSON.stringify(error)); @@ -365,19 +405,27 @@ describe("Custom Pages, Email Verification, Password Reset", () => { }) .then(() => { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set("email", "testInvalidConfig@parse.com"); - user.signUp(null) - .then(() => 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 and email verification functionality.') - done(); - }); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'testInvalidConfig@parse.com'); + user + .signUp(null) + .then(() => + 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 and email verification functionality.' + ); + done(); + } + ); }) .catch(error => { fail(JSON.stringify(error)); @@ -393,19 +441,27 @@ describe("Custom Pages, Email Verification, Password Reset", () => { }) .then(() => { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set("email", "testInvalidConfig@parse.com"); - user.signUp(null) - .then(() => 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 and email verification functionality.') - done(); - }); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'testInvalidConfig@parse.com'); + user + .signUp(null) + .then(() => + 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 and email verification functionality.' + ); + done(); + } + ); }) .catch(error => { fail(JSON.stringify(error)); @@ -425,16 +481,22 @@ describe("Custom Pages, Email Verification, Password Reset", () => { }) .then(() => { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set("email", "testInvalidConfig@parse.com"); - user.signUp(null) - .then(() => Parse.User.requestPasswordReset("testInvalidConfig@parse.com")) - .then(() => { - done(); - }, error => { - done(error); - }); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'testInvalidConfig@parse.com'); + user + .signUp(null) + .then(() => + Parse.User.requestPasswordReset('testInvalidConfig@parse.com') + ) + .then( + () => { + done(); + }, + error => { + done(error); + } + ); }) .catch(error => { fail(JSON.stringify(error)); @@ -450,7 +512,7 @@ describe("Custom Pages, Email Verification, Password Reset", () => { sendMail: function(options) { expect(options.to).toEqual('testValidConfig@parse.com'); return Promise.resolve(); - } + }, }); // delete that handler to force using the default @@ -460,20 +522,26 @@ describe("Custom Pages, Email Verification, Password Reset", () => { reconfigureServer({ appName: 'coolapp', publicServerURL: 'http://localhost:1337/1', - emailAdapter: adapter + emailAdapter: adapter, }) .then(() => { const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("testValidConfig@parse.com"); - user.signUp(null) - .then(() => Parse.User.requestPasswordReset("testValidConfig@parse.com")) - .then(() => { - expect(adapter.sendMail).toHaveBeenCalled(); - done(); - }, error => { - done(error); - }); + user.setPassword('asdf'); + user.setUsername('testValidConfig@parse.com'); + user + .signUp(null) + .then(() => + Parse.User.requestPasswordReset('testValidConfig@parse.com') + ) + .then( + () => { + expect(adapter.sendMail).toHaveBeenCalled(); + done(); + }, + error => { + done(error); + } + ); }) .catch(error => { fail(JSON.stringify(error)); @@ -485,25 +553,24 @@ describe("Custom Pages, Email Verification, Password Reset", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => Promise.resolve() - } + sendMail: () => Promise.resolve(), + }; reconfigureServer({ appName: 'unused', publicServerURL: 'http://localhost:1337/1', verifyUserEmails: false, emailAdapter: emailAdapter, - }) - .then(async () => { - spyOn(emailAdapter, 'sendVerificationEmail'); - const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - await user.signUp(); - await user.fetch() - expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0); - expect(user.get('emailVerified')).toEqual(undefined); - done(); - }); + }).then(async () => { + spyOn(emailAdapter, 'sendVerificationEmail'); + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + await user.signUp(); + await user.fetch(); + expect(emailAdapter.sendVerificationEmail.calls.count()).toEqual(0); + expect(user.get('emailVerified')).toEqual(undefined); + done(); + }); }); it('receives the app name and user in the adapter', done => { @@ -515,24 +582,23 @@ describe("Custom Pages, Email Verification, Password Reset", () => { emailSent = true; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailing app', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }) - .then(async () => { - const user = new Parse.User(); - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set('email', 'user@parse.com'); - await user.signUp(); - expect(emailSent).toBe(true); - done(); - }); - }) + publicServerURL: 'http://localhost:8378/1', + }).then(async () => { + const user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'user@parse.com'); + await user.signUp(); + expect(emailSent).toBe(true); + done(); + }); + }); it('when you click the link in the email it sets emailVerified to true and redirects you', done => { const user = new Parse.User(); @@ -542,39 +608,51 @@ describe("Custom Pages, Email Verification, Password Reset", () => { sendEmailOptions = options; }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailing app', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" + publicServerURL: 'http://localhost:8378/1', }) .then(() => { - user.setPassword("other-password"); - user.setUsername("user"); + user.setPassword('other-password'); + user.setUsername('user'); user.set('email', 'user@parse.com'); return user.signUp(); - }).then(() => { + }) + .then(() => { expect(sendEmailOptions).not.toBeUndefined(); - request.get(sendEmailOptions.link, { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user'); - user.fetch() - .then(() => { - expect(user.get('emailVerified')).toEqual(true); - done(); - }, (err) => { - jfail(err); - fail("this should not fail"); - done(); - }).catch((err) => { - jfail(err); - done(); - }) - }); + request.get( + sendEmailOptions.link, + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/verify_email_success.html?username=user' + ); + user + .fetch() + .then( + () => { + expect(user.get('emailVerified')).toEqual(true); + done(); + }, + err => { + jfail(err); + fail('this should not fail'); + done(); + } + ) + .catch(err => { + jfail(err); + done(); + }); + } + ); }); }); @@ -585,19 +663,24 @@ describe("Custom Pages, Email Verification, Password Reset", () => { emailAdapter: { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} + sendMail: () => {}, }, - publicServerURL: "http://localhost:8378/1" - }) - .then(() => { - request.get('http://localhost:8378/1/apps/test/verify_email', { + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + request.get( + 'http://localhost:8378/1/apps/test/verify_email', + { followRedirect: false, - }, (error, response) => { + }, + (error, response) => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); - done() - }); - }); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' + ); + done(); + } + ); + }); }); it('redirects you to invalid verification link page if you try to validate a nonexistant users email', done => { @@ -607,19 +690,24 @@ describe("Custom Pages, Email Verification, Password Reset", () => { emailAdapter: { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} + sendMail: () => {}, }, - publicServerURL: "http://localhost:8378/1" - }) - .then(() => { - request.get('http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', { + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + request.get( + 'http://localhost:8378/1/apps/test/verify_email?token=asdfasdf&username=sadfasga', + { followRedirect: false, - }, (error, response) => { + }, + (error, response) => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=sadfasga&appId=test'); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=sadfasga&appId=test' + ); done(); - }); - }); + } + ); + }); }); it('redirects you to link send fail page if you try to resend a link for a nonexistant user', done => { @@ -629,61 +717,70 @@ describe("Custom Pages, Email Verification, Password Reset", () => { emailAdapter: { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} + sendMail: () => {}, }, - publicServerURL: "http://localhost:8378/1" - }) - .then(() => { - request.post('http://localhost:8378/1/apps/test/resend_verification_email', { + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + request.post( + 'http://localhost:8378/1/apps/test/resend_verification_email', + { followRedirect: false, form: { - username: "sadfasga" - } - }, (error, response) => { + username: 'sadfasga', + }, + }, + (error, response) => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/link_send_fail.html'); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/link_send_fail.html' + ); done(); - }); - }); + } + ); + }); }); it('does not update email verified if you use an invalid token', done => { const user = new Parse.User(); const emailAdapter = { sendVerificationEmail: () => { - request.get('http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', { - followRedirect: false, - }, (error, response) => { - expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=zxcv&appId=test'); - user.fetch() - .then(() => { + request.get( + 'http://localhost:8378/1/apps/test/verify_email?token=invalid&username=zxcv', + { + followRedirect: false, + }, + (error, response) => { + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_verification_link.html?username=zxcv&appId=test' + ); + user.fetch().then(() => { expect(user.get('emailVerified')).toEqual(false); done(); }); - }); + } + ); }, sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailing app', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }) - .then(() => { - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set('email', 'user@parse.com'); - user.signUp(null, { - success: () => {}, - error: function() { - fail('Failed to save user'); - done(); - } - }); + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'user@parse.com'); + user.signUp(null, { + success: () => {}, + error: function() { + fail('Failed to save user'); + done(); + }, }); + }); }); it('should send a password reset link', done => { @@ -691,42 +788,45 @@ describe("Custom Pages, Email Verification, Password Reset", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { - followRedirect: false, - }, (error, response) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; + request.get( + options.link, + { + followRedirect: false, + }, + (error, response) => { + if (error) { + jfail(error); + fail('Failed to get the reset link'); + return; + } + expect(response.statusCode).toEqual(302); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv%2Bzxcv/; + expect(response.body.match(re)).not.toBe(null); + done(); } - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=[a-zA-Z0-9]+\&id=test\&username=zxcv%2Bzxcv/; - expect(response.body.match(re)).not.toBe(null); - done(); - }); + ); }, - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailing app', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }) - .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) => { - jfail(err); - fail("Should not fail requesting a password"); - done(); - } - }); + publicServerURL: 'http://localhost:8378/1', + }).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 => { + jfail(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 => { @@ -736,19 +836,24 @@ describe("Custom Pages, Email Verification, Password Reset", () => { emailAdapter: { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: () => Promise.resolve(), - sendMail: () => {} + sendMail: () => {}, }, - publicServerURL: "http://localhost:8378/1" - }) - .then(() => { - request.get('http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', { + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + request.get( + 'http://localhost:8378/1/apps/test/request_password_reset?token=asdfasdf&username=sadfasga', + { followRedirect: false, - }, (error, response) => { + }, + (error, response) => { expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html'); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/invalid_link.html' + ); done(); - }); - }); + } + ); + }); }); it('should programatically reset password', done => { @@ -756,81 +861,97 @@ describe("Custom Pages, Email Verification, Password Reset", () => { const emailAdapter = { sendVerificationEmail: () => Promise.resolve(), sendPasswordResetEmail: options => { - request.get(options.link, { - followRedirect: false, - }, (error, response) => { - if (error) { - jfail(error); - fail("Failed to get the reset link"); - return; - } - expect(response.statusCode).toEqual(302); - const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; - const match = response.body.match(re); - if (!match) { - fail("should have a token"); - done(); - return; - } - const token = match[1]; - - request.post({ - url: "http://localhost:8378/1/apps/test/request_password_reset" , - body: `new_password=hello&token=${token}&username=zxcv`, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, + request.get( + options.link, + { followRedirect: false, - }, (error, response) => { + }, + (error, response) => { if (error) { jfail(error); - fail("Failed to POST request password reset"); + fail('Failed to get the reset link'); return; } expect(response.statusCode).toEqual(302); - expect(response.body).toEqual('Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv'); - - Parse.User.logIn("zxcv", "hello").then(function(){ - const config = Config.get('test'); - 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); - done(); - }); - }, (err) => { - jfail(err); - fail("should login with new password"); + const re = /http:\/\/localhost:8378\/1\/apps\/choose_password\?token=([a-zA-Z0-9]+)\&id=test\&username=zxcv/; + const match = response.body.match(re); + if (!match) { + fail('should have a token'); done(); - }); - - }); - }); + return; + } + const token = match[1]; + + request.post( + { + url: 'http://localhost:8378/1/apps/test/request_password_reset', + body: `new_password=hello&token=${token}&username=zxcv`, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + followRedirect: false, + }, + (error, response) => { + if (error) { + jfail(error); + fail('Failed to POST request password reset'); + return; + } + expect(response.statusCode).toEqual(302); + expect(response.body).toEqual( + 'Found. Redirecting to http://localhost:8378/1/apps/password_reset_success.html?username=zxcv' + ); + + Parse.User.logIn('zxcv', 'hello').then( + function() { + const config = Config.get('test'); + 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 + ); + done(); + }); + }, + err => { + jfail(err); + fail('should login with new password'); + done(); + } + ); + } + ); + } + ); }, - sendMail: () => {} - } + sendMail: () => {}, + }; reconfigureServer({ appName: 'emailing app', verifyUserEmails: true, emailAdapter: emailAdapter, - publicServerURL: "http://localhost:8378/1" - }) - .then(() => { - user.setPassword("asdf"); - user.setUsername("zxcv"); - user.set('email', 'user@parse.com'); - user.signUp().then(() => { - Parse.User.requestPasswordReset('user@parse.com', { - error: (err) => { - jfail(err); - fail("Should not fail"); - done(); - } - }); + publicServerURL: 'http://localhost:8378/1', + }).then(() => { + user.setPassword('asdf'); + user.setUsername('zxcv'); + user.set('email', 'user@parse.com'); + user.signUp().then(() => { + Parse.User.requestPasswordReset('user@parse.com', { + error: err => { + jfail(err); + fail('Should not fail'); + done(); + }, }); }); + }); }); - }); - diff --git a/spec/VerifyUserPassword.spec.js b/spec/VerifyUserPassword.spec.js index 28bd7c38c3..945d7ca65d 100644 --- a/spec/VerifyUserPassword.spec.js +++ b/spec/VerifyUserPassword.spec.js @@ -1,29 +1,38 @@ -"use strict"; +'use strict'; const rp = require('request-promise'); const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); -const verifyPassword = function (login, password, isEmail = false) { - const body = (!isEmail) ? { username: login, password } : { email: login, password }; - return rp.get({ - url: Parse.serverURL + '/verifyPassword', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' - }, - body, - json: true - }).then((res) => res) - .catch((err) => err); +const verifyPassword = function(login, password, isEmail = false) { + const body = !isEmail + ? { username: login, password } + : { email: login, password }; + return rp + .get({ + url: Parse.serverURL + '/verifyPassword', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + body, + json: true, + }) + .then(res => res) + .catch(err => err); }; -const isAccountLockoutError = function (username, password, duration, waitTime) { +const isAccountLockoutError = function(username, password, duration, waitTime) { return new Promise((resolve, reject) => { setTimeout(() => { Parse.User.logIn(username, password) .then(() => reject('login should have failed')) .catch(err => { - if (err.message === 'Your account is locked due to multiple failed login attempts. Please try again after ' + duration + ' minute(s)') { + if ( + err.message === + 'Your account is locked due to multiple failed login attempts. Please try again after ' + + duration + + ' minute(s)' + ) { resolve(); } else { reject(err); @@ -33,8 +42,8 @@ const isAccountLockoutError = function (username, password, duration, waitTime) }); }; -describe("Verify User Password", () => { - it('fails to verify password when masterKey has locked out user', (done) => { +describe('Verify User Password', () => { + it('fails to verify password when masterKey has locked out user', done => { const user = new Parse.User(); const ACL = new Parse.ACL(); ACL.setPublicReadAccess(false); @@ -42,251 +51,325 @@ describe("Verify User Password", () => { user.setUsername('testuser'); user.setPassword('mypass'); user.setACL(ACL); - user.signUp().then(() => { - return Parse.User.logIn('testuser', 'mypass'); - }).then((user) => { - equal(user.get('username'), 'testuser'); - // Lock the user down - const ACL = new Parse.ACL(); - user.setACL(ACL); - return user.save(null, { useMasterKey: true }); - }).then(() => { - expect(user.getACL().getPublicReadAccess()).toBe(false); - return rp.get({ - url: Parse.serverURL + '/verifyPassword', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' - }, - qs: { - username: 'testuser', - password: 'mypass', - } + user + .signUp() + .then(() => { + return Parse.User.logIn('testuser', 'mypass'); + }) + .then(user => { + equal(user.get('username'), 'testuser'); + // Lock the user down + const ACL = new Parse.ACL(); + user.setACL(ACL); + return user.save(null, { useMasterKey: true }); + }) + .then(() => { + expect(user.getACL().getPublicReadAccess()).toBe(false); + return rp.get({ + url: Parse.serverURL + '/verifyPassword', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + qs: { + username: 'testuser', + password: 'mypass', + }, + }); + }) + .then(res => { + fail(res); + done(); + }) + .catch(err => { + expect(err.statusCode).toBe(404); + expect(err.error).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); + done(); }); - }).then((res) => { - fail(res); - done(); - }).catch((err) => { - expect(err.statusCode).toBe(404); - expect(err.error).toMatch('{"code":101,"error":"Invalid username/password."}'); - done(); - }); }); - it('fails to verify password when username is not provided in query string REST API', (done) => { + it('fails to verify password when username is not provided in query string REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return rp.get({ - url: Parse.serverURL + '/verifyPassword', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' - }, - qs: { - username: '', - password: 'mypass', - } + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return rp.get({ + url: Parse.serverURL + '/verifyPassword', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + qs: { + username: '', + password: 'mypass', + }, + }); + }) + .then(res => { + fail(res); + done(); + }) + .catch(err => { + expect(err.statusCode).toBe(400); + expect(err.error).toMatch( + '{"code":200,"error":"username/email is required."}' + ); + done(); }); - }).then((res) => { - fail(res); - done(); - }).catch((err) => { - expect(err.statusCode).toBe(400); - expect(err.error).toMatch('{"code":200,"error":"username/email is required."}'); - done(); - }); }); - it('fails to verify password when email is not provided in query string REST API', (done) => { + it('fails to verify password when email is not provided in query string REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return rp.get({ - url: Parse.serverURL + '/verifyPassword', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' - }, - qs: { - email: '', - password: 'mypass', - } + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return rp.get({ + url: Parse.serverURL + '/verifyPassword', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + qs: { + email: '', + password: 'mypass', + }, + }); + }) + .then(res => { + fail(res); + done(); + }) + .catch(err => { + expect(err.statusCode).toBe(400); + expect(err.error).toMatch( + '{"code":200,"error":"username/email is required."}' + ); + done(); }); - }).then((res) => { - fail(res); - done(); - }).catch((err) => { - expect(err.statusCode).toBe(400); - expect(err.error).toMatch('{"code":200,"error":"username/email is required."}'); - done(); - }); }); - it('fails to verify password when username is not provided with json payload REST API', (done) => { + it('fails to verify password when username is not provided with json payload REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword('', 'mypass'); - }).then((res) => { - expect(res.statusCode).toBe(400); - expect(JSON.stringify(res.error)).toMatch('{"code":200,"error":"username/email is required."}'); - done(); - }).catch((err) => { - fail(err); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword('', 'mypass'); + }) + .then(res => { + expect(res.statusCode).toBe(400); + expect(JSON.stringify(res.error)).toMatch( + '{"code":200,"error":"username/email is required."}' + ); + done(); + }) + .catch(err => { + fail(err); + done(); + }); }); - it('fails to verify password when email is not provided with json payload REST API', (done) => { + it('fails to verify password when email is not provided with json payload REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword('', 'mypass', true); - }).then((res) => { - expect(res.statusCode).toBe(400); - expect(JSON.stringify(res.error)).toMatch('{"code":200,"error":"username/email is required."}'); - done(); - }).catch((err) => { - fail(err); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword('', 'mypass', true); + }) + .then(res => { + expect(res.statusCode).toBe(400); + expect(JSON.stringify(res.error)).toMatch( + '{"code":200,"error":"username/email is required."}' + ); + done(); + }) + .catch(err => { + fail(err); + done(); + }); }); - it('fails to verify password when password is not provided with json payload REST API', (done) => { + it('fails to verify password when password is not provided with json payload REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword('testuser', ''); - }).then((res) => { - expect(res.statusCode).toBe(400); - expect(JSON.stringify(res.error)).toMatch('{"code":201,"error":"password is required."}'); - done(); - }).catch((err) => { - fail(err); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword('testuser', ''); + }) + .then(res => { + expect(res.statusCode).toBe(400); + expect(JSON.stringify(res.error)).toMatch( + '{"code":201,"error":"password is required."}' + ); + done(); + }) + .catch(err => { + fail(err); + done(); + }); }); - it('fails to verify password when username matches but password does not match hash with json payload REST API', (done) => { + it('fails to verify password when username matches but password does not match hash with json payload REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword('testuser', 'wrong password'); - }).then((res) => { - expect(res.statusCode).toBe(404); - expect(JSON.stringify(res.error)).toMatch('{"code":101,"error":"Invalid username/password."}'); - done(); - }).catch((err) => { - fail(err); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword('testuser', 'wrong password'); + }) + .then(res => { + expect(res.statusCode).toBe(404); + expect(JSON.stringify(res.error)).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); + done(); + }) + .catch(err => { + fail(err); + done(); + }); }); - it('fails to verify password when email matches but password does not match hash with json payload REST API', (done) => { + it('fails to verify password when email matches but password does not match hash with json payload REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword('my@user.com', 'wrong password', true); - }).then((res) => { - expect(res.statusCode).toBe(404); - expect(JSON.stringify(res.error)).toMatch('{"code":101,"error":"Invalid username/password."}'); - done(); - }).catch((err) => { - fail(err); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword('my@user.com', 'wrong password', true); + }) + .then(res => { + expect(res.statusCode).toBe(404); + expect(JSON.stringify(res.error)).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); + done(); + }) + .catch(err => { + fail(err); + done(); + }); }); - it('fails to verify password when typeof username does not equal string REST API', (done) => { + it('fails to verify password when typeof username does not equal string REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword(123, 'mypass'); - }).then((res) => { - expect(res.statusCode).toBe(404); - expect(JSON.stringify(res.error)).toMatch('{"code":101,"error":"Invalid username/password."}'); - done(); - }).catch((err) => { - fail(err); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword(123, 'mypass'); + }) + .then(res => { + expect(res.statusCode).toBe(404); + expect(JSON.stringify(res.error)).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); + done(); + }) + .catch(err => { + fail(err); + done(); + }); }); - it('fails to verify password when typeof email does not equal string REST API', (done) => { + it('fails to verify password when typeof email does not equal string REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword(123, 'mypass', true); - }).then((res) => { - expect(res.statusCode).toBe(404); - expect(JSON.stringify(res.error)).toMatch('{"code":101,"error":"Invalid username/password."}'); - done(); - }).catch((err) => { - fail(err); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword(123, 'mypass', true); + }) + .then(res => { + expect(res.statusCode).toBe(404); + expect(JSON.stringify(res.error)).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); + done(); + }) + .catch(err => { + fail(err); + done(); + }); }); - it('fails to verify password when typeof password does not equal string REST API', (done) => { + it('fails to verify password when typeof password does not equal string REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword('my@user.com', 123, true); - }).then((res) => { - expect(res.statusCode).toBe(404); - expect(JSON.stringify(res.error)).toMatch('{"code":101,"error":"Invalid username/password."}'); - done(); - }).catch((err) => { - fail(err); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword('my@user.com', 123, true); + }) + .then(res => { + expect(res.statusCode).toBe(404); + expect(JSON.stringify(res.error)).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); + done(); + }) + .catch(err => { + fail(err); + done(); + }); }); - it('fails to verify password when username cannot be found REST API', (done) => { + it('fails to verify password when username cannot be found REST API', done => { verifyPassword('mytestuser', 'mypass') - .then((res) => { + .then(res => { expect(res.statusCode).toBe(404); - expect(JSON.stringify(res.error)).toMatch('{"code":101,"error":"Invalid username/password."}'); + expect(JSON.stringify(res.error)).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); done(); - }).catch((err) => { + }) + .catch(err => { fail(err); done(); }); }); - it('fails to verify password when email cannot be found REST API', (done) => { + it('fails to verify password when email cannot be found REST API', done => { verifyPassword('my@user.com', 'mypass', true) - .then((res) => { + .then(res => { expect(res.statusCode).toBe(404); - expect(JSON.stringify(res.error)).toMatch('{"code":101,"error":"Invalid username/password."}'); + expect(JSON.stringify(res.error)).toMatch( + '{"code":101,"error":"Invalid username/password."}' + ); done(); - }).catch((err) => { + }) + .catch(err => { fail(err); done(); }); }); - it('fails to verify password when preventLoginWithUnverifiedEmail is set to true REST API', (done) => { + it('fails to verify password when preventLoginWithUnverifiedEmail is set to true REST API', done => { reconfigureServer({ - publicServerURL: "http://localhost:8378/", + publicServerURL: 'http://localhost:8378/', appName: 'emailVerify', verifyUserEmails: true, preventLoginWithUnverifiedEmail: true, @@ -295,40 +378,46 @@ describe("Verify User Password", () => { apiKey: 'k', domain: 'd', }), - }).then(() => { - const user = new Parse.User(); - return user.save({ - username: 'unverified-user', - password: 'mypass', - email: 'unverified-email@user.com' + }) + .then(() => { + const user = new Parse.User(); + return user.save({ + username: 'unverified-user', + password: 'mypass', + email: 'unverified-email@user.com', + }); + }) + .then(() => { + return verifyPassword('unverified-email@user.com', 'mypass', true); + }) + .then(res => { + expect(res.statusCode).toBe(400); + expect(JSON.stringify(res.error)).toMatch( + '{"code":205,"error":"User email is not verified."}' + ); + done(); + }) + .catch(err => { + fail(err); + done(); }); - }).then(() => { - return verifyPassword('unverified-email@user.com', 'mypass', true); - }).then((res) => { - expect(res.statusCode).toBe(400); - expect(JSON.stringify(res.error)).toMatch('{"code":205,"error":"User email is not verified."}'); - done(); - }).catch((err) => { - fail(err); - done(); - }); }); it('verify password lock account if failed verify password attempts are above threshold', done => { reconfigureServer({ appName: 'lockout threshold', accountLockout: { duration: 1, - threshold: 2 + threshold: 2, }, - publicServerURL: "http://localhost:8378/" + publicServerURL: 'http://localhost:8378/', }) .then(() => { const user = new Parse.User(); return user.save({ username: 'testuser', password: 'mypass', - email: 'my@user.com' - }) + email: 'my@user.com', + }); }) .then(() => { return verifyPassword('testuser', 'wrong password'); @@ -346,149 +435,174 @@ describe("Verify User Password", () => { done(); }) .catch(err => { - fail('lock account after failed login attempts test failed: ' + JSON.stringify(err)); + fail( + 'lock account after failed login attempts test failed: ' + + JSON.stringify(err) + ); done(); }); }); - it('succeed in verifying password when username and email are provided and password matches hash with json payload REST API', (done) => { + it('succeed in verifying password when username and email are provided and password matches hash with json payload REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return rp.get({ - url: Parse.serverURL + '/verifyPassword', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' - }, - body: { - username: 'testuser', - email: 'my@user.com', - password: 'mypass' - }, - json: true - }).then((res) => res) - .catch((err) => err); - }).then((res) => { - expect(typeof res).toBe('object'); - expect(typeof res['objectId']).toEqual('string'); - expect(res.hasOwnProperty('sessionToken')).toEqual(false); - expect(res.hasOwnProperty('password')).toEqual(false); - done(); - }).catch((err) => { - fail(err); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return rp + .get({ + url: Parse.serverURL + '/verifyPassword', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + body: { + username: 'testuser', + email: 'my@user.com', + password: 'mypass', + }, + json: true, + }) + .then(res => res) + .catch(err => err); + }) + .then(res => { + expect(typeof res).toBe('object'); + expect(typeof res['objectId']).toEqual('string'); + expect(res.hasOwnProperty('sessionToken')).toEqual(false); + expect(res.hasOwnProperty('password')).toEqual(false); + done(); + }) + .catch(err => { + fail(err); + done(); + }); }); - it('succeed in verifying password when username and password matches hash with json payload REST API', (done) => { + it('succeed in verifying password when username and password matches hash with json payload REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword('testuser', 'mypass'); - }).then((res) => { - expect(typeof res).toBe('object'); - expect(typeof res['objectId']).toEqual('string'); - expect(res.hasOwnProperty('sessionToken')).toEqual(false); - expect(res.hasOwnProperty('password')).toEqual(false); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword('testuser', 'mypass'); + }) + .then(res => { + expect(typeof res).toBe('object'); + expect(typeof res['objectId']).toEqual('string'); + expect(res.hasOwnProperty('sessionToken')).toEqual(false); + expect(res.hasOwnProperty('password')).toEqual(false); + done(); + }); }); - it('succeed in verifying password when email and password matches hash with json payload REST API', (done) => { + it('succeed in verifying password when email and password matches hash with json payload REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return verifyPassword('my@user.com', 'mypass', true); - }).then((res) => { - expect(typeof res).toBe('object'); - expect(typeof res['objectId']).toEqual('string'); - expect(res.hasOwnProperty('sessionToken')).toEqual(false); - expect(res.hasOwnProperty('password')).toEqual(false); - done(); - }); + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return verifyPassword('my@user.com', 'mypass', true); + }) + .then(res => { + expect(typeof res).toBe('object'); + expect(typeof res['objectId']).toEqual('string'); + expect(res.hasOwnProperty('sessionToken')).toEqual(false); + expect(res.hasOwnProperty('password')).toEqual(false); + done(); + }); }); - it('succeed to verify password when username and password provided in query string REST API', (done) => { + it('succeed to verify password when username and password provided in query string REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return rp.get({ - url: Parse.serverURL + '/verifyPassword', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' - }, - qs: { - username: 'testuser', - password: 'mypass', - } + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return rp.get({ + url: Parse.serverURL + '/verifyPassword', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + qs: { + username: 'testuser', + password: 'mypass', + }, + }); + }) + .then(res => { + expect(typeof res).toBe('string'); + const body = JSON.parse(res); + expect(typeof body['objectId']).toEqual('string'); + expect(body.hasOwnProperty('sessionToken')).toEqual(false); + expect(body.hasOwnProperty('password')).toEqual(false); + done(); }); - }).then((res) => { - expect(typeof res).toBe('string'); - const body = JSON.parse(res); - expect(typeof body['objectId']).toEqual('string'); - expect(body.hasOwnProperty('sessionToken')).toEqual(false); - expect(body.hasOwnProperty('password')).toEqual(false); - done(); - }); }); - it('succeed to verify password when email and password provided in query string REST API', (done) => { + it('succeed to verify password when email and password provided in query string REST API', done => { const user = new Parse.User(); - user.save({ - username: 'testuser', - password: 'mypass', - email: 'my@user.com' - }).then(() => { - return rp.get({ - url: Parse.serverURL + '/verifyPassword', - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-REST-API-Key': 'rest' - }, - qs: { - email: 'my@user.com', - password: 'mypass', - } + user + .save({ + username: 'testuser', + password: 'mypass', + email: 'my@user.com', + }) + .then(() => { + return rp.get({ + url: Parse.serverURL + '/verifyPassword', + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-REST-API-Key': 'rest', + }, + qs: { + email: 'my@user.com', + password: 'mypass', + }, + }); + }) + .then(res => { + expect(typeof res).toBe('string'); + const body = JSON.parse(res); + expect(typeof body['objectId']).toEqual('string'); + expect(body.hasOwnProperty('sessionToken')).toEqual(false); + expect(body.hasOwnProperty('password')).toEqual(false); + done(); }); - }).then((res) => { - expect(typeof res).toBe('string'); - const body = JSON.parse(res); - expect(typeof body['objectId']).toEqual('string'); - expect(body.hasOwnProperty('sessionToken')).toEqual(false); - expect(body.hasOwnProperty('password')).toEqual(false); - done(); - }); }); - it('succeed to verify password with username when user1 has username === user2 email REST API', (done) => { + it('succeed to verify password with username when user1 has username === user2 email REST API', done => { const user1 = new Parse.User(); - user1.save({ - username: 'email@user.com', - password: 'mypass1', - email: '1@user.com' - }).then(() => { - const user2 = new Parse.User(); - return user2.save({ - username: 'user2', - password: 'mypass2', - email: 'email@user.com' + user1 + .save({ + username: 'email@user.com', + password: 'mypass1', + email: '1@user.com', + }) + .then(() => { + const user2 = new Parse.User(); + return user2.save({ + username: 'user2', + password: 'mypass2', + email: 'email@user.com', + }); + }) + .then(() => { + return verifyPassword('email@user.com', 'mypass1'); + }) + .then(res => { + expect(typeof res).toBe('object'); + expect(typeof res['objectId']).toEqual('string'); + expect(res.hasOwnProperty('sessionToken')).toEqual(false); + expect(res.hasOwnProperty('password')).toEqual(false); + done(); }); - }).then(() => { - return verifyPassword('email@user.com', 'mypass1'); - }).then((res) => { - expect(typeof res).toBe('object'); - expect(typeof res['objectId']).toEqual('string'); - expect(res.hasOwnProperty('sessionToken')).toEqual(false); - expect(res.hasOwnProperty('password')).toEqual(false); - done(); - }); }); -}) +}); diff --git a/spec/WinstonLoggerAdapter.spec.js b/spec/WinstonLoggerAdapter.spec.js index 4a48745b6d..a7ca7fed94 100644 --- a/spec/WinstonLoggerAdapter.spec.js +++ b/spec/WinstonLoggerAdapter.spec.js @@ -1,62 +1,70 @@ 'use strict'; -const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter').WinstonLoggerAdapter; +const WinstonLoggerAdapter = require('../lib/Adapters/Logger/WinstonLoggerAdapter') + .WinstonLoggerAdapter; const request = require('request'); describe('info logs', () => { - - it("Verify INFO logs", (done) => { + it('Verify INFO logs', done => { const winstonLoggerAdapter = new WinstonLoggerAdapter(); winstonLoggerAdapter.log('info', 'testing info logs', () => { - winstonLoggerAdapter.query({ - from: new Date(Date.now() - 500), - size: 100, - level: 'info' - }, (results) => { - if (results.length == 0) { - fail('The adapter should return non-empty results'); - } else { - expect(results[0].message).toEqual('testing info logs'); - } - // Check the error log - // Regression #2639 - winstonLoggerAdapter.query({ - from: new Date(Date.now() - 200), + winstonLoggerAdapter.query( + { + from: new Date(Date.now() - 500), size: 100, - level: 'error' - }, (results) => { - expect(results.length).toEqual(0); - done(); - }); - }); + level: 'info', + }, + results => { + if (results.length == 0) { + fail('The adapter should return non-empty results'); + } else { + expect(results[0].message).toEqual('testing info logs'); + } + // Check the error log + // Regression #2639 + winstonLoggerAdapter.query( + { + from: new Date(Date.now() - 200), + size: 100, + level: 'error', + }, + results => { + expect(results.length).toEqual(0); + done(); + } + ); + } + ); }); }); }); describe('error logs', () => { - it("Verify ERROR logs", (done) => { + it('Verify ERROR logs', done => { const winstonLoggerAdapter = new WinstonLoggerAdapter(); winstonLoggerAdapter.log('error', 'testing error logs', () => { - winstonLoggerAdapter.query({ - from: new Date(Date.now() - 500), - size: 100, - level: 'error' - }, (results) => { - if(results.length == 0) { - fail('The adapter should return non-empty results'); - done(); - } - else { - expect(results[0].message).toEqual('testing error logs'); - done(); + winstonLoggerAdapter.query( + { + from: new Date(Date.now() - 500), + size: 100, + level: 'error', + }, + results => { + if (results.length == 0) { + fail('The adapter should return non-empty results'); + done(); + } else { + expect(results[0].message).toEqual('testing error logs'); + done(); + } } - }); + ); }); }); }); describe('verbose logs', () => { - it("mask sensitive information in _User class", (done) => { + it('mask sensitive information in _User class', done => { reconfigureServer({ verbose: true }) .then(() => createTestUser()) .then(() => { @@ -64,36 +72,43 @@ describe('verbose logs', () => { return winstonLoggerAdapter.query({ from: new Date(Date.now() - 500), size: 100, - level: 'verbose' + level: 'verbose', }); - }).then((results) => { + }) + .then(results => { const logString = JSON.stringify(results); expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0); expect(logString.match(/moon-y/g)).toBe(null); const headers = { 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.get({ - headers: headers, - url: 'http://localhost:8378/1/login?username=test&password=moon-y' - }, () => { - const winstonLoggerAdapter = new WinstonLoggerAdapter(); - return winstonLoggerAdapter.query({ - from: new Date(Date.now() - 500), - size: 100, - level: 'verbose' - }).then((results) => { - const logString = JSON.stringify(results); - expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0); - expect(logString.match(/moon-y/g)).toBe(null); - done(); - }); - }); - }).catch((err) => { + request.get( + { + headers: headers, + url: 'http://localhost:8378/1/login?username=test&password=moon-y', + }, + () => { + const winstonLoggerAdapter = new WinstonLoggerAdapter(); + return winstonLoggerAdapter + .query({ + from: new Date(Date.now() - 500), + size: 100, + level: 'verbose', + }) + .then(results => { + const logString = JSON.stringify(results); + expect(logString.match(/\*\*\*\*\*\*\*\*/g).length).not.toBe(0); + expect(logString.match(/moon-y/g)).toBe(null); + done(); + }); + } + ); + }) + .catch(err => { fail(JSON.stringify(err)); done(); - }) + }); }); }); diff --git a/spec/batch.spec.js b/spec/batch.spec.js index 0cc36796ab..5f002d4ca1 100644 --- a/spec/batch.spec.js +++ b/spec/batch.spec.js @@ -9,35 +9,53 @@ const publicServerURLNaked = 'http://domain.com/'; describe('batch', () => { it('should return the proper url', () => { - const internalURL = batch.makeBatchRoutingPathFunction(originalURL)('/parse/classes/Object'); + const internalURL = batch.makeBatchRoutingPathFunction(originalURL)( + '/parse/classes/Object' + ); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url same public/local endpoint', () => { const originalURL = '/parse/batch'; - const internalURL = batch.makeBatchRoutingPathFunction(originalURL, serverURL, publicServerURL)('/parse/classes/Object'); + const internalURL = batch.makeBatchRoutingPathFunction( + originalURL, + serverURL, + publicServerURL + )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with different public/local mount', () => { const originalURL = '/parse/batch'; - const internalURL = batch.makeBatchRoutingPathFunction(originalURL, serverURL1, publicServerURL)('/parse/classes/Object'); + const internalURL = batch.makeBatchRoutingPathFunction( + originalURL, + serverURL1, + publicServerURL + )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with naked public', () => { const originalURL = '/batch'; - const internalURL = batch.makeBatchRoutingPathFunction(originalURL, serverURL, publicServerURLNaked)('/classes/Object'); + const internalURL = batch.makeBatchRoutingPathFunction( + originalURL, + serverURL, + publicServerURLNaked + )('/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); it('should return the proper url with naked local', () => { const originalURL = '/parse/batch'; - const internalURL = batch.makeBatchRoutingPathFunction(originalURL, serverURLNaked, publicServerURL)('/parse/classes/Object'); + const internalURL = batch.makeBatchRoutingPathFunction( + originalURL, + serverURLNaked, + publicServerURL + )('/parse/classes/Object'); expect(internalURL).toEqual('/classes/Object'); }); diff --git a/spec/cryptoUtils.spec.js b/spec/cryptoUtils.spec.js index 8270e052cf..a3b1c69718 100644 --- a/spec/cryptoUtils.spec.js +++ b/spec/cryptoUtils.spec.js @@ -27,7 +27,9 @@ describe('randomString', () => { }); it('returns unique results', () => { - expect(givesUniqueResults(() => cryptoUtils.randomString(10), 100)).toBe(true); + expect(givesUniqueResults(() => cryptoUtils.randomString(10), 100)).toBe( + true + ); }); }); @@ -50,7 +52,9 @@ describe('randomHexString', () => { }); it('returns unique results', () => { - expect(givesUniqueResults(() => cryptoUtils.randomHexString(20), 100)).toBe(true); + expect(givesUniqueResults(() => cryptoUtils.randomHexString(20), 100)).toBe( + true + ); }); }); diff --git a/spec/features.spec.js b/spec/features.spec.js index c2a60ebd8e..df91a73841 100644 --- a/spec/features.spec.js +++ b/spec/features.spec.js @@ -1,20 +1,23 @@ 'use strict'; -const request = require("request"); +const request = require('request'); describe('features', () => { it('requires the master key to get features', done => { - request.get({ - url: 'http://localhost:8378/1/serverInfo', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + request.get( + { + url: 'http://localhost:8378/1/serverInfo', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized: master key is required'); + done(); } - }, (error, response, body) => { - expect(response.statusCode).toEqual(403); - expect(body.error).toEqual('unauthorized: master key is required'); - done(); - }); + ); }); }); diff --git a/spec/helper.js b/spec/helper.js index efa3f3f70a..393d2d48c8 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -1,11 +1,17 @@ -"use strict" +'use strict'; // Sets up a Parse API server for testing. const SpecReporter = require('jasmine-spec-reporter').SpecReporter; const supportsColor = require('supports-color'); -jasmine.DEFAULT_TIMEOUT_INTERVAL = process.env.PARSE_SERVER_TEST_TIMEOUT || 5000; +jasmine.DEFAULT_TIMEOUT_INTERVAL = + process.env.PARSE_SERVER_TEST_TIMEOUT || 5000; jasmine.getEnv().clearReporters(); -jasmine.getEnv().addReporter(new SpecReporter({ colors: { enabled: supportsColor.stdout }, spec: { displayDuration: true }})); +jasmine.getEnv().addReporter( + new SpecReporter({ + colors: { enabled: supportsColor.stdout }, + spec: { displayDuration: true }, + }) +); global.on_db = (db, callback, elseCallback) => { if (process.env.PARSE_SERVER_TEST_DB == db) { @@ -16,7 +22,7 @@ global.on_db = (db, callback, elseCallback) => { if (elseCallback) { return elseCallback(); } -} +}; if (global._babelPolyfill) { console.error('We should not use polyfilled tests'); @@ -27,14 +33,20 @@ const cache = require('../lib/cache').default; const ParseServer = require('../lib/index').ParseServer; const path = require('path'); const TestUtils = require('../lib/TestUtils'); -const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter').GridStoreAdapter; +const GridStoreAdapter = require('../lib/Adapters/Files/GridStoreAdapter') + .GridStoreAdapter; const FSAdapter = require('@parse/fs-files-adapter'); -const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter').default; -const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; -const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter').default; - -const mongoURI = 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; -const postgresURI = 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; +const PostgresStorageAdapter = require('../lib/Adapters/Storage/Postgres/PostgresStorageAdapter') + .default; +const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter') + .default; +const RedisCacheAdapter = require('../lib/Adapters/Cache/RedisCacheAdapter') + .default; + +const mongoURI = + 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase'; +const postgresURI = + 'postgres://localhost:5432/parse_server_postgres_adapter_test_database'; let databaseAdapter; // need to bind for mocking mocha @@ -49,8 +61,8 @@ if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { } else { startDB = require('mongodb-runner/mocha/before').bind({ timeout: () => {}, - slow: () => {} - }) + slow: () => {}, + }); stopDB = require('mongodb-runner/mocha/after'); databaseAdapter = new MongoStorageAdapter({ uri: mongoURI, @@ -62,11 +74,15 @@ const port = 8378; let filesAdapter; -on_db('mongo', () => { - filesAdapter = new GridStoreAdapter(mongoURI); -}, () => { - filesAdapter = new FSAdapter(); -}); +on_db( + 'mongo', + () => { + filesAdapter = new GridStoreAdapter(mongoURI); + }, + () => { + filesAdapter = new FSAdapter(); + } +); let logLevel; let silent = true; @@ -98,15 +114,16 @@ const defaultConfiguration = { android: { senderId: 'yolo', apiKey: 'yolo', - } + }, }, - auth: { // Override the facebook provider + auth: { + // Override the facebook provider facebook: mockFacebook(), myoauth: { - module: path.resolve(__dirname, "myoauth") // relative path as it's run from src + module: path.resolve(__dirname, 'myoauth'), // relative path as it's run from src }, - shortLivedAuth: mockShortLivedAuth() - } + shortLivedAuth: mockShortLivedAuth(), + }, }; if (process.env.PARSE_SERVER_TEST_CACHE === 'redis') { @@ -127,15 +144,21 @@ const reconfigureServer = changedConfiguration => { }); } try { - const newConfiguration = Object.assign({}, defaultConfiguration, changedConfiguration, { - __indexBuildCompletionCallbackForTests: indexBuildPromise => indexBuildPromise.then(resolve, reject), - mountPath: '/1', - port, - }); + const newConfiguration = Object.assign( + {}, + defaultConfiguration, + changedConfiguration, + { + __indexBuildCompletionCallbackForTests: indexBuildPromise => + indexBuildPromise.then(resolve, reject), + mountPath: '/1', + port, + } + ); cache.clear(); const parseServer = ParseServer.start(newConfiguration); parseServer.app.use(require('./testing-routes').router); - parseServer.expressApp.use('/1', (err) => { + parseServer.expressApp.use('/1', err => { console.error(err); fail('should not call next'); }); @@ -143,13 +166,15 @@ const reconfigureServer = changedConfiguration => { server.on('connection', connection => { const key = `${connection.remoteAddress}:${connection.remotePort}`; openConnections[key] = connection; - connection.on('close', () => { delete openConnections[key] }); + connection.on('close', () => { + delete openConnections[key]; + }); }); - } catch(error) { + } catch (error) { reject(error); } }); -} +}; // Set up a Parse client to talk to our test API server const Parse = require('parse/node'); @@ -170,8 +195,11 @@ beforeEach(done => { } TestUtils.destroyAllDataPermanently(true) .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')) { + // 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); @@ -183,53 +211,72 @@ beforeEach(done => { Parse.initialize('test', 'test', 'test'); Parse.serverURL = 'http://localhost:' + port + '/1'; done(); - }).catch(done.fail); + }) + .catch(done.fail); }); afterEach(function(done) { const afterLogOut = () => { if (Object.keys(openConnections).length > 0) { - fail('There were open connections to the server left after the test finished'); + fail( + 'There were open connections to the server left after the test finished' + ); } - on_db('postgres', () => { - TestUtils.destroyAllDataPermanently(true).then(done, done); - }, done); + on_db( + 'postgres', + () => { + TestUtils.destroyAllDataPermanently(true).then(done, done); + }, + done + ); }; Parse.Cloud._removeAllHooks(); - databaseAdapter.getAllClasses() + databaseAdapter + .getAllClasses() .then(allSchemas => { - allSchemas.forEach((schema) => { + allSchemas.forEach(schema => { const 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', '_Audience'].indexOf(className) >= 0; - } - }}); + 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', + '_Audience', + ].indexOf(className) >= 0 + ); + } + }, + }); }); }) .then(() => Parse.User.logOut()) .then(() => {}, () => {}) // swallow errors .then(() => { // Connection close events are not immediate on node 10+... wait a bit - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(resolve, 0); }); }) - .then(afterLogOut) + .then(afterLogOut); }); const TestObject = Parse.Object.extend({ - className: "TestObject" + className: 'TestObject', }); const Item = Parse.Object.extend({ - className: "Item" + className: 'Item', }); const Container = Parse.Object.extend({ - className: "Container" + className: 'Container', }); // Convenience method to create a new TestObject with a callback @@ -323,7 +370,7 @@ function mockShortLivedAuth() { let accessToken; auth.setValidAccessToken = function(validAccessToken) { accessToken = validAccessToken; - } + }; auth.validateAuthData = function(authData) { if (authData.access_token == accessToken) { return Promise.resolve(); @@ -337,7 +384,6 @@ function mockShortLivedAuth() { return auth; } - // This is polluting, but, it makes it way easier to directly port old tests. global.Parse = Parse; global.TestObject = TestObject; @@ -357,7 +403,7 @@ global.defaultConfiguration = defaultConfiguration; global.mockFacebookAuthenticator = mockFacebookAuthenticator; global.jfail = function(err) { fail(JSON.stringify(err)); -} +}; global.it_exclude_dbs = excluded => { if (excluded.indexOf(process.env.PARSE_SERVER_TEST_DB) >= 0) { @@ -365,10 +411,13 @@ global.it_exclude_dbs = excluded => { } else { return it; } -} +}; global.it_only_db = db => { - if (process.env.PARSE_SERVER_TEST_DB === db || !process.env.PARSE_SERVER_TEST_DB && db == 'mongo') { + if ( + process.env.PARSE_SERVER_TEST_DB === db || + (!process.env.PARSE_SERVER_TEST_DB && db == 'mongo') + ) { return it; } else { return xit; @@ -381,7 +430,7 @@ global.fit_exclude_dbs = excluded => { } else { return fit; } -} +}; global.describe_only_db = db => { if (process.env.PARSE_SERVER_TEST_DB == db) { @@ -391,9 +440,9 @@ global.describe_only_db = db => { } else { return xdescribe; } -} +}; -global.describe_only = (validator) =>{ +global.describe_only = validator => { if (validator()) { return describe; } else { @@ -401,7 +450,6 @@ global.describe_only = (validator) =>{ } }; - const libraryCache = {}; jasmine.mockLibrary = function(library, name, mock) { const original = require(library)[name]; @@ -410,11 +458,11 @@ jasmine.mockLibrary = function(library, name, mock) { } require(library)[name] = mock; libraryCache[library][name] = original; -} +}; jasmine.restoreLibrary = function(library, name) { if (!libraryCache[library] || !libraryCache[library][name]) { throw 'Can not find library ' + library + ' ' + name; } require(library)[name] = libraryCache[library][name]; -} +}; diff --git a/spec/index.spec.js b/spec/index.spec.js index 4e48cb2b48..9614590e4f 100644 --- a/spec/index.spec.js +++ b/spec/index.spec.js @@ -1,12 +1,13 @@ -"use strict" +'use strict'; const request = require('request'); const parseServerPackage = require('../package.json'); const MockEmailAdapterWithOptions = require('./MockEmailAdapterWithOptions'); -const ParseServer = require("../lib/index"); +const ParseServer = require('../lib/index'); const Config = require('../lib/Config'); const express = require('express'); -const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter').default; +const MongoStorageAdapter = require('../lib/Adapters/Storage/Mongo/MongoStorageAdapter') + .default; describe('server', () => { it('requires a master key and app id', done => { @@ -27,37 +28,50 @@ describe('server', () => { it('support http basic authentication with masterkey', done => { reconfigureServer({ appId: 'test' }).then(() => { - request.get({ - url: 'http://localhost:8378/1/classes/TestObject', - headers: { - 'Authorization': 'Basic ' + new Buffer('test:' + 'test').toString('base64') + request.get( + { + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + Authorization: + 'Basic ' + new Buffer('test:' + 'test').toString('base64'), + }, + }, + (error, response) => { + expect(response.statusCode).toEqual(200); + done(); } - }, (error, response) => { - expect(response.statusCode).toEqual(200); - done(); - }); - }) + ); + }); }); it('support http basic authentication with javascriptKey', done => { reconfigureServer({ appId: 'test' }).then(() => { - request.get({ - url: 'http://localhost:8378/1/classes/TestObject', - headers: { - 'Authorization': 'Basic ' + new Buffer('test:javascript-key=' + 'test').toString('base64') + request.get( + { + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + Authorization: + 'Basic ' + + new Buffer('test:javascript-key=' + 'test').toString('base64'), + }, + }, + (error, response) => { + expect(response.statusCode).toEqual(200); + done(); } - }, (error, response) => { - expect(response.statusCode).toEqual(200); - done(); - }); - }) + ); + }); }); it('fails if database is unreachable', done => { - reconfigureServer({ databaseAdapter: new MongoStorageAdapter({ uri: 'mongodb://fake:fake@localhost:43605/drew3' }) }) - .catch(() => { + 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({ + request.post( + { url: 'http://localhost:8378/1/classes/NewClass', headers: { 'X-Parse-Application-Id': 'test', @@ -65,13 +79,15 @@ describe('server', () => { }, body: {}, json: true, - }, (error, response, body) => { + }, + (error, response, body) => { expect(response.statusCode).toEqual(500); expect(body.code).toEqual(1); expect(body.message).toEqual('Internal server error.'); reconfigureServer().then(done, done); - }); - }); + } + ); + }); }); it('can load email adapter via object', done => { @@ -83,7 +99,7 @@ describe('server', () => { apiKey: 'k', domain: 'd', }), - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', }).then(done, fail); }); @@ -97,9 +113,9 @@ describe('server', () => { fromAddress: 'parse@example.com', apiKey: 'k', domain: 'd', - } + }, }, - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', }).then(done, fail); }); @@ -113,9 +129,9 @@ describe('server', () => { fromAddress: 'parse@example.com', apiKey: 'k', domain: 'd', - } + }, }, - publicServerURL: 'http://localhost:8378/1' + publicServerURL: 'http://localhost:8378/1', }).then(done, fail); }); @@ -124,12 +140,13 @@ describe('server', () => { appName: 'unused', verifyUserEmails: true, emailAdapter: '@parse/simple-mailgun-adapter', - publicServerURL: 'http://localhost:8378/1' - }) - .catch(error => { - expect(error).toEqual('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }).catch(error => { + expect(error).toEqual( + 'SimpleMailgunAdapter requires an API Key, domain, and fromAddress.' + ); + done(); + }); }); it('throws if you initialize email adapter incorrectly', done => { @@ -140,28 +157,32 @@ describe('server', () => { module: '@parse/simple-mailgun-adapter', options: { domain: 'd', - } + }, }, - publicServerURL: 'http://localhost:8378/1' - }) - .catch(error => { - expect(error).toEqual('SimpleMailgunAdapter requires an API Key, domain, and fromAddress.'); - done(); - }); + publicServerURL: 'http://localhost:8378/1', + }).catch(error => { + expect(error).toEqual( + 'SimpleMailgunAdapter requires an API Key, domain, and fromAddress.' + ); + done(); + }); }); it('can report the server version', done => { - request.get({ - url: 'http://localhost:8378/1/serverInfo', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', + request.get( + { + url: 'http://localhost:8378/1/serverInfo', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + json: true, }, - json: true, - }, (error, response, body) => { - expect(body.parseServerVersion).toEqual(parseServerPackage.version); - done(); - }) + (error, response, body) => { + expect(body.parseServerVersion).toEqual(parseServerPackage.version); + done(); + } + ); }); it('can properly sets the push support', done => { @@ -169,40 +190,48 @@ describe('server', () => { const config = Config.get('test'); expect(config.hasPushSupport).toEqual(true); expect(config.hasPushScheduledSupport).toEqual(false); - request.get({ - url: 'http://localhost:8378/1/serverInfo', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - json: true, - }, (error, response, body) => { - expect(body.features.push.immediatePush).toEqual(true); - expect(body.features.push.scheduledPush).toEqual(false); - done(); - }) - }); - - it('can properly sets the push support when not configured', done => { - reconfigureServer({ - push: undefined // force no config - }).then(() => { - const config = Config.get('test'); - expect(config.hasPushSupport).toEqual(false); - expect(config.hasPushScheduledSupport).toEqual(false); - request.get({ + request.get( + { url: 'http://localhost:8378/1/serverInfo', headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-Master-Key': 'test', }, json: true, - }, (error, response, body) => { - expect(body.features.push.immediatePush).toEqual(false); + }, + (error, response, body) => { + expect(body.features.push.immediatePush).toEqual(true); expect(body.features.push.scheduledPush).toEqual(false); done(); + } + ); + }); + + it('can properly sets the push support when not configured', done => { + reconfigureServer({ + push: undefined, // force no config + }) + .then(() => { + const config = Config.get('test'); + expect(config.hasPushSupport).toEqual(false); + expect(config.hasPushScheduledSupport).toEqual(false); + request.get( + { + url: 'http://localhost:8378/1/serverInfo', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + json: true, + }, + (error, response, body) => { + expect(body.features.push.immediatePush).toEqual(false); + expect(body.features.push.scheduledPush).toEqual(false); + done(); + } + ); }) - }).catch(done.fail); + .catch(done.fail); }); it('can properly sets the push support ', done => { @@ -210,26 +239,31 @@ describe('server', () => { push: { adapter: { send() {}, - getValidPushTypes() {} - } - } - }).then(() => { - const config = Config.get('test'); - expect(config.hasPushSupport).toEqual(true); - expect(config.hasPushScheduledSupport).toEqual(false); - request.get({ - url: 'http://localhost:8378/1/serverInfo', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', + getValidPushTypes() {}, }, - json: true, - }, (error, response, body) => { - expect(body.features.push.immediatePush).toEqual(true); - expect(body.features.push.scheduledPush).toEqual(false); - done(); + }, + }) + .then(() => { + const config = Config.get('test'); + expect(config.hasPushSupport).toEqual(true); + expect(config.hasPushScheduledSupport).toEqual(false); + request.get( + { + url: 'http://localhost:8378/1/serverInfo', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + json: true, + }, + (error, response, body) => { + expect(body.features.push.immediatePush).toEqual(true); + expect(body.features.push.scheduledPush).toEqual(false); + done(); + } + ); }) - }).catch(done.fail); + .catch(done.fail); }); it('can properly sets the push schedule support', done => { @@ -237,91 +271,103 @@ describe('server', () => { push: { adapter: { send() {}, - getValidPushTypes() {} - } + getValidPushTypes() {}, + }, }, scheduledPush: true, - }).then(() => { - const config = Config.get('test'); - expect(config.hasPushSupport).toEqual(true); - expect(config.hasPushScheduledSupport).toEqual(true); - request.get({ - url: 'http://localhost:8378/1/serverInfo', - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - json: true, - }, (error, response, body) => { - expect(body.features.push.immediatePush).toEqual(true); - expect(body.features.push.scheduledPush).toEqual(true); - done(); + }) + .then(() => { + const config = Config.get('test'); + expect(config.hasPushSupport).toEqual(true); + expect(config.hasPushScheduledSupport).toEqual(true); + request.get( + { + url: 'http://localhost:8378/1/serverInfo', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + json: true, + }, + (error, response, body) => { + expect(body.features.push.immediatePush).toEqual(true); + expect(body.features.push.scheduledPush).toEqual(true); + done(); + } + ); }) - }).catch(done.fail); + .catch(done.fail); }); it('can respond 200 on path health', done => { - request.get({ - url: 'http://localhost:8378/1/health', - }, (error, response) => { - expect(response.statusCode).toBe(200); - done(); - }); + request.get( + { + url: 'http://localhost:8378/1/health', + }, + (error, response) => { + expect(response.statusCode).toBe(200); + done(); + } + ); }); it('can create a parse-server v1', done => { - const parseServer = new ParseServer.default(Object.assign({}, - defaultConfiguration, { - appId: "aTestApp", - masterKey: "aTestMasterKey", - serverURL: "http://localhost:12666/parse", + const parseServer = new ParseServer.default( + Object.assign({}, defaultConfiguration, { + appId: 'aTestApp', + masterKey: 'aTestMasterKey', + serverURL: 'http://localhost:12666/parse', __indexBuildCompletionCallbackForTests: promise => { - promise - .then(() => { - expect(Parse.applicationId).toEqual("aTestApp"); - const app = express(); - app.use('/parse', parseServer.app); - - const server = app.listen(12666); - const obj = new Parse.Object("AnObject"); - let objId; - obj.save().then((obj) => { + promise.then(() => { + expect(Parse.applicationId).toEqual('aTestApp'); + const app = express(); + app.use('/parse', parseServer.app); + + const server = app.listen(12666); + const obj = new Parse.Object('AnObject'); + let objId; + obj + .save() + .then(obj => { objId = obj.id; - const q = new Parse.Query("AnObject"); + const q = new Parse.Query('AnObject'); return q.first(); - }).then((obj) => { + }) + .then(obj => { expect(obj.id).toEqual(objId); server.close(done); - }).catch(() => { - server.close(done); }) - }); - }}) + .catch(() => { + server.close(done); + }); + }); + }, + }) ); }); it('can create a parse-server v2', done => { let objId; - let server - const parseServer = ParseServer.ParseServer(Object.assign({}, - defaultConfiguration, { - appId: "anOtherTestApp", - masterKey: "anOtherTestMasterKey", - serverURL: "http://localhost:12667/parse", + let server; + const parseServer = ParseServer.ParseServer( + Object.assign({}, defaultConfiguration, { + appId: 'anOtherTestApp', + masterKey: 'anOtherTestMasterKey', + serverURL: 'http://localhost:12667/parse', __indexBuildCompletionCallbackForTests: promise => { promise .then(() => { - expect(Parse.applicationId).toEqual("anOtherTestApp"); + expect(Parse.applicationId).toEqual('anOtherTestApp'); const app = express(); app.use('/parse', parseServer); server = app.listen(12667); - const obj = new Parse.Object("AnObject"); - return obj.save() + const obj = new Parse.Object('AnObject'); + return obj.save(); }) .then(obj => { objId = obj.id; - const q = new Parse.Query("AnObject"); + const q = new Parse.Query('AnObject'); return q.first(); }) .then(obj => { @@ -329,28 +375,35 @@ describe('server', () => { server.close(done); }) .catch(error => { - fail(JSON.stringify(error)) + fail(JSON.stringify(error)); if (server) { server.close(done); } else { done(); } }); - }} - )); + }, + }) + ); }); it('has createLiveQueryServer', done => { // original implementation through the factory - expect(typeof ParseServer.ParseServer.createLiveQueryServer).toEqual('function'); + expect(typeof ParseServer.ParseServer.createLiveQueryServer).toEqual( + 'function' + ); // For import calls - expect(typeof ParseServer.default.createLiveQueryServer).toEqual('function'); + expect(typeof ParseServer.default.createLiveQueryServer).toEqual( + 'function' + ); done(); }); it('exposes correct adapters', done => { expect(ParseServer.S3Adapter).toThrow(); - expect(ParseServer.GCSAdapter).toThrow('GCSAdapter is not provided by parse-server anymore; please install @parse/gcs-files-adapter'); + expect(ParseServer.GCSAdapter).toThrow( + 'GCSAdapter is not provided by parse-server anymore; please install @parse/gcs-files-adapter' + ); expect(ParseServer.FileSystemAdapter).toThrow(); expect(ParseServer.InMemoryCacheAdapter).toThrow(); expect(ParseServer.NullCacheAdapter).toThrow(); @@ -358,29 +411,30 @@ describe('server', () => { }); it('properly gives publicServerURL when set', done => { - reconfigureServer({ publicServerURL: 'https://myserver.com/1' }) - .then(() => { + reconfigureServer({ publicServerURL: 'https://myserver.com/1' }).then( + () => { const config = Config.get('test', 'http://localhost:8378/1'); expect(config.mount).toEqual('https://myserver.com/1'); done(); - }); + } + ); }); it('properly removes trailing slash in mount', done => { - reconfigureServer({}) - .then(() => { - const config = Config.get('test', 'http://localhost:8378/1/'); - expect(config.mount).toEqual('http://localhost:8378/1'); - done(); - }); + reconfigureServer({}).then(() => { + const config = Config.get('test', 'http://localhost:8378/1/'); + expect(config.mount).toEqual('http://localhost:8378/1'); + done(); + }); }); it('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(); - }) + reconfigureServer({ publicServerURL: 'blabla:/some' }).catch(error => { + expect(error).toEqual( + 'publicServerURL should be a valid HTTPS URL starting with https://' + ); + done(); + }); }); it('fails if the session length is not a number', done => { @@ -397,7 +451,7 @@ describe('server', () => { .then(done.fail) .catch(error => { expect(error).toEqual('Session length must be a value greater than 0.'); - return reconfigureServer({ sessionLength: '0' }) + return reconfigureServer({ sessionLength: '0' }); }) .catch(error => { expect(error).toEqual('Session length must be a value greater than 0.'); @@ -405,71 +459,78 @@ describe('server', () => { }); }); - it('ignores the session length when expireInactiveSessions set to false', (done) => { + it('ignores the session length when expireInactiveSessions set to false', done => { reconfigureServer({ sessionLength: '-33', - expireInactiveSessions: false + expireInactiveSessions: false, }) - .then(() => reconfigureServer({ - sessionLength: '0', - expireInactiveSessions: false - })) + .then(() => + reconfigureServer({ + sessionLength: '0', + expireInactiveSessions: false, + }) + ) .then(done); - }) + }); - it('fails if maxLimit is negative', (done) => { - reconfigureServer({ maxLimit: -100 }) - .catch(error => { - expect(error).toEqual('Max limit must be a value greater than 0.'); - done(); - }); + it('fails if maxLimit is negative', done => { + reconfigureServer({ maxLimit: -100 }).catch(error => { + expect(error).toEqual('Max limit must be a value greater than 0.'); + done(); + }); }); it('fails if you try to set revokeSessionOnPasswordReset to non-boolean', done => { - reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' }) - .catch(done); + reconfigureServer({ revokeSessionOnPasswordReset: 'non-bool' }).catch(done); }); it('fails if you provides invalid ip in masterKeyIps', done => { - reconfigureServer({ masterKeyIps: ['invalidIp','1.2.3.4'] }) - .catch(error => { + reconfigureServer({ masterKeyIps: ['invalidIp', '1.2.3.4'] }).catch( + error => { expect(error).toEqual('Invalid ip in masterKeyIps: invalidIp'); done(); - }) + } + ); }); it('should succeed if you provide valid ip in masterKeyIps', done => { - reconfigureServer({ masterKeyIps: ['1.2.3.4','2001:0db8:0000:0042:0000:8a2e:0370:7334'] }) - .then(done) + reconfigureServer({ + masterKeyIps: ['1.2.3.4', '2001:0db8:0000:0042:0000:8a2e:0370:7334'], + }).then(done); }); - it('should load a middleware', (done) => { + it('should load a middleware', done => { const obj = { middleware: function(req, res, next) { next(); - } - } + }, + }; const spy = spyOn(obj, 'middleware').and.callThrough(); reconfigureServer({ - middleware: obj.middleware - }).then(() => { - const query = new Parse.Query('AnObject'); - return query.find(); - }).then(() => { - expect(spy).toHaveBeenCalled(); - done(); - }).catch(done.fail); + middleware: obj.middleware, + }) + .then(() => { + const query = new Parse.Query('AnObject'); + return query.find(); + }) + .then(() => { + expect(spy).toHaveBeenCalled(); + done(); + }) + .catch(done.fail); }); - it('should load a middleware from string', (done) => { + it('should load a middleware from string', done => { reconfigureServer({ - middleware: 'spec/support/CustomMiddleware' - }).then(() => { - return request.get('http://localhost:8378/1', (err, res) => { - // Just check that the middleware set the header - expect(res.headers['x-yolo']).toBe('1'); - done(); - }); - }).catch(done.fail); + middleware: 'spec/support/CustomMiddleware', + }) + .then(() => { + return request.get('http://localhost:8378/1', (err, res) => { + // Just check that the middleware set the header + expect(res.headers['x-yolo']).toBe('1'); + done(); + }); + }) + .catch(done.fail); }); }); diff --git a/spec/myoauth.js b/spec/myoauth.js index d28f9e8130..2367ad62ce 100644 --- a/spec/myoauth.js +++ b/spec/myoauth.js @@ -2,7 +2,7 @@ // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - if (authData.id == "12345" && authData.access_token == "12345") { + if (authData.id == '12345' && authData.access_token == '12345') { return Promise.resolve(); } return Promise.reject(); @@ -13,5 +13,5 @@ function validateAppId() { module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/spec/parsers.spec.js b/spec/parsers.spec.js index 9249c0fa6f..d2a6f20991 100644 --- a/spec/parsers.spec.js +++ b/spec/parsers.spec.js @@ -13,7 +13,9 @@ describe('parsers', () => { const parser = numberParser('key'); expect(parser(2)).toEqual(2); expect(parser('2')).toEqual(2); - expect(() => {parser('string')}).toThrow(); + expect(() => { + parser('string'); + }).toThrow(); }); it('parses correctly with numberOrBoolParser', () => { @@ -38,24 +40,28 @@ describe('parsers', () => { it('parses correctly with objectParser', () => { const parser = objectParser; - expect(parser({hello: 'world'})).toEqual({hello: 'world'}); - expect(parser('{"hello": "world"}')).toEqual({hello: 'world'}); - expect(() => {parser('string')}).toThrow(); + expect(parser({ hello: 'world' })).toEqual({ hello: 'world' }); + expect(parser('{"hello": "world"}')).toEqual({ hello: 'world' }); + expect(() => { + parser('string'); + }).toThrow(); }); it('parses correctly with moduleOrObjectParser', () => { const parser = moduleOrObjectParser; - expect(parser({hello: 'world'})).toEqual({hello: 'world'}); - expect(parser('{"hello": "world"}')).toEqual({hello: 'world'}); + expect(parser({ hello: 'world' })).toEqual({ hello: 'world' }); + expect(parser('{"hello": "world"}')).toEqual({ hello: 'world' }); expect(parser('string')).toEqual('string'); }); it('parses correctly with arrayParser', () => { const parser = arrayParser; - expect(parser([1,2,3])).toEqual([1,2,3]); + expect(parser([1, 2, 3])).toEqual([1, 2, 3]); expect(parser('{"hello": "world"}')).toEqual(['{"hello": "world"}']); - expect(parser('1,2,3')).toEqual(['1','2','3']); - expect(() => {parser(1)}).toThrow(); + expect(parser('1,2,3')).toEqual(['1', '2', '3']); + expect(() => { + parser(1); + }).toThrow(); }); it('parses correctly with nullParser', () => { diff --git a/spec/rest.spec.js b/spec/rest.spec.js index 79a1094423..7234536de6 100644 --- a/spec/rest.spec.js +++ b/spec/rest.spec.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; // These tests check the "create" / "update" functionality of the REST API. const auth = require('../lib/Auth'); const Config = require('../lib/Config'); @@ -12,14 +12,14 @@ let config; let database; describe('rest create', () => { - beforeEach(() => { config = Config.get('test'); database = config.database; }); it('handles _id', done => { - rest.create(config, auth.nobody(config), 'Foo', {}) + rest + .create(config, auth.nobody(config), 'Foo', {}) .then(() => database.adapter.find('Foo', { fields: {} }, {}, {})) .then(results => { expect(results.length).toEqual(1); @@ -33,9 +33,10 @@ describe('rest create', () => { it('can use custom _id size', done => { config.objectIdSize = 20; - rest.create(config, auth.nobody(config), 'Foo', {}) + rest + .create(config, auth.nobody(config), 'Foo', {}) .then(() => database.adapter.find('Foo', { fields: {} }, {}, {})) - .then((results) => { + .then(results => { expect(results.length).toEqual(1); const obj = results[0]; expect(typeof obj.objectId).toEqual('string'); @@ -45,48 +46,66 @@ describe('rest create', () => { }); it('is backwards compatible when _id size changes', done => { - rest.create(config, auth.nobody(config), 'Foo', {size: 10}) + rest + .create(config, auth.nobody(config), 'Foo', { size: 10 }) .then(() => { config.objectIdSize = 20; - return rest.find(config, auth.nobody(config), 'Foo', {size: 10}); + return rest.find(config, auth.nobody(config), 'Foo', { size: 10 }); }) - .then((response) => { + .then(response => { expect(response.results.length).toEqual(1); expect(response.results[0].objectId.length).toEqual(10); - return rest.update(config, auth.nobody(config), 'Foo', {objectId: response.results[0].objectId}, {update: 20}); + return rest.update( + config, + auth.nobody(config), + 'Foo', + { objectId: response.results[0].objectId }, + { update: 20 } + ); }) .then(() => { - return rest.find(config, auth.nobody(config), 'Foo', {size: 10}); - }).then((response) => { + return rest.find(config, auth.nobody(config), 'Foo', { size: 10 }); + }) + .then(response => { expect(response.results.length).toEqual(1); expect(response.results[0].objectId.length).toEqual(10); expect(response.results[0].update).toEqual(20); - return rest.create(config, auth.nobody(config), 'Foo', {size: 20}); + return rest.create(config, auth.nobody(config), 'Foo', { size: 20 }); }) .then(() => { config.objectIdSize = 10; - return rest.find(config, auth.nobody(config), 'Foo', {size: 20}); + return rest.find(config, auth.nobody(config), 'Foo', { size: 20 }); }) - .then((response) => { + .then(response => { expect(response.results.length).toEqual(1); expect(response.results[0].objectId.length).toEqual(20); done(); }); }); - it('handles array, object, date', (done) => { + it('handles array, object, date', done => { const now = new Date(); const obj = { array: [1, 2, 3], - object: {foo: 'bar'}, + object: { foo: 'bar' }, date: Parse._encode(now), }; - rest.create(config, auth.nobody(config), 'MyClass', obj) - .then(() => database.adapter.find('MyClass', { fields: { - array: { type: 'Array' }, - object: { type: 'Object' }, - date: { type: 'Date' }, - } }, {}, {})) + 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); const mob = results[0]; @@ -99,13 +118,14 @@ describe('rest create', () => { }); it('handles object and subdocument', done => { - const obj = { subdoc: {foo: 'bar', wu: 'tan'} }; + const obj = { subdoc: { foo: 'bar', wu: 'tan' } }; Parse.Cloud.beforeSave('MyClass', function() { // this beforeSave trigger should do nothing but can mess with the object }); - rest.create(config, auth.nobody(config), 'MyClass', obj) + rest + .create(config, auth.nobody(config), 'MyClass', obj) .then(() => database.adapter.find('MyClass', { fields: {} }, {}, {})) .then(results => { expect(results.length).toEqual(1); @@ -115,7 +135,13 @@ describe('rest create', () => { expect(mob.subdoc.wu).toBe('tan'); expect(typeof mob.objectId).toEqual('string'); const obj = { 'subdoc.wu': 'clan' }; - return rest.update(config, auth.nobody(config), 'MyClass', { objectId: mob.objectId }, obj); + return rest.update( + config, + auth.nobody(config), + 'MyClass', + { objectId: mob.objectId }, + obj + ); }) .then(() => database.adapter.find('MyClass', { fields: {} }, {}, {})) .then(results => { @@ -133,87 +159,114 @@ describe('rest create', () => { }); }); - it('handles create on non-existent class when disabled client class creation', (done) => { - const customConfig = Object.assign({}, config, {allowClientClassCreation: false}); - rest.create(customConfig, auth.nobody(customConfig), 'ClientClassCreation', {}) - .then(() => { - fail('Should throw an error'); - done(); - }, (err) => { - expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); - expect(err.message).toEqual('This user is not allowed to access ' + - 'non-existent class: ClientClassCreation'); - done(); - }); + it('handles create on non-existent class when disabled client class creation', done => { + const customConfig = Object.assign({}, config, { + allowClientClassCreation: false, + }); + rest + .create( + customConfig, + auth.nobody(customConfig), + 'ClientClassCreation', + {} + ) + .then( + () => { + fail('Should throw an error'); + done(); + }, + err => { + expect(err.code).toEqual(Parse.Error.OPERATION_FORBIDDEN); + expect(err.message).toEqual( + 'This user is not allowed to access ' + + 'non-existent class: ClientClassCreation' + ); + done(); + } + ); }); - it('handles create on existent class when disabled client class creation', (done) => { - const customConfig = Object.assign({}, config, {allowClientClassCreation: false}); - config.database.loadSchema() + it('handles create on existent class when disabled client class creation', done => { + const 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', {}); + return rest.create( + customConfig, + auth.nobody(customConfig), + 'ClientClassCreation', + {} + ); }) - .then(() => { - done(); - }, () => { - fail('Should not throw error') - }); + .then( + () => { + done(); + }, + () => { + fail('Should not throw error'); + } + ); }); - it('handles user signup', (done) => { + it('handles user signup', done => { const user = { username: 'asdf', password: 'zxcv', foo: 'bar', }; - 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'); - done(); - }); + 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'); + done(); + }); }); - it('handles anonymous user signup', (done) => { + it('handles anonymous user signup', done => { const data1 = { authData: { anonymous: { - id: '00000000-0000-0000-0000-000000000001' - } - } + id: '00000000-0000-0000-0000-000000000001', + }, + }, }; const data2 = { authData: { anonymous: { - id: '00000000-0000-0000-0000-000000000002' - } - } + id: '00000000-0000-0000-0000-000000000002', + }, + }, }; let username1; - rest.create(config, auth.nobody(config), '_User', data1) - .then((r) => { + rest + .create(config, auth.nobody(config), '_User', data1) + .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); expect(typeof r.response.username).toEqual('string'); return rest.create(config, auth.nobody(config), '_User', data1); - }).then((r) => { + }) + .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.username).toEqual('string'); expect(typeof r.response.updatedAt).toEqual('string'); username1 = r.response.username; return rest.create(config, auth.nobody(config), '_User', data2); - }).then((r) => { + }) + .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); return rest.create(config, auth.nobody(config), '_User', data2); - }).then((r) => { + }) + .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.username).toEqual('string'); @@ -223,99 +276,123 @@ describe('rest create', () => { }); }); - it('handles anonymous user signup and upgrade to new user', (done) => { + it('handles anonymous user signup and upgrade to new user', done => { const data1 = { authData: { anonymous: { - id: '00000000-0000-0000-0000-000000000001' - } - } + id: '00000000-0000-0000-0000-000000000001', + }, + }, }; const updatedData = { authData: { anonymous: null }, username: 'hello', - password: 'world' - } + password: 'world', + }; let objectId; - rest.create(config, auth.nobody(config), '_User', data1) - .then((r) => { + rest + .create(config, auth.nobody(config), '_User', data1) + .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); objectId = r.response.objectId; - return auth.getAuthForSessionToken({config, sessionToken: r.response.sessionToken }) - }).then((sessionAuth) => { - return rest.update(config, sessionAuth, '_User', { objectId }, updatedData); - }).then(() => { + return auth.getAuthForSessionToken({ + config, + sessionToken: r.response.sessionToken, + }); + }) + .then(sessionAuth => { + return rest.update( + config, + sessionAuth, + '_User', + { objectId }, + updatedData + ); + }) + .then(() => { return Parse.User.logOut().then(() => { return Parse.User.logIn('hello', 'world'); - }) - }).then((r) => { + }); + }) + .then(r => { expect(r.id).toEqual(objectId); expect(r.get('username')).toEqual('hello'); done(); - }).catch((err) => { + }) + .catch(err => { jfail(err); done(); - }) + }); }); - it('handles no anonymous users config', (done) => { + it('handles no anonymous users config', done => { const NoAnnonConfig = Object.assign({}, config); NoAnnonConfig.authDataManager.setEnableAnonymousUsers(false); const data1 = { authData: { anonymous: { - id: '00000000-0000-0000-0000-000000000001' - } - } + id: '00000000-0000-0000-0000-000000000001', + }, + }, }; - rest.create(NoAnnonConfig, auth.nobody(NoAnnonConfig), '_User', data1).then(() => { - fail("Should throw an error"); - done(); - }, (err) => { - expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE); - expect(err.message).toEqual('This authentication method is unsupported.'); - NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true); - done(); - }) + rest.create(NoAnnonConfig, auth.nobody(NoAnnonConfig), '_User', data1).then( + () => { + fail('Should throw an error'); + done(); + }, + err => { + expect(err.code).toEqual(Parse.Error.UNSUPPORTED_SERVICE); + expect(err.message).toEqual( + 'This authentication method is unsupported.' + ); + NoAnnonConfig.authDataManager.setEnableAnonymousUsers(true); + done(); + } + ); }); - it('test facebook signup and login', (done) => { + it('test facebook signup and login', done => { const data = { authData: { facebook: { id: '8675309', - access_token: 'jenny' - } - } + access_token: 'jenny', + }, + }, }; let newUserSignedUpByFacebookObjectId; - rest.create(config, auth.nobody(config), '_User', data) - .then((r) => { + rest + .create(config, auth.nobody(config), '_User', data) + .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.sessionToken).toEqual('string'); newUserSignedUpByFacebookObjectId = r.response.objectId; return rest.create(config, auth.nobody(config), '_User', data); - }).then((r) => { + }) + .then(r => { expect(typeof r.response.objectId).toEqual('string'); expect(typeof r.response.createdAt).toEqual('string'); expect(typeof r.response.username).toEqual('string'); expect(typeof r.response.updatedAt).toEqual('string'); expect(r.response.objectId).toEqual(newUserSignedUpByFacebookObjectId); - return rest.find(config, auth.master(config), - '_Session', {sessionToken: r.response.sessionToken}); - }).then((response) => { + return rest.find(config, auth.master(config), '_Session', { + sessionToken: r.response.sessionToken, + }); + }) + .then(response => { expect(response.results.length).toEqual(1); const output = response.results[0]; expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId); done(); - }).catch(err => { + }) + .catch(err => { jfail(err); done(); - }) + }); }); it('stores pointers', done => { @@ -324,14 +401,24 @@ describe('rest create', () => { aPointer: { __type: 'Pointer', className: 'JustThePointer', - objectId: 'qwerty1234' // make it 10 chars to match PG storage - } + objectId: 'qwerty1234', // make it 10 chars to match PG storage + }, }; - rest.create(config, auth.nobody(config), 'APointerDarkly', obj) - .then(() => database.adapter.find('APointerDarkly', { fields: { - foo: { type: 'String' }, - aPointer: { type: 'Pointer', targetClass: 'JustThePointer' }, - }}, {}, {})) + rest + .create(config, auth.nobody(config), 'APointerDarkly', obj) + .then(() => + database.adapter.find( + 'APointerDarkly', + { + fields: { + foo: { type: 'String' }, + aPointer: { type: 'Pointer', targetClass: 'JustThePointer' }, + }, + }, + {}, + {} + ) + ) .then(results => { expect(results.length).toEqual(1); const output = results[0]; @@ -341,34 +428,37 @@ describe('rest create', () => { expect(output.aPointer).toEqual({ __type: 'Pointer', className: 'JustThePointer', - objectId: 'qwerty1234' + objectId: 'qwerty1234', }); done(); }); }); - it("cannot set objectId", (done) => { + it('cannot set objectId', done => { const headers = { 'Content-Type': 'application/octet-stream', 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest' + 'X-Parse-REST-API-Key': 'rest', }; - request.post({ - headers: headers, - url: 'http://localhost:8378/1/classes/TestObject', - body: JSON.stringify({ - 'foo': 'bar', - 'objectId': 'hello' - }) - }, (error, response, body) => { - const b = JSON.parse(body); - expect(b.code).toEqual(105); - expect(b.error).toEqual('objectId is an invalid field name.'); - done(); - }); + request.post( + { + headers: headers, + url: 'http://localhost:8378/1/classes/TestObject', + body: JSON.stringify({ + foo: 'bar', + objectId: 'hello', + }), + }, + (error, response, body) => { + const b = JSON.parse(body); + expect(b.code).toEqual(105); + expect(b.error).toEqual('objectId is an invalid field name.'); + done(); + } + ); }); - it("test default session length", (done) => { + it('test default session length', done => { const user = { username: 'asdf', password: 'zxcv', @@ -376,21 +466,23 @@ describe('rest create', () => { }; const now = new Date(); - rest.create(config, auth.nobody(config), '_User', user) - .then((r) => { + 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}); + return rest.find(config, auth.master(config), '_Session', { + sessionToken: r.response.sessionToken, + }); }) - .then((r) => { + .then(r => { expect(r.results.length).toEqual(1); const session = r.results[0]; const actual = new Date(session.expiresAt.iso); - const expected = new Date(now.getTime() + (1000 * 3600 * 24 * 365)); + const expected = new Date(now.getTime() + 1000 * 3600 * 24 * 365); expect(actual.getFullYear()).toEqual(expected.getFullYear()); expect(actual.getMonth()).toEqual(expected.getMonth()); @@ -402,7 +494,7 @@ describe('rest create', () => { }); }); - it("test specified session length", (done) => { + it('test specified session length', done => { const user = { username: 'asdf', password: 'zxcv', @@ -412,21 +504,23 @@ describe('rest create', () => { now = new Date(); // For reference later config.sessionLength = sessionLength; - rest.create(config, auth.nobody(config), '_User', user) - .then((r) => { + 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}); + return rest.find(config, auth.master(config), '_Session', { + sessionToken: r.response.sessionToken, + }); }) - .then((r) => { + .then(r => { expect(r.results.length).toEqual(1); const session = r.results[0]; const actual = new Date(session.expiresAt.iso); - const expected = new Date(now.getTime() + (sessionLength * 1000)); + const expected = new Date(now.getTime() + sessionLength * 1000); expect(actual.getFullYear()).toEqual(expected.getFullYear()); expect(actual.getMonth()).toEqual(expected.getMonth()); @@ -435,141 +529,154 @@ describe('rest create', () => { expect(actual.getMinutes()).toEqual(expected.getMinutes()); done(); - }).catch(err => { + }) + .catch(err => { jfail(err); done(); }); }); - it("can create a session with no expiration", (done) => { + it('can create a session with no expiration', done => { const user = { username: 'asdf', password: 'zxcv', - foo: 'bar' + foo: 'bar', }; config.expireInactiveSessions = false; - rest.create(config, auth.nobody(config), '_User', user) - .then((r) => { + 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}); + return rest.find(config, auth.master(config), '_Session', { + sessionToken: r.response.sessionToken, + }); }) - .then((r) => { + .then(r => { expect(r.results.length).toEqual(1); const session = r.results[0]; expect(session.expiresAt).toBeUndefined(); done(); - }).catch(err => { + }) + .catch(err => { console.error(err); fail(err); done(); - }) + }); }); - it("can create object in volatileClasses if masterKey", (done) =>{ - rest.create(config, auth.master(config), '_PushStatus', {}) - .then((r) => { + it('can create object in volatileClasses if masterKey', done => { + rest + .create(config, auth.master(config), '_PushStatus', {}) + .then(r => { expect(r.response.objectId.length).toBe(10); }) .then(() => { - rest.create(config, auth.master(config), '_JobStatus', {}) - .then((r) => { - expect(r.response.objectId.length).toBe(10); - done(); - }) - }) - + rest.create(config, auth.master(config), '_JobStatus', {}).then(r => { + expect(r.response.objectId.length).toBe(10); + done(); + }); + }); }); - it("cannot create object in volatileClasses if not masterKey", (done) =>{ + it('cannot create object in volatileClasses if not masterKey', done => { Promise.resolve() .then(() => { - return rest.create(config, auth.nobody(config), '_PushStatus', {}) + return rest.create(config, auth.nobody(config), '_PushStatus', {}); }) - .then((r) => { + .then(r => { console.log(r); }) - .catch((error) => { + .catch(error => { expect(error.code).toEqual(119); done(); - }) + }); }); - it('locks down session', (done) => { + it('locks down session', done => { let currentUser; - Parse.User.signUp('foo', 'bar').then((user) => { - currentUser = user; - const sessionToken = user.getSessionToken(); - const headers = { - 'Content-Type': 'application/octet-stream', - 'X-Parse-Application-Id': 'test', - 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Session-Token': sessionToken, - }; - let sessionId; - return rp.get({ - headers: headers, - url: 'http://localhost:8378/1/sessions/me', - json: true, - }).then(body => { - sessionId = body.objectId; - return rp.put({ - headers, - url: 'http://localhost:8378/1/sessions/' + sessionId, - json: { - installationId: 'yolo' - } - }) - }).then(done.fail, (res) => { - expect(res.statusCode).toBe(400); - expect(res.error.code).toBe(105); - return rp.put({ - headers, - url: 'http://localhost:8378/1/sessions/' + sessionId, - json: { - sessionToken: 'yolo' - } - }) - }).then(done.fail, (res) => { - expect(res.statusCode).toBe(400); - expect(res.error.code).toBe(105); - return Parse.User.signUp('other', 'user'); - }).then((otherUser) => { - const user = new Parse.User(); - user.id = otherUser.id; - return rp.put({ - headers, - url: 'http://localhost:8378/1/sessions/' + sessionId, - json: { - user: Parse._encode(user) - } - }) - }).then(done.fail, (res) => { - expect(res.statusCode).toBe(400); - expect(res.error.code).toBe(105); - const user = new Parse.User(); - user.id = currentUser.id; - return rp.put({ - headers, - url: 'http://localhost:8378/1/sessions/' + sessionId, - json: { - user: Parse._encode(user) - } - }) - }).then(done).catch(done.fail); - }).catch(done.fail); + Parse.User.signUp('foo', 'bar') + .then(user => { + currentUser = user; + const sessionToken = user.getSessionToken(); + const headers = { + 'Content-Type': 'application/octet-stream', + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Session-Token': sessionToken, + }; + let sessionId; + return rp + .get({ + headers: headers, + url: 'http://localhost:8378/1/sessions/me', + json: true, + }) + .then(body => { + sessionId = body.objectId; + return rp.put({ + headers, + url: 'http://localhost:8378/1/sessions/' + sessionId, + json: { + installationId: 'yolo', + }, + }); + }) + .then(done.fail, res => { + expect(res.statusCode).toBe(400); + expect(res.error.code).toBe(105); + return rp.put({ + headers, + url: 'http://localhost:8378/1/sessions/' + sessionId, + json: { + sessionToken: 'yolo', + }, + }); + }) + .then(done.fail, res => { + expect(res.statusCode).toBe(400); + expect(res.error.code).toBe(105); + return Parse.User.signUp('other', 'user'); + }) + .then(otherUser => { + const user = new Parse.User(); + user.id = otherUser.id; + return rp.put({ + headers, + url: 'http://localhost:8378/1/sessions/' + sessionId, + json: { + user: Parse._encode(user), + }, + }); + }) + .then(done.fail, res => { + expect(res.statusCode).toBe(400); + expect(res.error.code).toBe(105); + const user = new Parse.User(); + user.id = currentUser.id; + return rp.put({ + headers, + url: 'http://localhost:8378/1/sessions/' + sessionId, + json: { + user: Parse._encode(user), + }, + }); + }) + .then(done) + .catch(done.fail); + }) + .catch(done.fail); }); - it ('sets current user in new sessions', (done) => { + it('sets current user in new sessions', done => { let currentUser; Parse.User.signUp('foo', 'bar') - .then((user) => { + .then(user => { currentUser = user; const sessionToken = user.getSessionToken(); const headers = { @@ -581,10 +688,12 @@ describe('rest create', () => { headers, url: 'http://localhost:8378/1/sessions', json: true, - body: { 'user': { '__type': 'Pointer', 'className':'_User', 'objectId': 'fakeId' } }, - }) + body: { + user: { __type: 'Pointer', className: '_User', objectId: 'fakeId' }, + }, + }); }) - .then((body) => { + .then(body => { if (body.user.objectId === currentUser.id) { return done(); } else { @@ -592,34 +701,40 @@ describe('rest create', () => { } }) .catch(done.fail); - }) + }); }); describe('rest update', () => { - it('ignores createdAt', done => { const config = Config.get('test'); const nobody = auth.nobody(config); const className = 'Foo'; const newCreatedAt = new Date('1970-01-01T00:00:00.000Z'); - rest.create(config, nobody, className, {}).then(res => { - const objectId = res.response.objectId; - const restObject = { - createdAt: {__type: "Date", iso: newCreatedAt}, // should be ignored - }; - - return rest.update(config, nobody, className, { objectId }, restObject).then(() => { - const restWhere = { - objectId: objectId, + rest + .create(config, nobody, className, {}) + .then(res => { + const objectId = res.response.objectId; + const restObject = { + createdAt: { __type: 'Date', iso: newCreatedAt }, // should be ignored }; - return rest.find(config, nobody, className, restWhere, {}); - }); - }).then(res2 => { - const updatedObject = res2.results[0]; - expect(new Date(updatedObject.createdAt)).not.toEqual(newCreatedAt); - done(); - }).then(done).catch(done.fail); + + return rest + .update(config, nobody, className, { objectId }, restObject) + .then(() => { + const restWhere = { + objectId: objectId, + }; + return rest.find(config, nobody, className, restWhere, {}); + }); + }) + .then(res2 => { + const updatedObject = res2.results[0]; + expect(new Date(updatedObject.createdAt)).not.toEqual(newCreatedAt); + done(); + }) + .then(done) + .catch(done.fail); }); }); @@ -628,132 +743,181 @@ describe('read-only masterKey', () => { const config = Config.get('test'); const readOnly = auth.readOnly(config); expect(() => { - rest.create(config, readOnly, 'AnObject', {}) - }).toThrow(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `read-only masterKey isn't allowed to perform the create operation.`)); + rest.create(config, readOnly, 'AnObject', {}); + }).toThrow( + new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + `read-only masterKey isn't allowed to perform the create operation.` + ) + ); expect(() => { - rest.update(config, readOnly, 'AnObject', {}) + rest.update(config, readOnly, 'AnObject', {}); }).toThrow(); expect(() => { - rest.del(config, readOnly, 'AnObject', {}) + rest.del(config, readOnly, 'AnObject', {}); }).toThrow(); }); - it('properly blocks writes', (done) => { + it('properly blocks writes', done => { reconfigureServer({ - readOnlyMasterKey: 'yolo-read-only' - }).then(() => { - return rp.post(`${Parse.serverURL}/classes/MyYolo`, { - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': 'yolo-read-only', - }, - json: { foo: 'bar' } + readOnlyMasterKey: 'yolo-read-only', + }) + .then(() => { + return rp.post(`${Parse.serverURL}/classes/MyYolo`, { + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'yolo-read-only', + }, + json: { foo: 'bar' }, + }); + }) + .then(done.fail) + .catch(res => { + expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.error.error).toBe( + "read-only masterKey isn't allowed to perform the create operation." + ); + done(); }); - }).then(done.fail).catch((res) => { - expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.error.error).toBe('read-only masterKey isn\'t allowed to perform the create operation.'); - done(); - }); }); - it('should throw when masterKey and readOnlyMasterKey are the same', (done) => { + it('should throw when masterKey and readOnlyMasterKey are the same', done => { reconfigureServer({ masterKey: 'yolo', - readOnlyMasterKey: 'yolo' - }).then(done.fail).catch((err) => { - expect(err).toEqual(new Error('masterKey and readOnlyMasterKey should be different')); - done(); - }); + readOnlyMasterKey: 'yolo', + }) + .then(done.fail) + .catch(err => { + expect(err).toEqual( + new Error('masterKey and readOnlyMasterKey should be different') + ); + done(); + }); }); it('should throw when trying to create RestWrite', () => { const config = Config.get('test'); expect(() => { new RestWrite(config, auth.readOnly(config)); - }).toThrow(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Cannot perform a write operation when using readOnlyMasterKey')); + }).toThrow( + new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + 'Cannot perform a write operation when using readOnlyMasterKey' + ) + ); }); - it('should throw when trying to create schema', (done) => { - return rp.post(`${Parse.serverURL}/schemas`, { - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': 'read-only-test', - }, - json: {} - }).then(done.fail).catch((res) => { - expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.error.error).toBe('read-only masterKey isn\'t allowed to create a schema.'); - done(); - }); + it('should throw when trying to create schema', done => { + return rp + .post(`${Parse.serverURL}/schemas`, { + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + }, + json: {}, + }) + .then(done.fail) + .catch(res => { + expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.error.error).toBe( + "read-only masterKey isn't allowed to create a schema." + ); + done(); + }); }); - it('should throw when trying to create schema with a name', (done) => { - return rp.post(`${Parse.serverURL}/schemas/MyClass`, { - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': 'read-only-test', - }, - json: {} - }).then(done.fail).catch((res) => { - expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.error.error).toBe('read-only masterKey isn\'t allowed to create a schema.'); - done(); - }); + it('should throw when trying to create schema with a name', done => { + return rp + .post(`${Parse.serverURL}/schemas/MyClass`, { + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + }, + json: {}, + }) + .then(done.fail) + .catch(res => { + expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.error.error).toBe( + "read-only masterKey isn't allowed to create a schema." + ); + done(); + }); }); - it('should throw when trying to update schema', (done) => { - return rp.put(`${Parse.serverURL}/schemas/MyClass`, { - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': 'read-only-test', - }, - json: {} - }).then(done.fail).catch((res) => { - expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.error.error).toBe('read-only masterKey isn\'t allowed to update a schema.'); - done(); - }); + it('should throw when trying to update schema', done => { + return rp + .put(`${Parse.serverURL}/schemas/MyClass`, { + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + }, + json: {}, + }) + .then(done.fail) + .catch(res => { + expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.error.error).toBe( + "read-only masterKey isn't allowed to update a schema." + ); + done(); + }); }); - it('should throw when trying to delete schema', (done) => { - return rp.del(`${Parse.serverURL}/schemas/MyClass`, { - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': 'read-only-test', - }, - json: {} - }).then(done.fail).catch((res) => { - expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.error.error).toBe('read-only masterKey isn\'t allowed to delete a schema.'); - done(); - }); + it('should throw when trying to delete schema', done => { + return rp + .del(`${Parse.serverURL}/schemas/MyClass`, { + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + }, + json: {}, + }) + .then(done.fail) + .catch(res => { + expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.error.error).toBe( + "read-only masterKey isn't allowed to delete a schema." + ); + done(); + }); }); - it('should throw when trying to update the global config', (done) => { - return rp.put(`${Parse.serverURL}/config`, { - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': 'read-only-test', - }, - json: {} - }).then(done.fail).catch((res) => { - expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.error.error).toBe('read-only masterKey isn\'t allowed to update the config.'); - done(); - }); + it('should throw when trying to update the global config', done => { + return rp + .put(`${Parse.serverURL}/config`, { + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + }, + json: {}, + }) + .then(done.fail) + .catch(res => { + expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.error.error).toBe( + "read-only masterKey isn't allowed to update the config." + ); + done(); + }); }); - it('should throw when trying to send push', (done) => { - return rp.post(`${Parse.serverURL}/push`, { - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Master-Key': 'read-only-test', - }, - json: {} - }).then(done.fail).catch((res) => { - expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); - expect(res.error.error).toBe('read-only masterKey isn\'t allowed to send push notifications.'); - done(); - }); + it('should throw when trying to send push', done => { + return rp + .post(`${Parse.serverURL}/push`, { + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': 'read-only-test', + }, + json: {}, + }) + .then(done.fail) + .catch(res => { + expect(res.error.code).toBe(Parse.Error.OPERATION_FORBIDDEN); + expect(res.error.error).toBe( + "read-only masterKey isn't allowed to send push notifications." + ); + done(); + }); }); }); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index dacce31b4b..3826ce2fb5 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -14,10 +14,13 @@ const hasAllPODobject = () => { obj.set('aString', 'string'); obj.set('aBool', true); obj.set('aDate', new Date()); - obj.set('aObject', {k1: 'value', k2: true, k3: 5}); + obj.set('aObject', { k1: 'value', k2: true, k3: 5 }); obj.set('aArray', ['contents', true, 5]); - obj.set('aGeoPoint', new Parse.GeoPoint({latitude: 0, longitude: 0})); - obj.set('aFile', new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' })); + obj.set('aGeoPoint', new Parse.GeoPoint({ latitude: 0, longitude: 0 })); + obj.set( + 'aFile', + new Parse.File('f.txt', { base64: 'V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=' }) + ); const objACL = new Parse.ACL(); objACL.setPublicWriteAccess(false); obj.setACL(objACL); @@ -26,54 +29,54 @@ const hasAllPODobject = () => { const defaultClassLevelPermissions = { find: { - '*': true + '*': true, }, create: { - '*': true + '*': true, }, get: { - '*': true + '*': true, }, update: { - '*': true + '*': true, }, addField: { - '*': true + '*': true, }, delete: { - '*': true - } -} + '*': true, + }, +}; const plainOldDataSchema = { className: 'HasAllPOD', fields: { //Default fields - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, //Custom fields - aNumber: {type: 'Number'}, - aString: {type: 'String'}, - aBool: {type: 'Boolean'}, - aDate: {type: 'Date'}, - aObject: {type: 'Object'}, - aArray: {type: 'Array'}, - aGeoPoint: {type: 'GeoPoint'}, - aFile: {type: 'File'} + aNumber: { type: 'Number' }, + aString: { type: 'String' }, + aBool: { type: 'Boolean' }, + aDate: { type: 'Date' }, + aObject: { type: 'Object' }, + aArray: { type: 'Array' }, + aGeoPoint: { type: 'GeoPoint' }, + aFile: { type: 'File' }, }, - classLevelPermissions: defaultClassLevelPermissions + classLevelPermissions: defaultClassLevelPermissions, }; const pointersAndRelationsSchema = { className: 'HasPointersAndRelations', fields: { //Default fields - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, //Custom fields aPointer: { type: 'Pointer', @@ -84,38 +87,38 @@ const pointersAndRelationsSchema = { targetClass: 'HasAllPOD', }, }, - classLevelPermissions: defaultClassLevelPermissions -} + 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"}, - "authData": {"type": "Object"} + 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' }, + authData: { type: 'Object' }, }, - "classLevelPermissions": defaultClassLevelPermissions, -} + classLevelPermissions: defaultClassLevelPermissions, +}; const roleSchema = { - "className": "_Role", - "fields": { - "objectId": {"type": "String"}, - "createdAt": {"type": "Date"}, - "updatedAt": {"type": "Date"}, - "ACL": {"type": "ACL"}, - "name": {"type":"String"}, - "users": {"type":"Relation", "targetClass":"_User"}, - "roles": {"type":"Relation", "targetClass":"_Role"} + className: '_Role', + fields: { + objectId: { type: 'String' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + ACL: { type: 'ACL' }, + name: { type: 'String' }, + users: { type: 'Relation', targetClass: '_User' }, + roles: { type: 'Relation', targetClass: '_Role' }, }, - "classLevelPermissions": defaultClassLevelPermissions, -} + classLevelPermissions: defaultClassLevelPermissions, +}; const noAuthHeaders = { 'X-Parse-Application-Id': 'test', @@ -140,580 +143,710 @@ describe('schemas', () => { config.database.schemaCache.clear(); }); - it('requires the master key to get all schemas', (done) => { - request.get({ - url: 'http://localhost:8378/1/schemas', - json: true, - headers: noAuthHeaders, - }, (error, response, body) => { - //api.parse.com uses status code 401, but due to the lack of keys - //being necessary in parse-server, 403 makes more sense - expect(response.statusCode).toEqual(403); - expect(body.error).toEqual('unauthorized'); - done(); - }); + it('requires the master key to get all schemas', done => { + request.get( + { + url: 'http://localhost:8378/1/schemas', + json: true, + headers: noAuthHeaders, + }, + (error, response, body) => { + //api.parse.com uses status code 401, but due to the lack of keys + //being necessary in parse-server, 403 makes more sense + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized'); + done(); + } + ); }); - it('requires the master key to get one schema', (done) => { - request.get({ - url: 'http://localhost:8378/1/schemas/SomeSchema', - json: true, - headers: restKeyHeaders, - }, (error, response, body) => { - expect(response.statusCode).toEqual(403); - expect(body.error).toEqual('unauthorized: master key is required'); - done(); - }); + it('requires the master key to get one schema', done => { + request.get( + { + url: 'http://localhost:8378/1/schemas/SomeSchema', + json: true, + headers: restKeyHeaders, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized: master key is required'); + done(); + } + ); }); - it('asks for the master key if you use the rest key', (done) => { - request.get({ - url: 'http://localhost:8378/1/schemas', - json: true, - headers: restKeyHeaders, - }, (error, response, body) => { - expect(response.statusCode).toEqual(403); - expect(body.error).toEqual('unauthorized: master key is required'); - done(); - }); + it('asks for the master key if you use the rest key', done => { + request.get( + { + url: 'http://localhost:8378/1/schemas', + json: true, + headers: restKeyHeaders, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized: master key is required'); + done(); + } + ); }); it('creates _User schema when server starts', done => { - request.get({ - url: 'http://localhost:8378/1/schemas', - json: true, - headers: masterKeyHeaders, - }, (error, response, body) => { - const expected = { - results: [userSchema,roleSchema] - }; - expect(dd(body.results.sort((s1, s2) => s1.className > s2.className), expected.results.sort((s1, s2) => s1.className > s2.className))).toEqual(undefined); - done(); - }); - }); - - it('responds with a list of schemas after creating objects', done => { - const obj1 = hasAllPODobject(); - obj1.save().then(savedObj1 => { - const obj2 = new Parse.Object('HasPointersAndRelations'); - obj2.set('aPointer', savedObj1); - const relation = obj2.relation('aRelation'); - relation.add(obj1); - return obj2.save(); - }).then(() => { - request.get({ + request.get( + { url: 'http://localhost:8378/1/schemas', json: true, headers: masterKeyHeaders, - }, (error, response, body) => { + }, + (error, response, body) => { const expected = { - results: [userSchema,roleSchema,plainOldDataSchema,pointersAndRelationsSchema] + results: [userSchema, roleSchema], }; - expect(dd(body.results.sort((s1, s2) => s1.className > s2.className), expected.results.sort((s1, s2) => s1.className > s2.className))).toEqual(undefined); + expect( + dd( + body.results.sort((s1, s2) => s1.className > s2.className), + expected.results.sort((s1, s2) => s1.className > s2.className) + ) + ).toEqual(undefined); done(); + } + ); + }); + + it('responds with a list of schemas after creating objects', done => { + const obj1 = hasAllPODobject(); + obj1 + .save() + .then(savedObj1 => { + const obj2 = new Parse.Object('HasPointersAndRelations'); + obj2.set('aPointer', savedObj1); + const relation = obj2.relation('aRelation'); + relation.add(obj1); + return obj2.save(); }) - }); + .then(() => { + request.get( + { + url: 'http://localhost:8378/1/schemas', + json: true, + headers: masterKeyHeaders, + }, + (error, response, body) => { + const expected = { + results: [ + userSchema, + roleSchema, + plainOldDataSchema, + pointersAndRelationsSchema, + ], + }; + expect( + dd( + body.results.sort((s1, s2) => s1.className > s2.className), + expected.results.sort((s1, s2) => s1.className > s2.className) + ) + ).toEqual(undefined); + done(); + } + ); + }); }); it('responds with a single schema', done => { const obj = hasAllPODobject(); obj.save().then(() => { - request.get({ - url: 'http://localhost:8378/1/schemas/HasAllPOD', - json: true, - headers: masterKeyHeaders, - }, (error, response, body) => { - expect(body).toEqual(plainOldDataSchema); - done(); - }); + request.get( + { + url: 'http://localhost:8378/1/schemas/HasAllPOD', + json: true, + headers: masterKeyHeaders, + }, + (error, response, body) => { + expect(body).toEqual(plainOldDataSchema); + done(); + } + ); }); }); it('treats class names case sensitively', done => { const obj = hasAllPODobject(); obj.save().then(() => { - request.get({ - url: 'http://localhost:8378/1/schemas/HASALLPOD', - json: true, - headers: masterKeyHeaders, - }, (error, response, body) => { - expect(response.statusCode).toEqual(400); - expect(body).toEqual({ - code: 103, - error: 'Class HASALLPOD does not exist.', - }); - done(); - }); + request.get( + { + url: 'http://localhost:8378/1/schemas/HASALLPOD', + json: true, + headers: masterKeyHeaders, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body).toEqual({ + code: 103, + error: 'Class HASALLPOD does not exist.', + }); + done(); + } + ); }); }); it('requires the master key to create a schema', done => { - request.post({ - url: 'http://localhost:8378/1/schemas', - json: true, - headers: noAuthHeaders, - body: { - className: 'MyClass', + request.post( + { + url: 'http://localhost:8378/1/schemas', + json: true, + headers: noAuthHeaders, + body: { + className: 'MyClass', + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized'); + done(); } - }, (error, response, body) => { - expect(response.statusCode).toEqual(403); - expect(body.error).toEqual('unauthorized'); - done(); - }); + ); }); it('sends an error if you use mismatching class names', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/A', - headers: masterKeyHeaders, - json: true, - body: { - className: 'B', + request.post( + { + url: 'http://localhost:8378/1/schemas/A', + headers: masterKeyHeaders, + json: true, + body: { + className: 'B', + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body).toEqual({ + code: Parse.Error.INVALID_CLASS_NAME, + error: 'Class name mismatch between B and A.', + }); + done(); } - }, (error, response, body) => { - expect(response.statusCode).toEqual(400); - expect(body).toEqual({ - code: Parse.Error.INVALID_CLASS_NAME, - error: 'Class name mismatch between B and A.', - }); - done(); - }); + ); }); it('sends an error if you use no class name', done => { - request.post({ - url: 'http://localhost:8378/1/schemas', - headers: masterKeyHeaders, - json: true, - body: {}, - }, (error, response, body) => { - expect(response.statusCode).toEqual(400); - expect(body).toEqual({ - code: 135, - error: 'POST /schemas needs a class name.', - }); - done(); - }) + request.post( + { + url: 'http://localhost:8378/1/schemas', + headers: masterKeyHeaders, + json: true, + body: {}, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body).toEqual({ + code: 135, + error: 'POST /schemas needs a class name.', + }); + done(); + } + ); }); it('sends an error if you try to create the same class twice', done => { - request.post({ - url: 'http://localhost:8378/1/schemas', - headers: masterKeyHeaders, - json: true, - body: { - className: 'A', - }, - }, (error) => { - expect(error).toEqual(null); - request.post({ + request.post( + { url: 'http://localhost:8378/1/schemas', headers: masterKeyHeaders, json: true, body: { className: 'A', - } - }, (error, response, body) => { - expect(response.statusCode).toEqual(400); - expect(body).toEqual({ - code: Parse.Error.INVALID_CLASS_NAME, - error: 'Class A already exists.' - }); - done(); - }); - }); + }, + }, + error => { + expect(error).toEqual(null); + request.post( + { + url: 'http://localhost:8378/1/schemas', + headers: masterKeyHeaders, + json: true, + body: { + className: 'A', + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body).toEqual({ + code: Parse.Error.INVALID_CLASS_NAME, + error: 'Class A already exists.', + }); + done(); + } + ); + } + ); }); it('responds with all fields when you create a class', done => { - request.post({ - url: 'http://localhost:8378/1/schemas', - headers: masterKeyHeaders, - json: true, - body: { - className: "NewClass", - fields: { - foo: {type: 'Number'}, - ptr: {type: 'Pointer', targetClass: 'SomeClass'} - } - } - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - foo: {type: 'Number'}, - ptr: {type: 'Pointer', targetClass: 'SomeClass'}, + request.post( + { + url: 'http://localhost:8378/1/schemas', + headers: masterKeyHeaders, + json: true, + body: { + className: 'NewClass', + fields: { + foo: { type: 'Number' }, + ptr: { type: 'Pointer', targetClass: 'SomeClass' }, + }, }, - classLevelPermissions: defaultClassLevelPermissions - }); - done(); - }); + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + foo: { type: 'Number' }, + ptr: { type: 'Pointer', targetClass: 'SomeClass' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }); + done(); + } + ); }); it('responds with all fields when getting incomplete schema', done => { - config.database.loadSchema() - .then(schemaController => schemaController.addClassIfNotExists('_Installation', {}, defaultClassLevelPermissions)) + 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(); - }); + 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)) + fail(JSON.stringify(error)); done(); }); }); it('lets you specify class name in both places', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: { - className: "NewClass", - } - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, + request.post( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + className: 'NewClass', }, - classLevelPermissions: defaultClassLevelPermissions - }); - done(); - }); + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }); + done(); + } + ); }); it('requires the master key to modify schemas', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', - headers: noAuthHeaders, + headers: masterKeyHeaders, json: true, body: {}, - }, (error, response, body) => { - expect(response.statusCode).toEqual(403); - expect(body.error).toEqual('unauthorized'); - done(); - }); - }); + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: noAuthHeaders, + json: true, + body: {}, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized'); + done(); + } + ); + } + ); }); it('rejects class name mis-matches in put', done => { - request.put({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {className: 'WrongClassName'} - }, (error, response, body) => { - expect(response.statusCode).toEqual(400); - expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(body.error).toEqual('Class name mismatch between WrongClassName and NewClass.'); - done(); - }); + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { className: 'WrongClassName' }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(body.error).toEqual( + 'Class name mismatch between WrongClassName and NewClass.' + ); + done(); + } + ); }); it('refuses to add fields to non-existent classes', done => { - request.put({ - url: 'http://localhost:8378/1/schemas/NoClass', - headers: masterKeyHeaders, - json: true, - body: { - fields: { - newField: {type: 'String'} - } + request.put( + { + url: 'http://localhost:8378/1/schemas/NoClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newField: { type: 'String' }, + }, + }, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(body.error).toEqual('Class NoClass does not exist.'); + done(); } - }, (error, response, body) => { - expect(response.statusCode).toEqual(400); - expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(body.error).toEqual('Class NoClass does not exist.'); - done(); - }); + ); }); it('refuses to put to existing fields, even if it would not be a change', done => { const obj = hasAllPODobject(); - obj.save() - .then(() => { - request.put({ + obj.save().then(() => { + request.put( + { url: 'http://localhost:8378/1/schemas/HasAllPOD', headers: masterKeyHeaders, json: true, body: { fields: { - aString: {type: 'String'} - } - } - }, (error, response, body) => { + aString: { type: 'String' }, + }, + }, + }, + (error, response, body) => { expect(response.statusCode).toEqual(400); expect(body.code).toEqual(255); expect(body.error).toEqual('Field aString exists, cannot update.'); done(); - }); - }) + } + ); + }); }); it('refuses to delete non-existent fields', done => { const obj = hasAllPODobject(); - obj.save() - .then(() => { - request.put({ + obj.save().then(() => { + request.put( + { url: 'http://localhost:8378/1/schemas/HasAllPOD', headers: masterKeyHeaders, json: true, body: { fields: { - nonExistentKey: {__op: "Delete"}, - } - } - }, (error, response, body) => { + nonExistentKey: { __op: 'Delete' }, + }, + }, + }, + (error, response, body) => { expect(response.statusCode).toEqual(400); expect(body.code).toEqual(255); - expect(body.error).toEqual('Field nonExistentKey does not exist, cannot delete.'); + expect(body.error).toEqual( + 'Field nonExistentKey does not exist, cannot delete.' + ); done(); - }); - }); + } + ); + }); }); it('refuses to add a geopoint to a class that already has one', done => { const obj = hasAllPODobject(); - obj.save() - .then(() => { - request.put({ + obj.save().then(() => { + request.put( + { url: 'http://localhost:8378/1/schemas/HasAllPOD', headers: masterKeyHeaders, json: true, body: { fields: { - newGeo: {type: 'GeoPoint'} - } - } - }, (error, response, body) => { + newGeo: { type: 'GeoPoint' }, + }, + }, + }, + (error, response, body) => { expect(response.statusCode).toEqual(400); expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE); - expect(body.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding newGeo when aGeoPoint already exists.'); + expect(body.error).toEqual( + 'currently, only one GeoPoint field may exist in an object. Adding newGeo when aGeoPoint already exists.' + ); done(); - }); - }); + } + ); + }); }); it('refuses to add two geopoints', done => { const obj = new Parse.Object('NewClass'); obj.set('aString', 'aString'); - obj.save() - .then(() => { - request.put({ + obj.save().then(() => { + request.put( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, body: { fields: { - newGeo1: {type: 'GeoPoint'}, - newGeo2: {type: 'GeoPoint'}, - } - } - }, (error, response, body) => { + newGeo1: { type: 'GeoPoint' }, + newGeo2: { type: 'GeoPoint' }, + }, + }, + }, + (error, response, body) => { expect(response.statusCode).toEqual(400); expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE); - expect(body.error).toEqual('currently, only one GeoPoint field may exist in an object. Adding newGeo2 when newGeo1 already exists.'); + expect(body.error).toEqual( + 'currently, only one GeoPoint field may exist in an object. Adding newGeo2 when newGeo1 already exists.' + ); done(); - }); - }); + } + ); + }); }); it('allows you to delete and add a geopoint in the same request', done => { const obj = new Parse.Object('NewClass'); - obj.set('geo1', new Parse.GeoPoint({latitude: 0, longitude: 0})); - obj.save() - .then(() => { - request.put({ + obj.set('geo1', new Parse.GeoPoint({ latitude: 0, longitude: 0 })); + obj.save().then(() => { + request.put( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, body: { fields: { - geo2: {type: 'GeoPoint'}, - geo1: {__op: 'Delete'} - } - } - }, (error, response, body) => { - expect(dd(body, { - "className": "NewClass", - "fields": { - "ACL": {"type": "ACL"}, - "createdAt": {"type": "Date"}, - "objectId": {"type": "String"}, - "updatedAt": {"type": "Date"}, - "geo2": {"type": "GeoPoint"}, + geo2: { type: 'GeoPoint' }, + geo1: { __op: 'Delete' }, }, - classLevelPermissions: defaultClassLevelPermissions - })).toEqual(undefined); + }, + }, + (error, response, body) => { + expect( + dd(body, { + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + objectId: { type: 'String' }, + updatedAt: { type: 'Date' }, + geo2: { type: 'GeoPoint' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }) + ).toEqual(undefined); done(); - }); - }) + } + ); + }); }); it('put with no modifications returns all fields', done => { const obj = hasAllPODobject(); - obj.save() - .then(() => { - request.put({ + obj.save().then(() => { + request.put( + { url: 'http://localhost:8378/1/schemas/HasAllPOD', headers: masterKeyHeaders, json: true, body: {}, - }, (error, response, body) => { + }, + (error, response, body) => { expect(body).toEqual(plainOldDataSchema); done(); - }); - }) + } + ); + }); }); it('lets you add fields', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - fields: { - newField: {type: 'String'} - } - } - }, (error, response, body) => { - expect(dd(body, { - className: 'NewClass', - fields: { - "ACL": {"type": "ACL"}, - "createdAt": {"type": "Date"}, - "objectId": {"type": "String"}, - "updatedAt": {"type": "Date"}, - "newField": {"type": "String"}, - }, - classLevelPermissions: defaultClassLevelPermissions - })).toEqual(undefined); - request.get({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - newField: {type: 'String'}, + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newField: { type: 'String' }, + }, }, - classLevelPermissions: defaultClassLevelPermissions - }); - done(); - }); - }); - }) + }, + (error, response, body) => { + expect( + dd(body, { + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + objectId: { type: 'String' }, + updatedAt: { type: 'Date' }, + newField: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }) + ).toEqual(undefined); + request.get( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + newField: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }); + done(); + } + ); + } + ); + } + ); }); it('lets you add fields to system schema', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/_User', - headers: masterKeyHeaders, - json: true - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/_User', headers: masterKeyHeaders, json: true, - body: { - fields: { - newField: {type: 'String'} - } - } - }, (error, response, body) => { - expect(dd(body,{ - className: '_User', - fields: { - objectId: {type: 'String'}, - updatedAt: {type: 'Date'}, - createdAt: {type: 'Date'}, - username: {type: 'String'}, - password: {type: 'String'}, - email: {type: 'String'}, - emailVerified: {type: 'Boolean'}, - authData: {type: 'Object'}, - 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(dd(body,{ - className: '_User', - fields: { - objectId: {type: 'String'}, - updatedAt: {type: 'Date'}, - createdAt: {type: 'Date'}, - username: {type: 'String'}, - password: {type: 'String'}, - email: {type: 'String'}, - emailVerified: {type: 'Boolean'}, - authData: {type: 'Object'}, - newField: {type: 'String'}, - ACL: {type: 'ACL'} + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/_User', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newField: { type: 'String' }, + }, }, - classLevelPermissions: defaultClassLevelPermissions - })).toBeUndefined(); - done(); - }); - }); - }) + }, + (error, response, body) => { + expect( + dd(body, { + className: '_User', + fields: { + objectId: { type: 'String' }, + updatedAt: { type: 'Date' }, + createdAt: { type: 'Date' }, + username: { type: 'String' }, + password: { type: 'String' }, + email: { type: 'String' }, + emailVerified: { type: 'Boolean' }, + authData: { type: 'Object' }, + 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( + dd(body, { + className: '_User', + fields: { + objectId: { type: 'String' }, + updatedAt: { type: 'Date' }, + createdAt: { type: 'Date' }, + username: { type: 'String' }, + password: { type: 'String' }, + email: { type: 'String' }, + emailVerified: { type: 'Boolean' }, + authData: { type: 'Object' }, + newField: { type: 'String' }, + ACL: { type: 'ACL' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }) + ).toBeUndefined(); + done(); + } + ); + } + ); + } + ); }); it('lets you delete multiple fields and check schema', done => { @@ -725,176 +858,201 @@ describe('schemas', () => { return obj; }; - simpleOneObject().save() + simpleOneObject() + .save() .then(() => { - request.put({ - url: 'http://localhost:8378/1/schemas/SimpleOne', - headers: masterKeyHeaders, - json: true, - body: { - fields: { - aString: {__op: 'Delete'}, - aNumber: {__op: 'Delete'}, - } - } - }, (error, response, body) => { - expect(body).toEqual({ - className: 'SimpleOne', - fields: { - //Default fields - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - //Custom fields - aBool: {type: 'Boolean'}, + request.put( + { + url: 'http://localhost:8378/1/schemas/SimpleOne', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: { __op: 'Delete' }, + aNumber: { __op: 'Delete' }, + }, }, - classLevelPermissions: defaultClassLevelPermissions - }); + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'SimpleOne', + fields: { + //Default fields + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + //Custom fields + aBool: { type: 'Boolean' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }); - done(); - }); + done(); + } + ); }); }); it('lets you delete multiple fields and add fields', done => { const obj1 = hasAllPODobject(); - obj1.save() - .then(() => { - request.put({ + obj1.save().then(() => { + request.put( + { url: 'http://localhost:8378/1/schemas/HasAllPOD', headers: masterKeyHeaders, json: true, body: { fields: { - aString: {__op: 'Delete'}, - aNumber: {__op: 'Delete'}, - aNewString: {type: 'String'}, - aNewNumber: {type: 'Number'}, - aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'}, - aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'}, - } - } - }, (error, response, body) => { + aString: { __op: 'Delete' }, + aNumber: { __op: 'Delete' }, + aNewString: { type: 'String' }, + aNewNumber: { type: 'Number' }, + aNewRelation: { type: 'Relation', targetClass: 'HasAllPOD' }, + aNewPointer: { type: 'Pointer', targetClass: 'HasAllPOD' }, + }, + }, + }, + (error, response, body) => { expect(body).toEqual({ className: 'HasAllPOD', fields: { - //Default fields - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, + //Default fields + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, //Custom fields - aBool: {type: 'Boolean'}, - aDate: {type: 'Date'}, - aObject: {type: 'Object'}, - aArray: {type: 'Array'}, - aGeoPoint: {type: 'GeoPoint'}, - aFile: {type: 'File'}, - aNewNumber: {type: 'Number'}, - aNewString: {type: 'String'}, - aNewPointer: {type: 'Pointer', targetClass: 'HasAllPOD'}, - aNewRelation: {type: 'Relation', targetClass: 'HasAllPOD'}, + aBool: { type: 'Boolean' }, + aDate: { type: 'Date' }, + aObject: { type: 'Object' }, + aArray: { type: 'Array' }, + aGeoPoint: { type: 'GeoPoint' }, + aFile: { type: 'File' }, + aNewNumber: { type: 'Number' }, + aNewString: { type: 'String' }, + aNewPointer: { type: 'Pointer', targetClass: 'HasAllPOD' }, + aNewRelation: { type: 'Relation', targetClass: 'HasAllPOD' }, }, - classLevelPermissions: defaultClassLevelPermissions + classLevelPermissions: defaultClassLevelPermissions, }); const obj2 = new Parse.Object('HasAllPOD'); obj2.set('aNewPointer', obj1); const relation = obj2.relation('aNewRelation'); relation.add(obj1); obj2.save().then(done); //Just need to make sure saving works on the new object. - }); - }); + } + ); + }); }); it('will not delete any fields if the additions are invalid', done => { const obj = hasAllPODobject(); - obj.save() - .then(() => { - request.put({ + obj.save().then(() => { + request.put( + { url: 'http://localhost:8378/1/schemas/HasAllPOD', headers: masterKeyHeaders, json: true, body: { fields: { - fakeNewField: {type: 'fake type'}, - aString: {__op: 'Delete'} - } - } - }, (error, response, body) => { + fakeNewField: { type: 'fake type' }, + aString: { __op: 'Delete' }, + }, + }, + }, + (error, response, body) => { expect(body.code).toEqual(Parse.Error.INCORRECT_TYPE); expect(body.error).toEqual('invalid field type: fake type'); - request.get({ - url: 'http://localhost:8378/1/schemas/HasAllPOD', - headers: masterKeyHeaders, - json: true, - }, (error, response) => { - expect(response.body).toEqual(plainOldDataSchema); - done(); - }); - }); - }); + request.get( + { + url: 'http://localhost:8378/1/schemas/HasAllPOD', + headers: masterKeyHeaders, + json: true, + }, + (error, response) => { + expect(response.body).toEqual(plainOldDataSchema); + done(); + } + ); + } + ); + }); }); it('requires the master key to delete schemas', done => { - request.del({ - url: 'http://localhost:8378/1/schemas/DoesntMatter', - headers: noAuthHeaders, - json: true, - }, (error, response, body) => { - expect(response.statusCode).toEqual(403); - expect(body.error).toEqual('unauthorized'); - done(); - }); + request.del( + { + url: 'http://localhost:8378/1/schemas/DoesntMatter', + headers: noAuthHeaders, + json: true, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(403); + expect(body.error).toEqual('unauthorized'); + done(); + } + ); }); it('refuses to delete non-empty collection', done => { const obj = hasAllPODobject(); - obj.save() - .then(() => { - request.del({ + obj.save().then(() => { + request.del( + { url: 'http://localhost:8378/1/schemas/HasAllPOD', headers: masterKeyHeaders, json: true, - }, (error, response, body) => { + }, + (error, response, body) => { expect(response.statusCode).toEqual(400); expect(body.code).toEqual(255); expect(body.error).toMatch(/HasAllPOD/); expect(body.error).toMatch(/contains 1/); done(); - }); - }); + } + ); + }); }); it('fails when deleting collections with invalid class names', done => { - request.del({ - url: 'http://localhost:8378/1/schemas/_GlobalConfig', - headers: masterKeyHeaders, - json: true, - }, (error, response, body) => { - expect(response.statusCode).toEqual(400); - expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(body.error).toEqual('Invalid classname: _GlobalConfig, classnames can only have alphanumeric characters and _, and must start with an alpha character '); - done(); - }) + request.del( + { + url: 'http://localhost:8378/1/schemas/_GlobalConfig', + headers: masterKeyHeaders, + json: true, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(body.error).toEqual( + 'Invalid classname: _GlobalConfig, classnames can only have alphanumeric characters and _, and must start with an alpha character ' + ); + done(); + } + ); }); it('does not fail when deleting nonexistant collections', done => { - request.del({ - url: 'http://localhost:8378/1/schemas/Missing', - headers: masterKeyHeaders, - json: true, - }, (error, response, body) => { - expect(response.statusCode).toEqual(200); - expect(body).toEqual({}); - done(); - }); + request.del( + { + url: 'http://localhost:8378/1/schemas/Missing', + headers: masterKeyHeaders, + json: true, + }, + (error, response, body) => { + expect(response.statusCode).toEqual(200); + expect(body).toEqual({}); + done(); + } + ); }); it('deletes collections including join tables', done => { const obj = new Parse.Object('MyClass'); obj.set('data', 'data'); - obj.save() + obj + .save() .then(() => { const obj2 = new Parse.Object('MyOtherClass'); const relation = obj2.relation('aRelation'); @@ -903,102 +1061,80 @@ describe('schemas', () => { }) .then(obj2 => obj2.destroy()) .then(() => { - request.del({ - url: 'http://localhost:8378/1/schemas/MyOtherClass', - headers: masterKeyHeaders, - json: true, - }, (error, response) => { - expect(response.statusCode).toEqual(200); - expect(response.body).toEqual({}); - config.database.collectionExists('_Join:aRelation:MyOtherClass').then(exists => { - if (exists) { - fail('Relation collection should be deleted.'); - done(); - } - return config.database.collectionExists('MyOtherClass'); - }).then(exists => { - if (exists) { - fail('Class collection should be deleted.'); - done(); - } - }).then(() => { - request.get({ - url: 'http://localhost:8378/1/schemas/MyOtherClass', - headers: masterKeyHeaders, - json: true, - }, (error, response, body) => { - //Expect _SCHEMA entry to be gone. - expect(response.statusCode).toEqual(400); - expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(body.error).toEqual('Class MyOtherClass does not exist.'); - done(); - }); - }); - }); - }).then(() => { - }, error => { - fail(error); - done(); - }); + request.del( + { + url: 'http://localhost:8378/1/schemas/MyOtherClass', + headers: masterKeyHeaders, + json: true, + }, + (error, response) => { + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({}); + config.database + .collectionExists('_Join:aRelation:MyOtherClass') + .then(exists => { + if (exists) { + fail('Relation collection should be deleted.'); + done(); + } + return config.database.collectionExists('MyOtherClass'); + }) + .then(exists => { + if (exists) { + fail('Class collection should be deleted.'); + done(); + } + }) + .then(() => { + request.get( + { + url: 'http://localhost:8378/1/schemas/MyOtherClass', + headers: masterKeyHeaders, + json: true, + }, + (error, response, body) => { + //Expect _SCHEMA entry to be gone. + expect(response.statusCode).toEqual(400); + expect(body.code).toEqual(Parse.Error.INVALID_CLASS_NAME); + expect(body.error).toEqual( + 'Class MyOtherClass does not exist.' + ); + done(); + } + ); + }); + } + ); + }) + .then( + () => {}, + error => { + fail(error); + done(); + } + ); }); it('deletes schema when actual collection does not exist', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClassForDelete', - headers: masterKeyHeaders, - json: true, - body: { - className: 'NewClassForDelete' - } - }, (error, response) => { - expect(error).toEqual(null); - expect(response.body.className).toEqual('NewClassForDelete'); - request.del({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClassForDelete', headers: masterKeyHeaders, json: true, - }, (error, response) => { - expect(response.statusCode).toEqual(200); - expect(response.body).toEqual({}); - config.database.loadSchema().then(schema => { - schema.hasClass('NewClassForDelete').then(exist => { - expect(exist).toEqual(false); - done(); - }); - }) - }); - }); - }); - - it('deletes schema when actual collection exists', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClassForDelete', - headers: masterKeyHeaders, - json: true, - body: { - className: 'NewClassForDelete' - } - }, (error, response) => { - expect(error).toEqual(null); - expect(response.body.className).toEqual('NewClassForDelete'); - request.post({ - url: 'http://localhost:8378/1/classes/NewClassForDelete', - headers: restKeyHeaders, - json: true - }, (error, response) => { + body: { + className: 'NewClassForDelete', + }, + }, + (error, response) => { expect(error).toEqual(null); - expect(typeof response.body.objectId).toEqual('string'); - request.del({ - url: 'http://localhost:8378/1/classes/NewClassForDelete/' + response.body.objectId, - headers: restKeyHeaders, - json: true, - }, (error) => { - expect(error).toEqual(null); - request.del({ + expect(response.body.className).toEqual('NewClassForDelete'); + request.del( + { url: 'http://localhost:8378/1/schemas/NewClassForDelete', headers: masterKeyHeaders, json: true, - }, (error, response) => { + }, + (error, response) => { expect(response.statusCode).toEqual(200); expect(response.body).toEqual({}); config.database.loadSchema().then(schema => { @@ -1007,291 +1143,409 @@ describe('schemas', () => { done(); }); }); - }); - }); - }); - }); - }); - - it('should set/get schema permissions', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/AClass', - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: { - find: { - '*': true - }, - create: { - 'role:admin': true } - } + ); } - }, (error) => { - expect(error).toEqual(null); - request.get({ - url: 'http://localhost:8378/1/schemas/AClass', + ); + }); + + it('deletes schema when actual collection exists', done => { + request.post( + { + url: 'http://localhost:8378/1/schemas/NewClassForDelete', headers: masterKeyHeaders, json: true, - }, (error, response) => { - expect(response.statusCode).toEqual(200); - expect(response.body.classLevelPermissions).toEqual({ - find: { - '*': true - }, - create: { - 'role:admin': true + body: { + className: 'NewClassForDelete', + }, + }, + (error, response) => { + expect(error).toEqual(null); + expect(response.body.className).toEqual('NewClassForDelete'); + request.post( + { + url: 'http://localhost:8378/1/classes/NewClassForDelete', + headers: restKeyHeaders, + json: true, }, - get: {}, - update: {}, - delete: {}, - addField: {} - }); - done(); - }); - }); + (error, response) => { + expect(error).toEqual(null); + expect(typeof response.body.objectId).toEqual('string'); + request.del( + { + url: + 'http://localhost:8378/1/classes/NewClassForDelete/' + + response.body.objectId, + headers: restKeyHeaders, + json: true, + }, + error => { + expect(error).toEqual(null); + request.del( + { + url: 'http://localhost:8378/1/schemas/NewClassForDelete', + headers: masterKeyHeaders, + json: true, + }, + (error, response) => { + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({}); + config.database.loadSchema().then(schema => { + schema.hasClass('NewClassForDelete').then(exist => { + expect(exist).toEqual(false); + done(); + }); + }); + } + ); + } + ); + } + ); + } + ); }); - it('should fail setting schema permissions with invalid key', done => { - - const object = new Parse.Object('AClass'); - object.save().then(() => { - request.put({ + it('should set/get schema permissions', done => { + request.post( + { url: 'http://localhost:8378/1/schemas/AClass', headers: masterKeyHeaders, json: true, body: { classLevelPermissions: { find: { - '*': true + '*': true, }, create: { - 'role:admin': true + 'role:admin': true, }, - dummy: { - 'some': true - } - } - } - }, (error, response, body) => { - expect(error).toEqual(null); - expect(body.code).toEqual(107); - expect(body.error).toEqual('dummy is not a valid operation for class level permissions'); - done(); - }); - }); - }); - - it('should not be able to add a field', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/AClass', - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: { - create: { - '*': true - }, - find: { - '*': true }, - addField: { - 'role:admin': true + }, + }, + error => { + expect(error).toEqual(null); + request.get( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + }, + (error, response) => { + expect(response.statusCode).toEqual(200); + expect(response.body.classLevelPermissions).toEqual({ + find: { + '*': true, + }, + create: { + 'role:admin': true, + }, + get: {}, + update: {}, + delete: {}, + addField: {}, + }); + done(); } + ); + } + ); + }); + + it('should fail setting schema permissions with invalid key', done => { + const object = new Parse.Object('AClass'); + object.save().then(() => { + request.put( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '*': true, + }, + create: { + 'role:admin': true, + }, + dummy: { + some: true, + }, + }, + }, + }, + (error, response, body) => { + expect(error).toEqual(null); + expect(body.code).toEqual(107); + expect(body.error).toEqual( + 'dummy is not a valid operation for class level permissions' + ); + done(); } + ); + }); + }); + + it('should not be able to add a field', done => { + request.post( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + create: { + '*': true, + }, + find: { + '*': true, + }, + addField: { + 'role:admin': true, + }, + }, + }, + }, + error => { + expect(error).toEqual(null); + const object = new Parse.Object('AClass'); + object.set('hello', 'world'); + return object.save().then( + () => { + fail('should not be able to add a field'); + done(); + }, + err => { + expect(err.message).toEqual( + 'Permission denied for action addField on class AClass.' + ); + done(); + } + ); } - }, (error) => { - expect(error).toEqual(null); - const object = new Parse.Object('AClass'); - object.set('hello', 'world'); - return object.save().then(() => { - fail('should not be able to add a field'); - done(); - }, (err) => { - expect(err.message).toEqual('Permission denied for action addField on class AClass.'); - 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: { - create: { - '*': true + request.post( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + create: { + '*': true, + }, + addField: { + '*': true, + }, + }, + }, + }, + error => { + expect(error).toEqual(null); + const object = new Parse.Object('AClass'); + object.set('hello', 'world'); + return object.save().then( + () => { + done(); }, - addField: { - '*': true + () => { + fail('should be able to add a field'); + done(); } - } + ); } - }, (error) => { - expect(error).toEqual(null); - const object = new Parse.Object('AClass'); - object.set('hello', 'world'); - return object.save().then(() => { - done(); - }, () => { - fail('should be able to add a field'); - done(); - }) - }) + ); }); it('should throw with invalid userId (>10 chars)', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/AClass', - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: { - find: { - '1234567890A': true + request.post( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '1234567890A': true, + }, }, - } + }, + }, + (error, response, body) => { + expect(body.error).toEqual( + "'1234567890A' is not a valid key for class level permissions" + ); + done(); } - }, (error, response, body) => { - expect(body.error).toEqual("'1234567890A' is not a valid key for class level permissions"); - done(); - }) + ); }); it('should throw with invalid userId (<10 chars)', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/AClass', - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: { - find: { - 'a12345678': true + request.post( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + a12345678: true, + }, }, - } + }, + }, + (error, response, body) => { + expect(body.error).toEqual( + "'a12345678' is not a valid key for class level permissions" + ); + done(); } - }, (error, response, body) => { - expect(body.error).toEqual("'a12345678' is not a valid key for class level permissions"); - done(); - }) + ); }); it('should throw with invalid userId (invalid char)', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/AClass', - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: { - find: { - '12345_6789': true + request.post( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '12345_6789': true, + }, }, - } + }, + }, + (error, response, body) => { + expect(body.error).toEqual( + "'12345_6789' is not a valid key for class level permissions" + ); + done(); } - }, (error, response, body) => { - expect(body.error).toEqual("'12345_6789' is not a valid key for class level permissions"); - done(); - }) + ); }); it('should throw with invalid * (spaces before)', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/AClass', - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: { - find: { - ' *': true + request.post( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + ' *': true, + }, }, - } + }, + }, + (error, response, body) => { + expect(body.error).toEqual( + "' *' is not a valid key for class level permissions" + ); + done(); } - }, (error, response, body) => { - expect(body.error).toEqual("' *' is not a valid key for class level permissions"); - done(); - }) + ); }); it('should throw with invalid * (spaces after)', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/AClass', - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: { - find: { - '* ': true + request.post( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '* ': true, + }, }, - } + }, + }, + (error, response, body) => { + expect(body.error).toEqual( + "'* ' is not a valid key for class level permissions" + ); + done(); } - }, (error, response, body) => { - expect(body.error).toEqual("'* ' is not a valid key for class level permissions"); - done(); - }) + ); }); it('should throw if permission is number', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/AClass', - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: { - find: { - '*': 1 + request.post( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '*': 1, + }, }, - } + }, + }, + (error, response, body) => { + expect(body.error).toEqual( + "'1' is not a valid value for class level permissions find:*:1" + ); + done(); } - }, (error, response, body) => { - expect(body.error).toEqual("'1' is not a valid value for class level permissions find:*:1"); - done(); - }) + ); }); it('should throw if permission is empty string', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/AClass', - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: { - find: { - '*': "" + request.post( + { + url: 'http://localhost:8378/1/schemas/AClass', + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: { + find: { + '*': '', + }, }, - } + }, + }, + (error, response, body) => { + expect(body.error).toEqual( + "'' is not a valid value for class level permissions find:*:" + ); + done(); } - }, (error, response, body) => { - expect(body.error).toEqual("'' is not a valid value for class level permissions find:*:"); - done(); - }) + ); }); function setPermissionsOnClass(className, permissions, doPut) { let op = request.post; - if (doPut) - { + if (doPut) { op = request.put; } return new Promise((resolve, reject) => { - op({ - url: 'http://localhost:8378/1/schemas/' + className, - headers: masterKeyHeaders, - json: true, - body: { - classLevelPermissions: permissions - } - }, (error, response, body) => { - if (error) { - return reject(error); - } - if (body.error) { - return reject(body); + op( + { + url: 'http://localhost:8378/1/schemas/' + className, + headers: masterKeyHeaders, + json: true, + body: { + classLevelPermissions: permissions, + }, + }, + (error, response, body) => { + if (error) { + return reject(error); + } + if (body.error) { + return reject(body); + } + return resolve(body); } - return resolve(body); - }) + ); }); } @@ -1307,39 +1561,54 @@ describe('schemas', () => { const role = new Parse.Role('admin', new Parse.ACL()); setPermissionsOnClass('AClass', { - 'find': { - 'role:admin': true - } - }).then(() => { - return Parse.Object.saveAll([user, admin, role], {useMasterKey: true}); - }).then(()=> { - role.relation('users').add(admin); - return role.save(null, {useMasterKey: true}); - }).then(() => { - return Parse.User.logIn('user', 'user').then(() => { - const obj = new Parse.Object('AClass'); - return obj.save(null, {useMasterKey: true}); - }) - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find().then(() => { - fail('Use should hot be able to find!') - }, (err) => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); - return Promise.resolve(); - }) - }).then(() => { - return Parse.User.logIn('admin', 'admin'); - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find(); - }).then((results) => { - expect(results.length).toBe(1); - done(); - }).catch((err) => { - jfail(err); - done(); + find: { + 'role:admin': true, + }, }) + .then(() => { + return Parse.Object.saveAll([user, admin, role], { + useMasterKey: true, + }); + }) + .then(() => { + role.relation('users').add(admin); + return role.save(null, { useMasterKey: true }); + }) + .then(() => { + return Parse.User.logIn('user', 'user').then(() => { + const obj = new Parse.Object('AClass'); + return obj.save(null, { useMasterKey: true }); + }); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find().then( + () => { + fail('Use should hot be able to find!'); + }, + err => { + expect(err.message).toEqual( + 'Permission denied for action find on class AClass.' + ); + return Promise.resolve(); + } + ); + }) + .then(() => { + return Parse.User.logIn('admin', 'admin'); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(1); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); it('validate CLP 2', done => { @@ -1354,55 +1623,79 @@ describe('schemas', () => { const role = new Parse.Role('admin', new Parse.ACL()); setPermissionsOnClass('AClass', { - 'find': { - 'role:admin': true - } - }).then(() => { - return Parse.Object.saveAll([user, admin, role], {useMasterKey: true}); - }).then(()=> { - role.relation('users').add(admin); - return role.save(null, {useMasterKey: true}); - }).then(() => { - return Parse.User.logIn('user', 'user').then(() => { - const obj = new Parse.Object('AClass'); - return obj.save(null, {useMasterKey: true}); - }) - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find().then(() => { - fail('User should not be able to find!') - }, (err) => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); - return Promise.resolve(); - }) - }).then(() => { - // let everyone see it now - return setPermissionsOnClass('AClass', { - 'find': { - 'role:admin': true, - '*': true - } - }, true); - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find().then((result) => { - expect(result.length).toBe(1); - }, () => { - fail('User should be able to find!') + find: { + 'role:admin': true, + }, + }) + .then(() => { + return Parse.Object.saveAll([user, admin, role], { + useMasterKey: true, + }); + }) + .then(() => { + role.relation('users').add(admin); + return role.save(null, { useMasterKey: true }); + }) + .then(() => { + return Parse.User.logIn('user', 'user').then(() => { + const obj = new Parse.Object('AClass'); + return obj.save(null, { useMasterKey: true }); + }); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find().then( + () => { + fail('User should not be able to find!'); + }, + err => { + expect(err.message).toEqual( + 'Permission denied for action find on class AClass.' + ); + return Promise.resolve(); + } + ); + }) + .then(() => { + // let everyone see it now + return setPermissionsOnClass( + 'AClass', + { + find: { + 'role:admin': true, + '*': true, + }, + }, + true + ); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find().then( + result => { + expect(result.length).toBe(1); + }, + () => { + fail('User should be able to find!'); + done(); + } + ); + }) + .then(() => { + return Parse.User.logIn('admin', 'admin'); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(1); + done(); + }) + .catch(err => { + jfail(err); done(); }); - }).then(() => { - return Parse.User.logIn('admin', 'admin'); - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find(); - }).then((results) => { - expect(results.length).toBe(1); - done(); - }).catch((err) => { - jfail(err); - done(); - }) }); it('validate CLP 3', done => { @@ -1417,50 +1710,70 @@ describe('schemas', () => { const role = new Parse.Role('admin', new Parse.ACL()); setPermissionsOnClass('AClass', { - 'find': { - 'role:admin': true - } - }).then(() => { - return Parse.Object.saveAll([user, admin, role], {useMasterKey: true}); - }).then(()=> { - role.relation('users').add(admin); - return role.save(null, {useMasterKey: true}); - }).then(() => { - return Parse.User.logIn('user', 'user').then(() => { - const obj = new Parse.Object('AClass'); - return obj.save(null, {useMasterKey: true}); - }) - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find().then(() => { - fail('User should not be able to find!') - }, (err) => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); - return Promise.resolve(); - }) - }).then(() => { - // delete all CLP - return setPermissionsOnClass('AClass', null, true); - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find().then((result) => { - expect(result.length).toBe(1); - }, () => { - fail('User should be able to find!') + find: { + 'role:admin': true, + }, + }) + .then(() => { + return Parse.Object.saveAll([user, admin, role], { + useMasterKey: true, + }); + }) + .then(() => { + role.relation('users').add(admin); + return role.save(null, { useMasterKey: true }); + }) + .then(() => { + return Parse.User.logIn('user', 'user').then(() => { + const obj = new Parse.Object('AClass'); + return obj.save(null, { useMasterKey: true }); + }); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find().then( + () => { + fail('User should not be able to find!'); + }, + err => { + expect(err.message).toEqual( + 'Permission denied for action find on class AClass.' + ); + return Promise.resolve(); + } + ); + }) + .then(() => { + // delete all CLP + return setPermissionsOnClass('AClass', null, true); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find().then( + result => { + expect(result.length).toBe(1); + }, + () => { + fail('User should be able to find!'); + done(); + } + ); + }) + .then(() => { + return Parse.User.logIn('admin', 'admin'); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(1); + done(); + }) + .catch(err => { + jfail(err); done(); }); - }).then(() => { - return Parse.User.logIn('admin', 'admin'); - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find(); - }).then((results) => { - expect(results.length).toBe(1); - done(); - }).catch((err) => { - jfail(err); - done(); - }); }); it('validate CLP 4', done => { @@ -1475,58 +1788,87 @@ describe('schemas', () => { const role = new Parse.Role('admin', new Parse.ACL()); setPermissionsOnClass('AClass', { - 'find': { - 'role:admin': true - } - }).then(() => { - return Parse.Object.saveAll([user, admin, role], {useMasterKey: true}); - }).then(()=> { - role.relation('users').add(admin); - return role.save(null, {useMasterKey: true}); - }).then(() => { - return Parse.User.logIn('user', 'user').then(() => { - const obj = new Parse.Object('AClass'); - return obj.save(null, {useMasterKey: true}); - }) - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find().then(() => { - fail('User should not be able to find!') - }, (err) => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); - return Promise.resolve(); - }) - }).then(() => { - // borked CLP should not affec security - return setPermissionsOnClass('AClass', { - 'found': { - 'role:admin': true - } - }, true).then(() => { - fail("Should not be able to save a borked CLP"); - }, () => { - return Promise.resolve(); - }) - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find().then(() => { - fail('User should not be able to find!') - }, (err) => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); - return Promise.resolve(); - }); - }).then(() => { - return Parse.User.logIn('admin', 'admin'); - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find(); - }).then((results) => { - expect(results.length).toBe(1); - done(); - }).catch((err) => { - jfail(err); - done(); + find: { + 'role:admin': true, + }, }) + .then(() => { + return Parse.Object.saveAll([user, admin, role], { + useMasterKey: true, + }); + }) + .then(() => { + role.relation('users').add(admin); + return role.save(null, { useMasterKey: true }); + }) + .then(() => { + return Parse.User.logIn('user', 'user').then(() => { + const obj = new Parse.Object('AClass'); + return obj.save(null, { useMasterKey: true }); + }); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find().then( + () => { + fail('User should not be able to find!'); + }, + err => { + expect(err.message).toEqual( + 'Permission denied for action find on class AClass.' + ); + return Promise.resolve(); + } + ); + }) + .then(() => { + // borked CLP should not affec security + return setPermissionsOnClass( + 'AClass', + { + found: { + 'role:admin': true, + }, + }, + true + ).then( + () => { + fail('Should not be able to save a borked CLP'); + }, + () => { + return Promise.resolve(); + } + ); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find().then( + () => { + fail('User should not be able to find!'); + }, + err => { + expect(err.message).toEqual( + 'Permission denied for action find on class AClass.' + ); + return Promise.resolve(); + } + ); + }) + .then(() => { + return Parse.User.logIn('admin', 'admin'); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(1); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); it('validate CLP 5', done => { @@ -1543,921 +1885,1145 @@ describe('schemas', () => { const role = new Parse.Role('admin', new Parse.ACL()); - Promise.resolve().then(() => { - return Parse.Object.saveAll([user, user2, admin, role], {useMasterKey: true}); - }).then(()=> { - role.relation('users').add(admin); - return role.save(null, {useMasterKey: true}).then(() => { - const perm = { - find: {} - }; - // let the user find - perm['find'][user.id] = true; - return setPermissionsOnClass('AClass', perm); + Promise.resolve() + .then(() => { + return Parse.Object.saveAll([user, user2, admin, role], { + useMasterKey: true, + }); }) - }).then(() => { - return Parse.User.logIn('user', 'user').then(() => { - const obj = new Parse.Object('AClass'); - return obj.save(); + .then(() => { + role.relation('users').add(admin); + return role.save(null, { useMasterKey: true }).then(() => { + const perm = { + find: {}, + }; + // let the user find + perm['find'][user.id] = true; + return setPermissionsOnClass('AClass', perm); + }); }) - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find().then((res) => { - expect(res.length).toEqual(1); - }, () => { - fail('User should be able to find!') - return Promise.resolve(); - }) - }).then(() => { - return Parse.User.logIn('admin', 'admin'); - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find(); - }).then(() => { - fail("should not be able to read!"); - return Promise.resolve(); - }, (err) => { - expect(err.message).toEqual('Permission denied for action create on class AClass.'); - return Promise.resolve(); - }).then(() => { - return Parse.User.logIn('user2', 'user2'); - }).then(() => { - const query = new Parse.Query('AClass'); - return query.find(); - }).then(() => { - fail("should not be able to read!"); - return Promise.resolve(); - }, (err) => { - expect(err.message).toEqual('Permission denied for action find on class AClass.'); - return Promise.resolve(); - }).then(() => { - done(); - }); + .then(() => { + return Parse.User.logIn('user', 'user').then(() => { + const obj = new Parse.Object('AClass'); + return obj.save(); + }); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find().then( + res => { + expect(res.length).toEqual(1); + }, + () => { + fail('User should be able to find!'); + return Promise.resolve(); + } + ); + }) + .then(() => { + return Parse.User.logIn('admin', 'admin'); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find(); + }) + .then( + () => { + fail('should not be able to read!'); + return Promise.resolve(); + }, + err => { + expect(err.message).toEqual( + 'Permission denied for action create on class AClass.' + ); + return Promise.resolve(); + } + ) + .then(() => { + return Parse.User.logIn('user2', 'user2'); + }) + .then(() => { + const query = new Parse.Query('AClass'); + return query.find(); + }) + .then( + () => { + fail('should not be able to read!'); + return Promise.resolve(); + }, + err => { + expect(err.message).toEqual( + 'Permission denied for action find on class AClass.' + ); + return Promise.resolve(); + } + ) + .then(() => { + done(); + }); }); - it('can query with include and CLP (issue #2005)', (done) => { + it('can query with include and CLP (issue #2005)', done => { setPermissionsOnClass('AnotherObject', { - get: {"*": true}, + get: { '*': true }, find: {}, - create: {'*': true}, - update: {'*': true}, - delete: {'*': true}, - addField:{'*': true} - }).then(() => { - const obj = new Parse.Object('AnObject'); - const anotherObject = new Parse.Object('AnotherObject'); - return obj.save({ - anotherObject - }) - }).then(() => { - const query = new Parse.Query('AnObject'); - query.include('anotherObject'); - return query.find(); - }).then((res) => { - expect(res.length).toBe(1); - expect(res[0].get('anotherObject')).not.toBeUndefined(); - done(); - }).catch((err) => { - jfail(err); - done(); + create: { '*': true }, + update: { '*': true }, + delete: { '*': true }, + addField: { '*': true }, }) + .then(() => { + const obj = new Parse.Object('AnObject'); + const anotherObject = new Parse.Object('AnotherObject'); + return obj.save({ + anotherObject, + }); + }) + .then(() => { + const query = new Parse.Query('AnObject'); + query.include('anotherObject'); + return query.find(); + }) + .then(res => { + expect(res.length).toBe(1); + expect(res[0].get('anotherObject')).not.toBeUndefined(); + done(); + }) + .catch(err => { + jfail(err); + done(); + }); }); - it('can add field as master (issue #1257)', (done) => { + it('can add field as master (issue #1257)', done => { setPermissionsOnClass('AClass', { - 'addField': {} - }).then(() => { - const obj = new Parse.Object('AClass'); - obj.set('key', 'value'); - return obj.save(null, {useMasterKey: true}) - }).then((obj) => { - expect(obj.get('key')).toEqual('value'); - done(); - }, () => { - fail('should not fail'); - done(); - }); + addField: {}, + }) + .then(() => { + const obj = new Parse.Object('AClass'); + obj.set('key', 'value'); + return obj.save(null, { useMasterKey: true }); + }) + .then( + obj => { + expect(obj.get('key')).toEqual('value'); + done(); + }, + () => { + fail('should not fail'); + done(); + } + ); }); - it('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('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('unset field in beforeSave should not stop object creation', (done) => { + it('unset field in beforeSave should not stop object creation', done => { const hook = { method: function(req) { if (req.object.get('undesiredField')) { req.object.unset('undesiredField'); } - } + }, }; spyOn(hook, 'method').and.callThrough(); Parse.Cloud.beforeSave('AnObject', hook.method); setPermissionsOnClass('AnObject', { - get: {"*": true}, - find: {"*": true}, - create: {'*': true}, - update: {'*': true}, - delete: {'*': true}, - addField:{} - }).then(() => { - const obj = new Parse.Object('AnObject'); - obj.set('desiredField', 'createMe'); - return obj.save(null, {useMasterKey: true}); - }).then(() => { - const obj = new Parse.Object('AnObject'); - obj.set('desiredField', 'This value should be kept'); - obj.set('undesiredField', 'This value should be IGNORED'); - return obj.save(); - }).then(() => { - const query = new Parse.Query('AnObject'); - return query.find(); - }).then((results) => { - expect(results.length).toBe(2); - expect(results[0].has('desiredField')).toBe(true); - expect(results[1].has('desiredField')).toBe(true); - expect(results[0].has('undesiredField')).toBe(false); - expect(results[1].has('undesiredField')).toBe(false); - expect(hook.method).toHaveBeenCalled(); - done(); - }); + get: { '*': true }, + find: { '*': true }, + create: { '*': true }, + update: { '*': true }, + delete: { '*': true }, + addField: {}, + }) + .then(() => { + const obj = new Parse.Object('AnObject'); + obj.set('desiredField', 'createMe'); + return obj.save(null, { useMasterKey: true }); + }) + .then(() => { + const obj = new Parse.Object('AnObject'); + obj.set('desiredField', 'This value should be kept'); + obj.set('undesiredField', 'This value should be IGNORED'); + return obj.save(); + }) + .then(() => { + const query = new Parse.Query('AnObject'); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(2); + expect(results[0].has('desiredField')).toBe(true); + expect(results[1].has('desiredField')).toBe(true); + expect(results[0].has('undesiredField')).toBe(false); + expect(results[1].has('undesiredField')).toBe(false); + expect(hook.method).toHaveBeenCalled(); + done(); + }); }); it('gives correct response when deleting a schema with CLPs (regression test #1919)', done => { - new Parse.Object('MyClass').save({ data: 'foo'}) + 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) => { - expect(response.statusCode).toEqual(200); - expect(response.body).toEqual({}); - done(); - }); + request.del( + { + url: 'http://localhost:8378/1/schemas/MyClass', + headers: masterKeyHeaders, + json: true, + }, + (error, response) => { + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({}); + done(); + } + ); }); }); - it("regression test for #1991", done => { + it('regression test for #1991', done => { const user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); const role = new Parse.Role('admin', new Parse.ACL()); const 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(() => { - const query = new Parse.Query('AnObject'); - return query.find(); - }).then((results) => { - expect(results.length).toBe(0); - done(); - }).catch((err) => { - fail('should not fail'); - jfail(err); - done(); - }); + 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(() => { + const query = new Parse.Query('AnObject'); + return query.find(); + }) + .then(results => { + expect(results.length).toBe(0); + done(); + }) + .catch(err => { + fail('should not fail'); + jfail(err); + done(); + }); }); - - it("regression test for #4409 (indexes override the clp)", done => { - setPermissionsOnClass('_Role', { - 'get': {"*": true}, - 'find': {"*": true}, - 'create': {'*': true}, - }, true).then(() => { - const config = Config.get('test'); - return config.database.adapter.updateSchemaWithIndexes(); - }).then(() => { - return rp.get({ - url: 'http://localhost:8378/1/schemas/_Role', - headers: masterKeyHeaders, - json: true, - }); - }).then((res) => { - expect(res.classLevelPermissions).toEqual({ - 'get': {"*": true}, - 'find': {"*": true}, - 'create': {'*': true}, - 'update': {}, - 'delete': {}, - 'addField': {}, - }); - }).then(done).catch(done.fail); + it('regression test for #4409 (indexes override the clp)', done => { + setPermissionsOnClass( + '_Role', + { + get: { '*': true }, + find: { '*': true }, + create: { '*': true }, + }, + true + ) + .then(() => { + const config = Config.get('test'); + return config.database.adapter.updateSchemaWithIndexes(); + }) + .then(() => { + return rp.get({ + url: 'http://localhost:8378/1/schemas/_Role', + headers: masterKeyHeaders, + json: true, + }); + }) + .then(res => { + expect(res.classLevelPermissions).toEqual({ + get: { '*': true }, + find: { '*': true }, + create: { '*': true }, + update: {}, + delete: {}, + addField: {}, + }); + }) + .then(done) + .catch(done.fail); }); it('regression test for #2246', done => { const profile = new Parse.Object('UserProfile'); const user = new Parse.User(); function initialize() { - return user.save({ - username: 'user', - password: 'password' - }).then(() => { - return profile.save({user}).then(() => { - return user.save({ - userProfile: profile - }, {useMasterKey: true}); + return user + .save({ + username: 'user', + password: 'password', + }) + .then(() => { + return profile.save({ user }).then(() => { + return user.save( + { + userProfile: profile, + }, + { useMasterKey: true } + ); + }); }); - }); } - initialize().then(() => { - return setPermissionsOnClass('UserProfile', { - 'readUserFields': ['user'], - 'writeUserFields': ['user'] - }, true); - }).then(() => { - return Parse.User.logIn('user', 'password') - }).then(() => { - const query = new Parse.Query('_User'); - query.include('userProfile'); - return query.get(user.id); - }).then((user) => { - expect(user.get('userProfile')).not.toBeUndefined(); - done(); - }, (err) => { - jfail(err); - done(); - }); + initialize() + .then(() => { + return setPermissionsOnClass( + 'UserProfile', + { + readUserFields: ['user'], + writeUserFields: ['user'], + }, + true + ); + }) + .then(() => { + return Parse.User.logIn('user', 'password'); + }) + .then(() => { + const query = new Parse.Query('_User'); + query.include('userProfile'); + return query.get(user.id); + }) + .then( + user => { + expect(user.get('userProfile')).not.toBeUndefined(); + done(); + }, + err => { + jfail(err); + done(); + } + ); }); describe('index management', () => { beforeEach(() => require('../lib/TestUtils').destroyAllDataPermanently()); it('cannot create index if field does not exist', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - indexes: { - name1: { aString: 1}, + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { aString: 1 }, + }, + }, + }, + (error, response, body) => { + expect(body.code).toBe(Parse.Error.INVALID_QUERY); + expect(body.error).toBe( + 'Field aString does not exist, cannot add index.' + ); + done(); } - } - }, (error, response, body) => { - expect(body.code).toBe(Parse.Error.INVALID_QUERY); - expect(body.error).toBe('Field aString does not exist, cannot add index.'); - done(); - }); - }) + ); + } + ); }); it('can create index on default field', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - indexes: { - name1: { createdAt: 1}, + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { createdAt: 1 }, + }, + }, + }, + (error, response, body) => { + expect(body.indexes.name1).toEqual({ createdAt: 1 }); + done(); } - } - }, (error, response, body) => { - expect(body.indexes.name1).toEqual({ createdAt: 1}); - done(); - }); - }) + ); + } + ); }); it('cannot create compound index if field does not exist', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - fields: { - aString: {type: 'String'} + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: { type: 'String' }, + }, + indexes: { + name1: { aString: 1, bString: 1 }, + }, + }, }, - indexes: { - name1: { aString: 1, bString: 1}, + (error, response, body) => { + expect(body.code).toBe(Parse.Error.INVALID_QUERY); + expect(body.error).toBe( + 'Field bString does not exist, cannot add index.' + ); + done(); } - } - }, (error, response, body) => { - expect(body.code).toBe(Parse.Error.INVALID_QUERY); - expect(body.error).toBe('Field bString does not exist, cannot add index.'); - done(); - }); - }) + ); + } + ); }); it('allows add index when you create a class', done => { - request.post({ - url: 'http://localhost:8378/1/schemas', - headers: masterKeyHeaders, - json: true, - body: { - className: "NewClass", - fields: { - aString: {type: 'String'} - }, - indexes: { - name1: { aString: 1}, + request.post( + { + url: 'http://localhost:8378/1/schemas', + headers: masterKeyHeaders, + json: true, + body: { + className: 'NewClass', + fields: { + aString: { type: 'String' }, + }, + indexes: { + name1: { aString: 1 }, + }, }, + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + name1: { aString: 1 }, + }, + }); + config.database.adapter.getIndexes('NewClass').then(indexes => { + expect(indexes.length).toBe(2); + done(); + }); } - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'} - }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - name1: { aString: 1}, - }, - }); - config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toBe(2); - done(); - }); - }); + ); }); it('empty index returns nothing', done => { - request.post({ - url: 'http://localhost:8378/1/schemas', - headers: masterKeyHeaders, - json: true, - body: { - className: "NewClass", - fields: { - aString: {type: 'String'} - }, - indexes: {}, - } - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'} - }, - classLevelPermissions: defaultClassLevelPermissions, - }); - done(); - }); - }); - - it('lets you add indexes', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ - url: 'http://localhost:8378/1/schemas/NewClass', + request.post( + { + url: 'http://localhost:8378/1/schemas', headers: masterKeyHeaders, json: true, body: { + className: 'NewClass', fields: { - aString: {type: 'String'} - }, - indexes: { - name1: { aString: 1}, + aString: { type: 'String' }, }, - } - }, (error, response, body) => { - expect(dd(body, { + indexes: {}, + }, + }, + (error, response, body) => { + expect(body).toEqual({ className: 'NewClass', fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'} + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, }, classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - name1: { aString: 1 }, - } - })).toEqual(undefined); - request.get({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'} - }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - name1: { aString: 1 }, - } - }); - config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(2); - done(); - }); }); - }); - }) + done(); + } + ); }); - it('lets you add multiple indexes', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + it('lets you add indexes', done => { + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - fields: { - aString: {type: 'String'}, - bString: {type: 'String'}, - cString: {type: 'String'}, - dString: {type: 'String'}, + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: { type: 'String' }, + }, + indexes: { + name1: { aString: 1 }, + }, + }, }, - indexes: { - name1: { aString: 1 }, - name2: { bString: 1 }, - name3: { cString: 1, dString: 1 }, + (error, response, body) => { + expect( + dd(body, { + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + name1: { aString: 1 }, + }, + }) + ).toEqual(undefined); + request.get( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + name1: { aString: 1 }, + }, + }); + config.database.adapter + .getIndexes('NewClass') + .then(indexes => { + expect(indexes.length).toEqual(2); + done(); + }); + } + ); } - } - }, (error, response, body) => { - expect(dd(body, { - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'}, - bString: {type: 'String'}, - cString: {type: 'String'}, - dString: {type: 'String'}, + ); + } + ); + }); + + it('lets you add multiple indexes', done => { + request.post( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: { type: 'String' }, + bString: { type: 'String' }, + cString: { type: 'String' }, + dString: { type: 'String' }, + }, + indexes: { + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1, dString: 1 }, + }, + }, }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - name1: { aString: 1 }, - name2: { bString: 1 }, - name3: { cString: 1, dString: 1 }, + (error, response, body) => { + expect( + dd(body, { + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + bString: { type: 'String' }, + cString: { type: 'String' }, + dString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1, dString: 1 }, + }, + }) + ).toEqual(undefined); + request.get( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + bString: { type: 'String' }, + cString: { type: 'String' }, + dString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1, dString: 1 }, + }, + }); + config.database.adapter + .getIndexes('NewClass') + .then(indexes => { + expect(indexes.length).toEqual(4); + done(); + }); + } + ); } - })).toEqual(undefined); - request.get({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'}, - bString: {type: 'String'}, - cString: {type: 'String'}, - dString: {type: 'String'}, - }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - name1: { aString: 1 }, - name2: { bString: 1 }, - name3: { cString: 1, dString: 1 }, - }, - }); - config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(4); - done(); - }); - }); - }); - }) + ); + } + ); }); it('lets you delete indexes', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - fields: { - aString: {type: 'String'}, - }, - indexes: { - name1: { aString: 1 }, - } - } - }, (error, response, body) => { - expect(dd(body, { - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'}, + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: { type: 'String' }, + }, + indexes: { + name1: { aString: 1 }, + }, + }, }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - name1: { aString: 1 }, + (error, response, body) => { + expect( + dd(body, { + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + name1: { aString: 1 }, + }, + }) + ).toEqual(undefined); + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { __op: 'Delete' }, + }, + }, + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + }, + }); + config.database.adapter + .getIndexes('NewClass') + .then(indexes => { + expect(indexes.length).toEqual(1); + done(); + }); + } + ); } - })).toEqual(undefined); - request.put({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: { - indexes: { - name1: { __op: 'Delete' } - } - } - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'}, - }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - } - }); - config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(1); - done(); - }); - }); - }); - }) + ); + } + ); }); it('lets you delete multiple indexes', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - fields: { - aString: {type: 'String'}, - bString: {type: 'String'}, - cString: {type: 'String'}, - }, - indexes: { - name1: { aString: 1 }, - name2: { bString: 1 }, - name3: { cString: 1 }, - } - } - }, (error, response, body) => { - expect(dd(body, { - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'}, - bString: {type: 'String'}, - cString: {type: 'String'}, + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: { type: 'String' }, + bString: { type: 'String' }, + cString: { type: 'String' }, + }, + indexes: { + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1 }, + }, + }, }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - name1: { aString: 1 }, - name2: { bString: 1 }, - name3: { cString: 1 }, - } - })).toEqual(undefined); - request.put({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: { - indexes: { - name1: { __op: 'Delete' }, - name2: { __op: 'Delete' }, - } + (error, response, body) => { + expect( + dd(body, { + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + bString: { type: 'String' }, + cString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1 }, + }, + }) + ).toEqual(undefined); + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { __op: 'Delete' }, + name2: { __op: 'Delete' }, + }, + }, + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + bString: { type: 'String' }, + cString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + name3: { cString: 1 }, + }, + }); + config.database.adapter + .getIndexes('NewClass') + .then(indexes => { + expect(indexes.length).toEqual(2); + done(); + }); + } + ); } - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'}, - bString: {type: 'String'}, - cString: {type: 'String'}, - }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - name3: { cString: 1 }, - } - }); - config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(2); - done(); - }); - }); - }); - }) + ); + } + ); }); it('lets you add and delete indexes', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - fields: { - aString: {type: 'String'}, - bString: {type: 'String'}, - cString: {type: 'String'}, - dString: {type: 'String'}, - }, - indexes: { - name1: { aString: 1 }, - name2: { bString: 1 }, - name3: { cString: 1 }, - } - } - }, (error, response, body) => { - expect(dd(body, { - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'}, - bString: {type: 'String'}, - cString: {type: 'String'}, - dString: {type: 'String'}, + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: { type: 'String' }, + bString: { type: 'String' }, + cString: { type: 'String' }, + dString: { type: 'String' }, + }, + indexes: { + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1 }, + }, + }, }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - name1: { aString: 1 }, - name2: { bString: 1 }, - name3: { cString: 1 }, + (error, response, body) => { + expect( + dd(body, { + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + bString: { type: 'String' }, + cString: { type: 'String' }, + dString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + name1: { aString: 1 }, + name2: { bString: 1 }, + name3: { cString: 1 }, + }, + }) + ).toEqual(undefined); + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { __op: 'Delete' }, + name2: { __op: 'Delete' }, + name4: { dString: 1 }, + }, + }, + }, + (error, response, body) => { + expect(body).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + aString: { type: 'String' }, + bString: { type: 'String' }, + cString: { type: 'String' }, + dString: { type: 'String' }, + }, + classLevelPermissions: defaultClassLevelPermissions, + indexes: { + _id_: { _id: 1 }, + name3: { cString: 1 }, + name4: { dString: 1 }, + }, + }); + config.database.adapter + .getIndexes('NewClass') + .then(indexes => { + expect(indexes.length).toEqual(3); + done(); + }); + } + ); } - })).toEqual(undefined); - request.put({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: { - indexes: { - name1: { __op: 'Delete' }, - name2: { __op: 'Delete' }, - name4: { dString: 1 }, - } - } - }, (error, response, body) => { - expect(body).toEqual({ - className: 'NewClass', - fields: { - ACL: {type: 'ACL'}, - createdAt: {type: 'Date'}, - updatedAt: {type: 'Date'}, - objectId: {type: 'String'}, - aString: {type: 'String'}, - bString: {type: 'String'}, - cString: {type: 'String'}, - dString: {type: 'String'}, - }, - classLevelPermissions: defaultClassLevelPermissions, - indexes: { - _id_: { _id: 1 }, - name3: { cString: 1 }, - name4: { dString: 1 }, - } - }); - config.database.adapter.getIndexes('NewClass').then((indexes) => { - expect(indexes.length).toEqual(3); - done(); - }); - }); - }); - }) + ); + } + ); }); it('cannot delete index that does not exist', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - indexes: { - unknownIndex: { __op: 'Delete' } + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + unknownIndex: { __op: 'Delete' }, + }, + }, + }, + (error, response, body) => { + expect(body.code).toBe(Parse.Error.INVALID_QUERY); + expect(body.error).toBe( + 'Index unknownIndex does not exist, cannot delete.' + ); + done(); } - } - }, (error, response, body) => { - expect(body.code).toBe(Parse.Error.INVALID_QUERY); - expect(body.error).toBe('Index unknownIndex does not exist, cannot delete.'); - done(); - }); - }) + ); + } + ); }); it('cannot update index that exist', done => { - request.post({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: {}, - }, () => { - request.put({ + request.post( + { url: 'http://localhost:8378/1/schemas/NewClass', headers: masterKeyHeaders, json: true, - body: { - fields: { - aString: {type: 'String'}, + body: {}, + }, + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + aString: { type: 'String' }, + }, + indexes: { + name1: { aString: 1 }, + }, + }, }, - indexes: { - name1: { aString: 1 } + () => { + request.put( + { + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + indexes: { + name1: { field2: 1 }, + }, + }, + }, + (error, response, body) => { + expect(body.code).toBe(Parse.Error.INVALID_QUERY); + expect(body.error).toBe('Index name1 exists, cannot update.'); + done(); + } + ); } - } - }, () => { - request.put({ - url: 'http://localhost:8378/1/schemas/NewClass', - headers: masterKeyHeaders, - json: true, - body: { - indexes: { - name1: { field2: 1 } - } - } - }, (error, response, body) => { - expect(body.code).toBe(Parse.Error.INVALID_QUERY); - expect(body.error).toBe('Index name1 exists, cannot update.'); - done(); - }); - }); - }) + ); + } + ); }); - it_exclude_dbs(['postgres'])('get indexes on startup', (done) => { + it_exclude_dbs(['postgres'])('get indexes on startup', done => { const obj = new Parse.Object('TestObject'); - obj.save().then(() => { - return reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', - }); - }).then(() => { - request.get({ - url: 'http://localhost:8378/1/schemas/TestObject', - headers: masterKeyHeaders, - json: true, - }, (error, response, body) => { - expect(body.indexes._id_).toBeDefined(); - done(); + obj + .save() + .then(() => { + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + }); + }) + .then(() => { + request.get( + { + url: 'http://localhost:8378/1/schemas/TestObject', + headers: masterKeyHeaders, + json: true, + }, + (error, response, body) => { + expect(body.indexes._id_).toBeDefined(); + done(); + } + ); }); - }); }); - it_exclude_dbs(['postgres'])('get compound indexes on startup', (done) => { + it_exclude_dbs(['postgres'])('get compound indexes on startup', done => { const obj = new Parse.Object('TestObject'); obj.set('subject', 'subject'); obj.set('comment', 'comment'); - obj.save().then(() => { - return config.database.adapter.createIndex('TestObject', {subject: 'text', comment: 'text'}); - }).then(() => { - return reconfigureServer({ - appId: 'test', - restAPIKey: 'test', - publicServerURL: 'http://localhost:8378/1', - }); - }).then(() => { - request.get({ - url: 'http://localhost:8378/1/schemas/TestObject', - headers: masterKeyHeaders, - json: true, - }, (error, response, body) => { - expect(body.indexes._id_).toBeDefined(); - expect(body.indexes._id_._id).toEqual(1); - expect(body.indexes.subject_text_comment_text).toBeDefined(); - expect(body.indexes.subject_text_comment_text.subject).toEqual('text'); - expect(body.indexes.subject_text_comment_text.comment).toEqual('text'); - done(); + obj + .save() + .then(() => { + return config.database.adapter.createIndex('TestObject', { + subject: 'text', + comment: 'text', + }); + }) + .then(() => { + return reconfigureServer({ + appId: 'test', + restAPIKey: 'test', + publicServerURL: 'http://localhost:8378/1', + }); + }) + .then(() => { + request.get( + { + url: 'http://localhost:8378/1/schemas/TestObject', + headers: masterKeyHeaders, + json: true, + }, + (error, response, body) => { + expect(body.indexes._id_).toBeDefined(); + expect(body.indexes._id_._id).toEqual(1); + expect(body.indexes.subject_text_comment_text).toBeDefined(); + expect(body.indexes.subject_text_comment_text.subject).toEqual( + 'text' + ); + expect(body.indexes.subject_text_comment_text.comment).toEqual( + 'text' + ); + done(); + } + ); }); - }); }); - it_exclude_dbs(['postgres'])('cannot update to duplicate value on unique index', (done) => { - const index = { - code: 1 - }; - const obj1 = new Parse.Object('UniqueIndexClass'); - obj1.set('code', 1); - const obj2 = new Parse.Object('UniqueIndexClass'); - obj2.set('code', 2); - const adapter = config.database.adapter; - adapter._adaptiveCollection('UniqueIndexClass').then(collection => { - return collection._ensureSparseUniqueIndexInBackground(index); - }).then(() => { - return obj1.save(); - }).then(() => { - return obj2.save(); - }).then(() => { - obj1.set('code', 2); - return obj1.save(); - }).then(done.fail).catch((error) => { - expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); - done(); - }); - }); + it_exclude_dbs(['postgres'])( + 'cannot update to duplicate value on unique index', + done => { + const index = { + code: 1, + }; + const obj1 = new Parse.Object('UniqueIndexClass'); + obj1.set('code', 1); + const obj2 = new Parse.Object('UniqueIndexClass'); + obj2.set('code', 2); + const adapter = config.database.adapter; + adapter + ._adaptiveCollection('UniqueIndexClass') + .then(collection => { + return collection._ensureSparseUniqueIndexInBackground(index); + }) + .then(() => { + return obj1.save(); + }) + .then(() => { + return obj2.save(); + }) + .then(() => { + obj1.set('code', 2); + return obj1.save(); + }) + .then(done.fail) + .catch(error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + done(); + }); + } + ); }); }); diff --git a/spec/support/CustomAuth.js b/spec/support/CustomAuth.js index 3a2630cff9..922b77ac4c 100644 --- a/spec/support/CustomAuth.js +++ b/spec/support/CustomAuth.js @@ -1,4 +1,3 @@ - module.exports = { validateAppId: function() { return Promise.resolve(); @@ -8,5 +7,5 @@ module.exports = { return Promise.resolve(); } return Promise.reject(); - } -} + }, +}; diff --git a/spec/support/CustomAuthFunction.js b/spec/support/CustomAuthFunction.js index d13165e75d..8dd5a31f1a 100644 --- a/spec/support/CustomAuthFunction.js +++ b/spec/support/CustomAuthFunction.js @@ -1,4 +1,3 @@ - module.exports = function(validAuthData) { return { validateAppId: function() { @@ -9,6 +8,6 @@ module.exports = function(validAuthData) { return Promise.resolve(); } return Promise.reject(); - } - } -} + }, + }; +}; diff --git a/spec/support/CustomMiddleware.js b/spec/support/CustomMiddleware.js index 4debc14677..368500b847 100644 --- a/spec/support/CustomMiddleware.js +++ b/spec/support/CustomMiddleware.js @@ -1,4 +1,4 @@ module.exports = function(req, res, next) { res.set('X-Yolo', '1'); next(); -} +}; diff --git a/spec/testing-routes.js b/spec/testing-routes.js index 29cb6cf5f6..8ba0287ad2 100644 --- a/spec/testing-routes.js +++ b/spec/testing-routes.js @@ -14,20 +14,21 @@ function createApp(req, res) { const appId = cryptoUtils.randomHexString(32); ParseServer({ - databaseURI: 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', + databaseURI: + 'mongodb://localhost:27017/parseServerMongoAdapterTestDatabase', appId: appId, masterKey: 'master', serverURL: Parse.serverURL, - collectionPrefix: appId + collectionPrefix: appId, }); const keys = { - 'application_id': appId, - 'client_key' : 'unused', - 'windows_key' : 'unused', - 'javascript_key': 'unused', - 'webhook_key' : 'unused', - 'rest_api_key' : 'unused', - 'master_key' : 'master' + application_id: appId, + client_key: 'unused', + windows_key: 'unused', + javascript_key: 'unused', + webhook_key: 'unused', + rest_api_key: 'unused', + master_key: 'master', }; res.status(200).send(keys); } @@ -35,7 +36,7 @@ function createApp(req, res) { // deletes all collections that belong to the app function clearApp(req, res) { if (!req.auth.isMaster) { - return res.status(401).send({ "error": "unauthorized" }); + return res.status(401).send({ error: 'unauthorized' }); } return req.config.database.deleteEverything().then(() => { res.status(200).send({}); @@ -45,7 +46,7 @@ function clearApp(req, res) { // deletes all collections and drops the app from cache function dropApp(req, res) { if (!req.auth.isMaster) { - return res.status(401).send({ "error": "unauthorized" }); + return res.status(401).send({ error: 'unauthorized' }); } return req.config.database.deleteEverything().then(() => { AppCache.del(req.config.applicationId); @@ -60,13 +61,29 @@ function notImplementedYet(req, res) { router.post('/rest_clear_app', middlewares.handleParseHeaders, clearApp); router.post('/rest_block', middlewares.handleParseHeaders, notImplementedYet); -router.post('/rest_mock_v8_client', middlewares.handleParseHeaders, notImplementedYet); -router.post('/rest_unmock_v8_client', middlewares.handleParseHeaders, notImplementedYet); -router.post('/rest_verify_analytics', middlewares.handleParseHeaders, notImplementedYet); +router.post( + '/rest_mock_v8_client', + middlewares.handleParseHeaders, + notImplementedYet +); +router.post( + '/rest_unmock_v8_client', + middlewares.handleParseHeaders, + notImplementedYet +); +router.post( + '/rest_verify_analytics', + middlewares.handleParseHeaders, + notImplementedYet +); router.post('/rest_create_app', createApp); router.post('/rest_drop_app', middlewares.handleParseHeaders, dropApp); -router.post('/rest_configure_app', middlewares.handleParseHeaders, notImplementedYet); +router.post( + '/rest_configure_app', + middlewares.handleParseHeaders, + notImplementedYet +); module.exports = { - router: router + router: router, }; diff --git a/src/AccountLockout.js b/src/AccountLockout.js index 358f1ac402..da3049cddc 100644 --- a/src/AccountLockout.js +++ b/src/AccountLockout.js @@ -12,11 +12,11 @@ export class AccountLockout { */ _setFailedLoginCount(value) { const query = { - username: this._user.username + username: this._user.username, }; const updateFields = { - _failed_login_count: value + _failed_login_count: value, }; return this._config.database.update('_User', query, updateFields); @@ -28,17 +28,16 @@ export class AccountLockout { _isFailedLoginCountSet() { const query = { username: this._user.username, - _failed_login_count: { $exists: true } + _failed_login_count: { $exists: true }, }; - return this._config.database.find('_User', query) - .then(users => { - if (Array.isArray(users) && users.length > 0) { - return true; - } else { - return false; - } - }); + return this._config.database.find('_User', query).then(users => { + if (Array.isArray(users) && users.length > 0) { + return true; + } else { + return false; + } + }); } /** @@ -46,12 +45,11 @@ export class AccountLockout { * else do nothing */ _initFailedLoginCount() { - return this._isFailedLoginCountSet() - .then(failedLoginCountIsSet => { - if (!failedLoginCountIsSet) { - return this._setFailedLoginCount(0); - } - }); + return this._isFailedLoginCountSet().then(failedLoginCountIsSet => { + if (!failedLoginCountIsSet) { + return this._setFailedLoginCount(0); + } + }); } /** @@ -59,10 +57,12 @@ export class AccountLockout { */ _incrementFailedLoginCount() { const query = { - username: this._user.username + username: this._user.username, }; - const updateFields = {_failed_login_count: {__op: 'Increment', amount: 1}}; + const updateFields = { + _failed_login_count: { __op: 'Increment', amount: 1 }, + }; return this._config.database.update('_User', query, updateFields); } @@ -75,18 +75,29 @@ export class AccountLockout { _setLockoutExpiration() { const query = { username: this._user.username, - _failed_login_count: { $gte: this._config.accountLockout.threshold } + _failed_login_count: { $gte: this._config.accountLockout.threshold }, }; const now = new Date(); const updateFields = { - _account_lockout_expires_at: Parse._encode(new Date(now.getTime() + this._config.accountLockout.duration * 60 * 1000)) + _account_lockout_expires_at: Parse._encode( + new Date( + now.getTime() + this._config.accountLockout.duration * 60 * 1000 + ) + ), }; - return this._config.database.update('_User', query, updateFields) + return this._config.database + .update('_User', query, updateFields) .catch(err => { - if (err && err.code && err.message && err.code === 101 && err.message === 'Object not found.') { + if ( + err && + err.code && + err.message && + err.code === 101 && + err.message === 'Object not found.' + ) { return; // nothing to update so we are good } else { throw err; // unknown error @@ -104,15 +115,19 @@ export class AccountLockout { const query = { username: this._user.username, _account_lockout_expires_at: { $gt: Parse._encode(new Date()) }, - _failed_login_count: {$gte: this._config.accountLockout.threshold} + _failed_login_count: { $gte: this._config.accountLockout.threshold }, }; - return this._config.database.find('_User', query) - .then(users => { - if (Array.isArray(users) && users.length > 0) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Your account is locked due to multiple failed login attempts. Please try again after ' + this._config.accountLockout.duration + ' minute(s)'); - } - }); + return this._config.database.find('_User', query).then(users => { + if (Array.isArray(users) && users.length > 0) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Your account is locked due to multiple failed login attempts. Please try again after ' + + this._config.accountLockout.duration + + ' minute(s)' + ); + } + }); } /** @@ -139,16 +154,14 @@ export class AccountLockout { if (!this._config.accountLockout) { return Promise.resolve(); } - return this._notLocked() - .then(() => { - if (loginSuccessful) { - return this._setFailedLoginCount(0); - } else { - return this._handleFailedLoginAttempt(); - } - }); + return this._notLocked().then(() => { + if (loginSuccessful) { + return this._setFailedLoginCount(0); + } else { + return this._handleFailedLoginAttempt(); + } + }); } - } export default AccountLockout; diff --git a/src/Adapters/AdapterLoader.js b/src/Adapters/AdapterLoader.js index 9bc1601277..990b406b56 100644 --- a/src/Adapters/AdapterLoader.js +++ b/src/Adapters/AdapterLoader.js @@ -16,10 +16,10 @@ export function loadAdapter(adapter, defaultAdapter, options): T { } // Load from the default adapter when no adapter is set return loadAdapter(defaultAdapter, undefined, options); - } else if (typeof adapter === "function") { + } else if (typeof adapter === 'function') { try { return adapter(options); - } catch(e) { + } catch (e) { if (e.name === 'TypeError') { var Adapter = adapter; return new Adapter(options); @@ -27,7 +27,7 @@ export function loadAdapter(adapter, defaultAdapter, options): T { throw e; } } - } else if (typeof adapter === "string") { + } else if (typeof adapter === 'string') { /* eslint-disable */ adapter = require(adapter); // If it's define as a module, get the default diff --git a/src/Adapters/Analytics/AnalyticsAdapter.js b/src/Adapters/Analytics/AnalyticsAdapter.js index 1125e4594f..3fd50242d9 100644 --- a/src/Adapters/Analytics/AnalyticsAdapter.js +++ b/src/Adapters/Analytics/AnalyticsAdapter.js @@ -6,7 +6,6 @@ * @interface AnalyticsAdapter */ export class AnalyticsAdapter { - /** @param {any} parameters: the analytics request body, analytics info will be in the dimensions property @param {Request} req: the original http request diff --git a/src/Adapters/Auth/AuthAdapter.js b/src/Adapters/Auth/AuthAdapter.js index dd8fd838c3..28c1479eeb 100644 --- a/src/Adapters/Auth/AuthAdapter.js +++ b/src/Adapters/Auth/AuthAdapter.js @@ -1,6 +1,5 @@ /*eslint no-unused-vars: "off"*/ export class AuthAdapter { - /* @param appIds: the specified app ids in the configuration @param authData: the client provided authData diff --git a/src/Adapters/Auth/OAuth1Client.js b/src/Adapters/Auth/OAuth1Client.js index 01e4033f7b..4e7f9267d3 100644 --- a/src/Adapters/Auth/OAuth1Client.js +++ b/src/Adapters/Auth/OAuth1Client.js @@ -3,8 +3,11 @@ var https = require('https'), var Parse = require('parse/node').Parse; var OAuth = function(options) { - if(!options) { - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'No options passed to OAuth'); + if (!options) { + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + 'No options passed to OAuth' + ); } this.consumer_key = options.consumer_key; this.consumer_secret = options.consumer_secret; @@ -14,23 +17,24 @@ var OAuth = function(options) { this.oauth_params = options.oauth_params || {}; }; -OAuth.prototype.send = function(method, path, params, body){ - +OAuth.prototype.send = function(method, path, params, body) { var request = this.buildRequest(method, path, params, body); // Encode the body properly, the current Parse Implementation don't do it properly return new Promise(function(resolve, reject) { - var httpRequest = https.request(request, function(res) { - var data = ''; - res.on('data', function(chunk) { - data += chunk; - }); - res.on('end', function() { - data = JSON.parse(data); - resolve(data); + var httpRequest = https + .request(request, function(res) { + var data = ''; + res.on('data', function(chunk) { + data += chunk; + }); + res.on('end', function() { + data = JSON.parse(data); + resolve(data); + }); + }) + .on('error', function() { + reject('Failed to make an OAuth request'); }); - }).on('error', function() { - reject('Failed to make an OAuth request'); - }); if (request.body) { httpRequest.write(request.body); } @@ -39,40 +43,45 @@ OAuth.prototype.send = function(method, path, params, body){ }; OAuth.prototype.buildRequest = function(method, path, params, body) { - if (path.indexOf("/") != 0) { - path = "/" + path; + if (path.indexOf('/') != 0) { + path = '/' + path; } if (params && Object.keys(params).length > 0) { - path += "?" + OAuth.buildParameterString(params); + path += '?' + OAuth.buildParameterString(params); } var request = { - host: this.host, - path: path, - method: method.toUpperCase() + host: this.host, + path: path, + method: method.toUpperCase(), }; var oauth_params = this.oauth_params || {}; oauth_params.oauth_consumer_key = this.consumer_key; - if(this.auth_token){ - oauth_params["oauth_token"] = this.auth_token; + if (this.auth_token) { + oauth_params['oauth_token'] = this.auth_token; } - request = OAuth.signRequest(request, oauth_params, this.consumer_secret, this.auth_token_secret); + request = OAuth.signRequest( + request, + oauth_params, + this.consumer_secret, + this.auth_token_secret + ); if (body && Object.keys(body).length > 0) { request.body = OAuth.buildParameterString(body); } return request; -} +}; OAuth.prototype.get = function(path, params) { - return this.send("GET", path, params); -} + return this.send('GET', path, params); +}; OAuth.prototype.post = function(path, params, body) { - return this.send("POST", path, params, body); -} + return this.send('POST', path, params, body); +}; /* Proper string %escape encoding @@ -99,8 +108,7 @@ OAuth.encode = function(str) { // example 3: rawurlencode('http://www.google.nl/search?q=php.js&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:en-US:unofficial&client=firefox-a'); // returns 3: 'http%3A%2F%2Fwww.google.nl%2Fsearch%3Fq%3Dphp.js%26ie%3Dutf-8%26oe%3Dutf-8%26aq%3Dt%26rls%3Dcom.ubuntu%3Aen-US%3Aunofficial%26client%3Dfirefox-a' - str = (str + '') - .toString(); + str = (str + '').toString(); // Tilde should be allowed unescaped in future versions of PHP (as reflected below), but if you want to reflect current // PHP behavior, you would need to add ".replace(/~/g, '%7E');" to the following. @@ -110,55 +118,72 @@ OAuth.encode = function(str) { .replace(/\(/g, '%28') .replace(/\)/g, '%29') .replace(/\*/g, '%2A'); -} +}; -OAuth.signatureMethod = "HMAC-SHA1"; -OAuth.version = "1.0"; +OAuth.signatureMethod = 'HMAC-SHA1'; +OAuth.version = '1.0'; /* Generate a nonce */ -OAuth.nonce = function(){ - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; +OAuth.nonce = function() { + var text = ''; + var possible = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for(var i = 0; i < 30; i++) + for (var i = 0; i < 30; i++) text += possible.charAt(Math.floor(Math.random() * possible.length)); return text; -} +}; -OAuth.buildParameterString = function(obj){ +OAuth.buildParameterString = function(obj) { // Sort keys and encode values if (obj) { var keys = Object.keys(obj).sort(); // Map key=value, join them by & - return keys.map(function(key){ - return key + "=" + OAuth.encode(obj[key]); - }).join("&"); + return keys + .map(function(key) { + return key + '=' + OAuth.encode(obj[key]); + }) + .join('&'); } - return ""; -} + return ''; +}; /* Build the signature string from the object */ -OAuth.buildSignatureString = function(method, url, parameters){ - return [method.toUpperCase(), OAuth.encode(url), OAuth.encode(parameters)].join("&"); -} +OAuth.buildSignatureString = function(method, url, parameters) { + return [ + method.toUpperCase(), + OAuth.encode(url), + OAuth.encode(parameters), + ].join('&'); +}; /* Retuns encoded HMAC-SHA1 from key and text */ -OAuth.signature = function(text, key){ - crypto = require("crypto"); - return OAuth.encode(crypto.createHmac('sha1', key).update(text).digest('base64')); -} +OAuth.signature = function(text, key) { + crypto = require('crypto'); + return OAuth.encode( + crypto + .createHmac('sha1', key) + .update(text) + .digest('base64') + ); +}; -OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_token_secret){ +OAuth.signRequest = function( + request, + oauth_parameters, + consumer_secret, + auth_token_secret +) { oauth_parameters = oauth_parameters || {}; // Set default values @@ -175,20 +200,20 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to oauth_parameters.oauth_version = OAuth.version; } - if(!auth_token_secret){ - auth_token_secret = ""; + if (!auth_token_secret) { + auth_token_secret = ''; } // Force GET method if unset if (!request.method) { - request.method = "GET" + request.method = 'GET'; } // Collect all the parameters in one signatureParameters object var signatureParams = {}; var parametersToMerge = [request.params, request.body, oauth_parameters]; - for(var i in parametersToMerge) { + for (var i in parametersToMerge) { var parameters = parametersToMerge[i]; - for(var k in parameters) { + for (var k in parameters) { signatureParams[k] = parameters[k]; } } @@ -197,32 +222,41 @@ OAuth.signRequest = function(request, oauth_parameters, consumer_secret, auth_to var parameterString = OAuth.buildParameterString(signatureParams); // Build the signature string - var url = "https://" + request.host + "" + request.path; + var url = 'https://' + request.host + '' + request.path; - var signatureString = OAuth.buildSignatureString(request.method, url, parameterString); + var signatureString = OAuth.buildSignatureString( + request.method, + url, + parameterString + ); // Hash the signature string - var signatureKey = [OAuth.encode(consumer_secret), OAuth.encode(auth_token_secret)].join("&"); + var signatureKey = [ + OAuth.encode(consumer_secret), + OAuth.encode(auth_token_secret), + ].join('&'); var signature = OAuth.signature(signatureString, signatureKey); // Set the signature in the params oauth_parameters.oauth_signature = signature; - if(!request.headers){ + if (!request.headers) { request.headers = {}; } // Set the authorization header - var authHeader = Object.keys(oauth_parameters).sort().map(function(key){ - var value = oauth_parameters[key]; - return key + '="' + value + '"'; - }).join(", ") + var authHeader = Object.keys(oauth_parameters) + .sort() + .map(function(key) { + var value = oauth_parameters[key]; + return key + '="' + value + '"'; + }) + .join(', '); request.headers.Authorization = 'OAuth ' + authHeader; // Set the content type header - request.headers["Content-Type"] = "application/x-www-form-urlencoded"; + request.headers['Content-Type'] = 'application/x-www-form-urlencoded'; return request; - -} +}; module.exports = OAuth; diff --git a/src/Adapters/Auth/facebook.js b/src/Adapters/Auth/facebook.js index c2476e4a1a..fdd454df2e 100644 --- a/src/Adapters/Auth/facebook.js +++ b/src/Adapters/Auth/facebook.js @@ -4,15 +4,17 @@ var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - return graphRequest('me?fields=id&access_token=' + authData.access_token) - .then((data) => { - if (data && data.id == authData.id) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Facebook auth is invalid for this user.'); - }); + return graphRequest( + 'me?fields=id&access_token=' + authData.access_token + ).then(data => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook auth is invalid for this user.' + ); + }); } // Returns a promise that fulfills iff this app id is valid. @@ -21,17 +23,18 @@ function validateAppId(appIds, authData) { if (!appIds.length) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, - 'Facebook auth is not configured.'); + 'Facebook auth is not configured.' + ); } - return graphRequest('app?access_token=' + access_token) - .then((data) => { - if (data && appIds.indexOf(data.id) != -1) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Facebook auth is invalid for this user.'); - }); + return graphRequest('app?access_token=' + access_token).then(data => { + if (data && appIds.indexOf(data.id) != -1) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook auth is invalid for this user.' + ); + }); } // A promisey wrapper for FB graph requests. @@ -41,5 +44,5 @@ function graphRequest(path) { module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/facebookaccountkit.js b/src/Adapters/Auth/facebookaccountkit.js index 94a05e4b62..a650d23840 100644 --- a/src/Adapters/Auth/facebookaccountkit.js +++ b/src/Adapters/Auth/facebookaccountkit.js @@ -1,16 +1,20 @@ const crypto = require('crypto'); const httpsRequest = require('./httpsRequest'); -const Parse = require('parse/node').Parse; +const Parse = require('parse/node').Parse; -const graphRequest = (path) => { +const graphRequest = path => { return httpsRequest.get(`https://graph.accountkit.com/v1.1/${path}`); }; function getRequestPath(authData, options) { - const access_token = authData.access_token, appSecret = options && options.appSecret; + const access_token = authData.access_token, + appSecret = options && options.appSecret; if (appSecret) { - const appsecret_proof = crypto.createHmac("sha256", appSecret).update(access_token).digest('hex'); - return `me?access_token=${access_token}&appsecret_proof=${appsecret_proof}` + const appsecret_proof = crypto + .createHmac('sha256', appSecret) + .update(access_token) + .digest('hex'); + return `me?access_token=${access_token}&appsecret_proof=${appsecret_proof}`; } return `me?access_token=${access_token}`; } @@ -20,36 +24,37 @@ function validateAppId(appIds, authData, options) { return Promise.reject( new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, - 'Facebook app id for Account Kit is not configured.') - ) + 'Facebook app id for Account Kit is not configured.' + ) + ); } - return graphRequest(getRequestPath(authData, options)) - .then(data => { - if (data && data.application && appIds.indexOf(data.application.id) != -1) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Facebook app id for Account Kit is invalid for this user.'); - }) + return graphRequest(getRequestPath(authData, options)).then(data => { + if (data && data.application && appIds.indexOf(data.application.id) != -1) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook app id for Account Kit is invalid for this user.' + ); + }); } function validateAuthData(authData, options) { - return graphRequest(getRequestPath(authData, options)) - .then(data => { - if (data && data.error) { - throw data.error; - } - if (data && data.id == authData.id) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Facebook Account Kit auth is invalid for this user.'); - }) + return graphRequest(getRequestPath(authData, options)).then(data => { + if (data && data.error) { + throw data.error; + } + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Facebook Account Kit auth is invalid for this user.' + ); + }); } module.exports = { validateAppId, - validateAuthData + validateAuthData, }; diff --git a/src/Adapters/Auth/github.js b/src/Adapters/Auth/github.js index 2936af8637..c3a167fdaa 100644 --- a/src/Adapters/Auth/github.js +++ b/src/Adapters/Auth/github.js @@ -4,15 +4,15 @@ const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - return request('user', authData.access_token) - .then((data) => { - if (data && data.id == authData.id) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Github auth is invalid for this user.'); - }); + return request('user', authData.access_token).then(data => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Github auth is invalid for this user.' + ); + }); } // Returns a promise that fulfills iff this app id is valid. @@ -26,13 +26,13 @@ function request(path, access_token) { host: 'api.github.com', path: '/' + path, headers: { - 'Authorization': 'bearer ' + access_token, - 'User-Agent': 'parse-server' - } + Authorization: 'bearer ' + access_token, + 'User-Agent': 'parse-server', + }, }); } module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/google.js b/src/Adapters/Auth/google.js index 89b0dcf5d7..9dacabdd62 100644 --- a/src/Adapters/Auth/google.js +++ b/src/Adapters/Auth/google.js @@ -3,27 +3,27 @@ var Parse = require('parse/node').Parse; const httpsRequest = require('./httpsRequest'); function validateIdToken(id, token) { - return googleRequest("tokeninfo?id_token=" + token) - .then((response) => { - if (response && (response.sub == id || response.user_id == id)) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Google auth is invalid for this user.'); - }); + return googleRequest('tokeninfo?id_token=' + token).then(response => { + if (response && (response.sub == id || response.user_id == id)) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Google auth is invalid for this user.' + ); + }); } function validateAuthToken(id, token) { - return googleRequest("tokeninfo?access_token=" + token) - .then((response) => { - if (response && (response.sub == id || response.user_id == id)) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Google auth is invalid for this user.'); - }); + return googleRequest('tokeninfo?access_token=' + token).then(response => { + if (response && (response.sub == id || response.user_id == id)) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Google auth is invalid for this user.' + ); + }); } // Returns a promise that fulfills if this user id is valid. @@ -31,13 +31,16 @@ function validateAuthData(authData) { if (authData.id_token) { return validateIdToken(authData.id, authData.id_token); } else { - return validateAuthToken(authData.id, authData.access_token).then(() => { - // Validation with auth token worked - return; - }, () => { - // Try with the id_token param - return validateIdToken(authData.id, authData.access_token); - }); + return validateAuthToken(authData.id, authData.access_token).then( + () => { + // Validation with auth token worked + return; + }, + () => { + // Try with the id_token param + return validateIdToken(authData.id, authData.access_token); + } + ); } } @@ -48,10 +51,10 @@ function validateAppId() { // A promisey wrapper for api requests function googleRequest(path) { - return httpsRequest.get("https://www.googleapis.com/oauth2/v3/" + path); + return httpsRequest.get('https://www.googleapis.com/oauth2/v3/' + path); } module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/httpsRequest.js b/src/Adapters/Auth/httpsRequest.js index 193d57e32d..0233904048 100644 --- a/src/Adapters/Auth/httpsRequest.js +++ b/src/Adapters/Auth/httpsRequest.js @@ -3,7 +3,7 @@ const https = require('https'); function makeCallback(resolve, reject, noJSON) { return function(res) { let data = ''; - res.on('data', (chunk) => { + res.on('data', chunk => { data += chunk; }); res.on('end', () => { @@ -12,7 +12,7 @@ function makeCallback(resolve, reject, noJSON) { } try { data = JSON.parse(data); - } catch(e) { + } catch (e) { return reject(e); } resolve(data); diff --git a/src/Adapters/Auth/index.js b/src/Adapters/Auth/index.js index 77101935b5..9a9fa82b3f 100755 --- a/src/Adapters/Auth/index.js +++ b/src/Adapters/Auth/index.js @@ -2,20 +2,20 @@ import loadAdapter from '../AdapterLoader'; const facebook = require('./facebook'); const facebookaccountkit = require('./facebookaccountkit'); -const instagram = require("./instagram"); -const linkedin = require("./linkedin"); -const meetup = require("./meetup"); -const google = require("./google"); -const github = require("./github"); -const twitter = require("./twitter"); -const spotify = require("./spotify"); -const digits = require("./twitter"); // digits tokens are validated by twitter -const janrainengage = require("./janrainengage"); -const janraincapture = require("./janraincapture"); -const vkontakte = require("./vkontakte"); -const qq = require("./qq"); -const wechat = require("./wechat"); -const weibo = require("./weibo"); +const instagram = require('./instagram'); +const linkedin = require('./linkedin'); +const meetup = require('./meetup'); +const google = require('./google'); +const github = require('./github'); +const twitter = require('./twitter'); +const spotify = require('./spotify'); +const digits = require('./twitter'); // digits tokens are validated by twitter +const janrainengage = require('./janrainengage'); +const janraincapture = require('./janraincapture'); +const vkontakte = require('./vkontakte'); +const qq = require('./qq'); +const wechat = require('./wechat'); +const weibo = require('./weibo'); const anonymous = { validateAuthData: () => { @@ -23,8 +23,8 @@ const anonymous = { }, validateAppId: () => { return Promise.resolve(); - } -} + }, +}; const providers = { facebook, @@ -43,8 +43,8 @@ const providers = { vkontakte, qq, wechat, - weibo -} + weibo, +}; function authDataValidator(adapter, appIds, options) { return function(authData) { return adapter.validateAuthData(authData, options).then(() => { @@ -53,7 +53,7 @@ function authDataValidator(adapter, appIds, options) { } return Promise.resolve(); }); - } + }; } function loadAuthAdapter(provider, authOptions) { @@ -69,9 +69,13 @@ function loadAuthAdapter(provider, authOptions) { // Try the configuration methods if (providerOptions) { - const optionalAdapter = loadAdapter(providerOptions, undefined, providerOptions); + const optionalAdapter = loadAdapter( + providerOptions, + undefined, + providerOptions + ); if (optionalAdapter) { - ['validateAuthData', 'validateAppId'].forEach((key) => { + ['validateAuthData', 'validateAppId'].forEach(key => { if (optionalAdapter[key]) { adapter[key] = optionalAdapter[key]; } @@ -83,34 +87,32 @@ function loadAuthAdapter(provider, authOptions) { return; } - return {adapter, appIds, providerOptions}; + return { adapter, appIds, providerOptions }; } module.exports = function(authOptions = {}, enableAnonymousUsers = true) { let _enableAnonymousUsers = enableAnonymousUsers; const setEnableAnonymousUsers = function(enable) { _enableAnonymousUsers = enable; - } + }; // To handle the test cases on configuration const getValidatorForProvider = function(provider) { - if (provider === 'anonymous' && !_enableAnonymousUsers) { return; } - const { - adapter, - appIds, - providerOptions - } = loadAuthAdapter(provider, authOptions); + const { adapter, appIds, providerOptions } = loadAuthAdapter( + provider, + authOptions + ); return authDataValidator(adapter, appIds, providerOptions); - } + }; return Object.freeze({ getValidatorForProvider, - setEnableAnonymousUsers - }) -} + setEnableAnonymousUsers, + }); +}; module.exports.loadAuthAdapter = loadAuthAdapter; diff --git a/src/Adapters/Auth/instagram.js b/src/Adapters/Auth/instagram.js index 2bf6ffaab6..ee6302c138 100644 --- a/src/Adapters/Auth/instagram.js +++ b/src/Adapters/Auth/instagram.js @@ -4,15 +4,17 @@ const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - return request("users/self/?access_token=" + authData.access_token) - .then((response) => { + return request('users/self/?access_token=' + authData.access_token).then( + response => { if (response && response.data && response.data.id == authData.id) { return; } throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, - 'Instagram auth is invalid for this user.'); - }); + 'Instagram auth is invalid for this user.' + ); + } + ); } // Returns a promise that fulfills iff this app id is valid. @@ -22,10 +24,10 @@ function validateAppId() { // A promisey wrapper for api requests function request(path) { - return httpsRequest.get("https://api.instagram.com/v1/" + path); + return httpsRequest.get('https://api.instagram.com/v1/' + path); } module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/janraincapture.js b/src/Adapters/Auth/janraincapture.js index 05b33b71d3..fbff3c2421 100644 --- a/src/Adapters/Auth/janraincapture.js +++ b/src/Adapters/Auth/janraincapture.js @@ -5,15 +5,19 @@ const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData, options) { - return request(options.janrain_capture_host, authData.access_token) - .then((data) => { + return request(options.janrain_capture_host, authData.access_token).then( + data => { //successful response will have a "stat" (status) of 'ok' and a result node that stores the uuid, because that's all we asked for //see: https://docs.janrain.com/api/registration/entity/#entity if (data && data.stat == 'ok' && data.result == authData.id) { return; } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Janrain capture auth is invalid for this user.'); - }); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Janrain capture auth is invalid for this user.' + ); + } + ); } // Returns a promise that fulfills iff this app id is valid. @@ -24,10 +28,9 @@ function validateAppId() { // A promisey wrapper for api requests function request(host, access_token) { - var query_string_data = querystring.stringify({ - 'access_token': access_token, - 'attribute_name': 'uuid' // we only need to pull the uuid for this access token to make sure it matches + access_token: access_token, + attribute_name: 'uuid', // we only need to pull the uuid for this access token to make sure it matches }); return httpsRequest.get({ host: host, path: '/entity?' + query_string_data }); @@ -35,5 +38,5 @@ function request(host, access_token) { module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/janrainengage.js b/src/Adapters/Auth/janrainengage.js index 0c0bcde3e0..6e1589e724 100644 --- a/src/Adapters/Auth/janrainengage.js +++ b/src/Adapters/Auth/janrainengage.js @@ -5,15 +5,17 @@ var querystring = require('querystring'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData, options) { - return apiRequest(options.api_key, authData.auth_token) - .then((data) => { - //successful response will have a "stat" (status) of 'ok' and a profile node with an identifier - //see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data - if (data && data.stat == 'ok' && data.profile.identifier == authData.id) { - return; - } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Janrain engage auth is invalid for this user.'); - }); + return apiRequest(options.api_key, authData.auth_token).then(data => { + //successful response will have a "stat" (status) of 'ok' and a profile node with an identifier + //see: http://developers.janrain.com/overview/social-login/identity-providers/user-profile-data/#normalized-user-profile-data + if (data && data.stat == 'ok' && data.profile.identifier == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Janrain engage auth is invalid for this user.' + ); + }); } // Returns a promise that fulfills iff this app id is valid. @@ -24,11 +26,10 @@ function validateAppId() { // A promisey wrapper for api requests function apiRequest(api_key, auth_token) { - var post_data = querystring.stringify({ - 'token': auth_token, - 'apiKey': api_key, - 'format': 'json' + token: auth_token, + apiKey: api_key, + format: 'json', }); var post_options = { @@ -37,8 +38,8 @@ function apiRequest(api_key, auth_token) { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': post_data.length - } + 'Content-Length': post_data.length, + }, }; return httpsRequest.request(post_options, post_data); @@ -46,5 +47,5 @@ function apiRequest(api_key, auth_token) { module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/linkedin.js b/src/Adapters/Auth/linkedin.js index 8f6a837713..858ecbb367 100644 --- a/src/Adapters/Auth/linkedin.js +++ b/src/Adapters/Auth/linkedin.js @@ -4,15 +4,19 @@ const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - return request('people/~:(id)', authData.access_token, authData.is_mobile_sdk) - .then((data) => { - if (data && data.id == authData.id) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Linkedin auth is invalid for this user.'); - }); + return request( + 'people/~:(id)', + authData.access_token, + authData.is_mobile_sdk + ).then(data => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Linkedin auth is invalid for this user.' + ); + }); } // Returns a promise that fulfills iff this app id is valid. @@ -23,21 +27,21 @@ function validateAppId() { // A promisey wrapper for api requests function request(path, access_token, is_mobile_sdk) { var headers = { - 'Authorization': 'Bearer ' + access_token, + Authorization: 'Bearer ' + access_token, 'x-li-format': 'json', - } + }; - if(is_mobile_sdk) { + if (is_mobile_sdk) { headers['x-li-src'] = 'msdk'; } return httpsRequest.get({ host: 'api.linkedin.com', path: '/v1/' + path, - headers: headers + headers: headers, }); } module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/meetup.js b/src/Adapters/Auth/meetup.js index a484e68cec..d949a65e4a 100644 --- a/src/Adapters/Auth/meetup.js +++ b/src/Adapters/Auth/meetup.js @@ -4,15 +4,15 @@ const httpsRequest = require('./httpsRequest'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - return request('member/self', authData.access_token) - .then((data) => { - if (data && data.id == authData.id) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Meetup auth is invalid for this user.'); - }); + return request('member/self', authData.access_token).then(data => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Meetup auth is invalid for this user.' + ); + }); } // Returns a promise that fulfills iff this app id is valid. @@ -26,12 +26,12 @@ function request(path, access_token) { host: 'api.meetup.com', path: '/2/' + path, headers: { - 'Authorization': 'bearer ' + access_token - } + Authorization: 'bearer ' + access_token, + }, }); } module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/qq.js b/src/Adapters/Auth/qq.js index a64193cf0f..45e776665e 100644 --- a/src/Adapters/Auth/qq.js +++ b/src/Adapters/Auth/qq.js @@ -4,11 +4,16 @@ var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - return graphRequest('me?access_token=' + authData.access_token).then(function (data) { + return graphRequest('me?access_token=' + authData.access_token).then(function( + data + ) { if (data && data.openid == authData.id) { return; } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'qq auth is invalid for this user.' + ); }); } @@ -19,18 +24,23 @@ function validateAppId() { // A promisey wrapper for qq graph requests. function graphRequest(path) { - return httpsRequest.get('https://graph.qq.com/oauth2.0/' + path, true).then((data) => { - return parseResponseData(data); - }); + return httpsRequest + .get('https://graph.qq.com/oauth2.0/' + path, true) + .then(data => { + return parseResponseData(data); + }); } function parseResponseData(data) { - const starPos = data.indexOf("("); - const endPos = data.indexOf(")"); - if(starPos == -1 || endPos == -1){ - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); + const starPos = data.indexOf('('); + const endPos = data.indexOf(')'); + if (starPos == -1 || endPos == -1) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'qq auth is invalid for this user.' + ); } - data = data.substring(starPos + 1,endPos - 1); + data = data.substring(starPos + 1, endPos - 1); return JSON.parse(data); } diff --git a/src/Adapters/Auth/spotify.js b/src/Adapters/Auth/spotify.js index f30bea09fb..1bafc44ba3 100644 --- a/src/Adapters/Auth/spotify.js +++ b/src/Adapters/Auth/spotify.js @@ -4,15 +4,15 @@ var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - return request('me', authData.access_token) - .then((data) => { - if (data && data.id == authData.id) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Spotify auth is invalid for this user.'); - }); + return request('me', authData.access_token).then(data => { + if (data && data.id == authData.id) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Spotify auth is invalid for this user.' + ); + }); } // Returns a promise that fulfills if this app id is valid. @@ -21,17 +21,18 @@ function validateAppId(appIds, authData) { if (!appIds.length) { throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, - 'Spotify auth is not configured.'); + 'Spotify auth is not configured.' + ); } - return request('me', access_token) - .then((data) => { - if (data && appIds.indexOf(data.id) != -1) { - return; - } - throw new Parse.Error( - Parse.Error.OBJECT_NOT_FOUND, - 'Spotify auth is invalid for this user.'); - }); + return request('me', access_token).then(data => { + if (data && appIds.indexOf(data.id) != -1) { + return; + } + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Spotify auth is invalid for this user.' + ); + }); } // A promisey wrapper for Spotify API requests. @@ -40,12 +41,12 @@ function request(path, access_token) { host: 'api.spotify.com', path: '/v1/' + path, headers: { - 'Authorization': 'Bearer ' + access_token - } + Authorization: 'Bearer ' + access_token, + }, }); } module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/twitter.js b/src/Adapters/Auth/twitter.js index ee8517819e..26f187e0c9 100644 --- a/src/Adapters/Auth/twitter.js +++ b/src/Adapters/Auth/twitter.js @@ -5,22 +5,26 @@ var logger = require('../../logger').default; // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData, options) { - if(!options) { - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing'); + if (!options) { + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + 'Twitter auth configuration missing' + ); } options = handleMultipleConfigurations(authData, options); var client = new OAuth(options); - client.host = "api.twitter.com"; + client.host = 'api.twitter.com'; client.auth_token = authData.auth_token; client.auth_token_secret = authData.auth_token_secret; - return client.get("/1.1/account/verify_credentials.json").then((data) => { + return client.get('/1.1/account/verify_credentials.json').then(data => { if (data && data.id_str == '' + authData.id) { return; } throw new Parse.Error( Parse.Error.OBJECT_NOT_FOUND, - 'Twitter auth is invalid for this user.'); + 'Twitter auth is invalid for this user.' + ); }); } @@ -33,16 +37,28 @@ function handleMultipleConfigurations(authData, options) { if (Array.isArray(options)) { const consumer_key = authData.consumer_key; if (!consumer_key) { - logger.error('Twitter Auth', 'Multiple twitter configurations are available, by no consumer_key was sent by the client.'); - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.'); + logger.error( + 'Twitter Auth', + 'Multiple twitter configurations are available, by no consumer_key was sent by the client.' + ); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Twitter auth is invalid for this user.' + ); } - options = options.filter((option) => { + options = options.filter(option => { return option.consumer_key == consumer_key; }); if (options.length == 0) { - logger.error('Twitter Auth','Cannot find a configuration for the provided consumer_key'); - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Twitter auth is invalid for this user.'); + logger.error( + 'Twitter Auth', + 'Cannot find a configuration for the provided consumer_key' + ); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Twitter auth is invalid for this user.' + ); } options = options[0]; } @@ -52,5 +68,5 @@ function handleMultipleConfigurations(authData, options) { module.exports = { validateAppId, validateAuthData, - handleMultipleConfigurations + handleMultipleConfigurations, }; diff --git a/src/Adapters/Auth/vkontakte.js b/src/Adapters/Auth/vkontakte.js index afaa1ee4c7..149a2f54f3 100644 --- a/src/Adapters/Auth/vkontakte.js +++ b/src/Adapters/Auth/vkontakte.js @@ -8,29 +8,62 @@ var logger = require('../../logger').default; // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData, params) { - return vkOAuth2Request(params).then(function (response) { + return vkOAuth2Request(params).then(function(response) { if (response && response.access_token) { - return request("api.vk.com", "method/users.get?access_token=" + authData.access_token + "&v=5.8").then(function (response) { - if (response && response.response && response.response.length && response.response[0].id == authData.id) { + return request( + 'api.vk.com', + 'method/users.get?access_token=' + authData.access_token + '&v=5.8' + ).then(function(response) { + if ( + response && + response.response && + response.response.length && + response.response[0].id == authData.id + ) { return; } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is invalid for this user.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Vk auth is invalid for this user.' + ); }); } logger.error('Vk Auth', 'Vk appIds or appSecret is incorrect.'); - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk appIds or appSecret is incorrect.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Vk appIds or appSecret is incorrect.' + ); }); } function vkOAuth2Request(params) { - return new Promise(function (resolve) { - if (!params || !params.appIds || !params.appIds.length || !params.appSecret || !params.appSecret.length) { - logger.error('Vk Auth', 'Vk auth is not configured. Missing appIds or appSecret.'); - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Vk auth is not configured. Missing appIds or appSecret.'); + return new Promise(function(resolve) { + if ( + !params || + !params.appIds || + !params.appIds.length || + !params.appSecret || + !params.appSecret.length + ) { + logger.error( + 'Vk Auth', + 'Vk auth is not configured. Missing appIds or appSecret.' + ); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Vk auth is not configured. Missing appIds or appSecret.' + ); } resolve(); - }).then(function () { - return request("oauth.vk.com", "access_token?client_id=" + params.appIds + "&client_secret=" + params.appSecret + "&v=5.59&grant_type=client_credentials"); + }).then(function() { + return request( + 'oauth.vk.com', + 'access_token?client_id=' + + params.appIds + + '&client_secret=' + + params.appSecret + + '&v=5.59&grant_type=client_credentials' + ); }); } @@ -41,10 +74,10 @@ function validateAppId() { // A promisey wrapper for api requests function request(host, path) { - return httpsRequest.get("https://" + host + "/" + path); + return httpsRequest.get('https://' + host + '/' + path); } module.exports = { validateAppId: validateAppId, - validateAuthData: validateAuthData + validateAuthData: validateAuthData, }; diff --git a/src/Adapters/Auth/wechat.js b/src/Adapters/Auth/wechat.js index d192df2535..56c2293b52 100644 --- a/src/Adapters/Auth/wechat.js +++ b/src/Adapters/Auth/wechat.js @@ -4,11 +4,16 @@ var Parse = require('parse/node').Parse; // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - return graphRequest('auth?access_token=' + authData.access_token + '&openid=' + authData.id).then(function (data) { + return graphRequest( + 'auth?access_token=' + authData.access_token + '&openid=' + authData.id + ).then(function(data) { if (data.errcode == 0) { return; } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'wechat auth is invalid for this user.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'wechat auth is invalid for this user.' + ); }); } @@ -24,5 +29,5 @@ function graphRequest(path) { module.exports = { validateAppId, - validateAuthData + validateAuthData, }; diff --git a/src/Adapters/Auth/weibo.js b/src/Adapters/Auth/weibo.js index 8d4a3a10f5..bcdaf11a36 100644 --- a/src/Adapters/Auth/weibo.js +++ b/src/Adapters/Auth/weibo.js @@ -5,11 +5,14 @@ var querystring = require('querystring'); // Returns a promise that fulfills iff this user id is valid. function validateAuthData(authData) { - return graphRequest(authData.access_token).then(function (data) { + return graphRequest(authData.access_token).then(function(data) { if (data && data.uid == authData.id) { return; } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'weibo auth is invalid for this user.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'weibo auth is invalid for this user.' + ); }); } @@ -21,7 +24,7 @@ function validateAppId() { // A promisey wrapper for weibo graph requests. function graphRequest(access_token) { var postData = querystring.stringify({ - "access_token": access_token + access_token: access_token, }); var options = { hostname: 'api.weibo.com', @@ -29,13 +32,13 @@ function graphRequest(access_token) { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(postData) - } + 'Content-Length': Buffer.byteLength(postData), + }, }; return httpsRequest.request(options, postData); } module.exports = { validateAppId, - validateAuthData + validateAuthData, }; diff --git a/src/Adapters/Cache/InMemoryCache.js b/src/Adapters/Cache/InMemoryCache.js index 38a0e0f39a..c97f82e34d 100644 --- a/src/Adapters/Cache/InMemoryCache.js +++ b/src/Adapters/Cache/InMemoryCache.js @@ -1,10 +1,7 @@ const DEFAULT_CACHE_TTL = 5 * 1000; - export class InMemoryCache { - constructor({ - ttl = DEFAULT_CACHE_TTL - }) { + constructor({ ttl = DEFAULT_CACHE_TTL }) { this.ttl = ttl; this.cache = Object.create(null); } @@ -32,8 +29,8 @@ export class InMemoryCache { var record = { value: value, - expire: ttl + Date.now() - } + expire: ttl + Date.now(), + }; if (!isNaN(record.expire)) { record.timeout = setTimeout(() => { @@ -59,7 +56,6 @@ export class InMemoryCache { clear() { this.cache = Object.create(null); } - } export default InMemoryCache; diff --git a/src/Adapters/Cache/InMemoryCacheAdapter.js b/src/Adapters/Cache/InMemoryCacheAdapter.js index 585e2eadde..e8036c51da 100644 --- a/src/Adapters/Cache/InMemoryCacheAdapter.js +++ b/src/Adapters/Cache/InMemoryCacheAdapter.js @@ -1,9 +1,8 @@ -import {LRUCache} from './LRUCache'; +import { LRUCache } from './LRUCache'; export class InMemoryCacheAdapter { - constructor(ctx) { - this.cache = new LRUCache(ctx) + this.cache = new LRUCache(ctx); } get(key) { diff --git a/src/Adapters/Cache/LRUCache.js b/src/Adapters/Cache/LRUCache.js index a580768a74..889ca3d015 100644 --- a/src/Adapters/Cache/LRUCache.js +++ b/src/Adapters/Cache/LRUCache.js @@ -1,14 +1,11 @@ import LRU from 'lru-cache'; -import defaults from '../../defaults'; +import defaults from '../../defaults'; export class LRUCache { - constructor({ - ttl = defaults.cacheTTL, - maxSize = defaults.cacheMaxSize, - }) { + constructor({ ttl = defaults.cacheTTL, maxSize = defaults.cacheMaxSize }) { this.cache = new LRU({ max: maxSize, - maxAge: ttl + maxAge: ttl, }); } @@ -27,7 +24,6 @@ export class LRUCache { clear() { this.cache.reset(); } - } export default LRUCache; diff --git a/src/Adapters/Cache/NullCacheAdapter.js b/src/Adapters/Cache/NullCacheAdapter.js index aafd8eaa95..812ee2ee38 100644 --- a/src/Adapters/Cache/NullCacheAdapter.js +++ b/src/Adapters/Cache/NullCacheAdapter.js @@ -1,11 +1,10 @@ export class NullCacheAdapter { - constructor() {} get() { - return new Promise((resolve) => { + return new Promise(resolve => { return resolve(null); - }) + }); } put() { diff --git a/src/Adapters/Cache/RedisCacheAdapter.js b/src/Adapters/Cache/RedisCacheAdapter.js index a59f1c7e4e..bc8bf89445 100644 --- a/src/Adapters/Cache/RedisCacheAdapter.js +++ b/src/Adapters/Cache/RedisCacheAdapter.js @@ -8,7 +8,6 @@ function debug() { } export class RedisCacheAdapter { - constructor(redisCtx, ttl = DEFAULT_REDIS_TTL) { this.client = redis.createClient(redisCtx); this.p = Promise.resolve(); @@ -18,10 +17,10 @@ export class RedisCacheAdapter { get(key) { debug('get', key); this.p = this.p.then(() => { - return new Promise((resolve) => { + return new Promise(resolve => { this.client.get(key, function(err, res) { debug('-> get', key, res); - if(!res) { + if (!res) { return resolve(null); } resolve(JSON.parse(res)); @@ -41,7 +40,7 @@ export class RedisCacheAdapter { ttl = DEFAULT_REDIS_TTL; } this.p = this.p.then(() => { - return new Promise((resolve) => { + return new Promise(resolve => { if (ttl === Infinity) { this.client.set(key, value, function() { resolve(); @@ -59,7 +58,7 @@ export class RedisCacheAdapter { del(key) { debug('del', key); this.p = this.p.then(() => { - return new Promise((resolve) => { + return new Promise(resolve => { this.client.del(key, function() { resolve(); }); @@ -71,7 +70,7 @@ export class RedisCacheAdapter { clear() { debug('clear'); this.p = this.p.then(() => { - return new Promise((resolve) => { + return new Promise(resolve => { this.client.flushdb(function() { resolve(); }); diff --git a/src/Adapters/Files/FilesAdapter.js b/src/Adapters/Files/FilesAdapter.js index d5d2592e28..836f828c40 100644 --- a/src/Adapters/Files/FilesAdapter.js +++ b/src/Adapters/Files/FilesAdapter.js @@ -13,7 +13,7 @@ // and for the API server to be using the DatabaseController with Mongo // database adapter. -import type { Config } from '../../Config' +import type { Config } from '../../Config'; /** * @module Adapters */ @@ -21,7 +21,6 @@ import type { Config } from '../../Config' * @interface FilesAdapter */ export class FilesAdapter { - /** Responsible for storing the file in order to be retrieved later by its filename * * @param {string} filename - the filename to save @@ -31,7 +30,7 @@ export class FilesAdapter { * * @return {Promise} a promise that should fail if the storage didn't succeed */ - createFile(filename: string, data, contentType: string): Promise { } + createFile(filename: string, data, contentType: string): Promise {} /** Responsible for deleting the specified file * @@ -39,7 +38,7 @@ export class FilesAdapter { * * @return {Promise} a promise that should fail if the deletion didn't succeed */ - deleteFile(filename: string): Promise { } + deleteFile(filename: string): Promise {} /** Responsible for retrieving the data of the specified file * @@ -47,7 +46,7 @@ export class FilesAdapter { * * @return {Promise} a promise that should pass with the file data or fail on error */ - getFileData(filename: string): Promise { } + getFileData(filename: string): Promise {} /** Returns an absolute URL where the file can be accessed * @@ -56,7 +55,7 @@ export class FilesAdapter { * * @return {string} Absolute URL */ - getFileLocation(config: Config, filename: string): string { } + getFileLocation(config: Config, filename: string): string {} } export default FilesAdapter; diff --git a/src/Adapters/Files/GridStoreAdapter.js b/src/Adapters/Files/GridStoreAdapter.js index 2a33b1af08..aa5885fc3f 100644 --- a/src/Adapters/Files/GridStoreAdapter.js +++ b/src/Adapters/Files/GridStoreAdapter.js @@ -7,9 +7,9 @@ */ // @flow-disable-next -import { MongoClient, GridStore, Db} from 'mongodb'; -import { FilesAdapter } from './FilesAdapter'; -import defaults from '../../defaults'; +import { MongoClient, GridStore, Db } from 'mongodb'; +import { FilesAdapter } from './FilesAdapter'; +import defaults from '../../defaults'; export class GridStoreAdapter extends FilesAdapter { _databaseURI: string; @@ -22,8 +22,9 @@ export class GridStoreAdapter extends FilesAdapter { _connect() { if (!this._connectionPromise) { - this._connectionPromise = MongoClient.connect(this._databaseURI) - .then((client) => client.db(client.s.options.dbName)); + this._connectionPromise = MongoClient.connect(this._databaseURI).then( + client => client.db(client.s.options.dbName) + ); } return this._connectionPromise; } @@ -31,41 +32,54 @@ export class GridStoreAdapter extends FilesAdapter { // For a given config object, filename, and data, store a file // Returns a promise createFile(filename: string, data) { - return this._connect().then((database) => { - const gridStore = new GridStore(database, filename, 'w'); - return gridStore.open(); - }).then(gridStore => { - return gridStore.write(data); - }).then(gridStore => { - return gridStore.close(); - }); + return this._connect() + .then(database => { + const gridStore = new GridStore(database, filename, 'w'); + return gridStore.open(); + }) + .then(gridStore => { + return gridStore.write(data); + }) + .then(gridStore => { + return gridStore.close(); + }); } deleteFile(filename: string) { - return this._connect().then(database => { - const gridStore = new GridStore(database, filename, 'r'); - return gridStore.open(); - }).then((gridStore) => { - return gridStore.unlink(); - }).then((gridStore) => { - return gridStore.close(); - }); + return this._connect() + .then(database => { + const gridStore = new GridStore(database, filename, 'r'); + return gridStore.open(); + }) + .then(gridStore => { + return gridStore.unlink(); + }) + .then(gridStore => { + return gridStore.close(); + }); } getFileData(filename: string) { - return this._connect().then(database => { - return GridStore.exist(database, filename) - .then(() => { + return this._connect() + .then(database => { + return GridStore.exist(database, filename).then(() => { const gridStore = new GridStore(database, filename, 'r'); return gridStore.open(); }); - }).then(gridStore => { - return gridStore.read(); - }); + }) + .then(gridStore => { + return gridStore.read(); + }); } getFileLocation(config, filename) { - return (config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename)); + return ( + config.mount + + '/files/' + + config.applicationId + + '/' + + encodeURIComponent(filename) + ); } getFileStream(filename: string) { diff --git a/src/Adapters/Logger/LoggerAdapter.js b/src/Adapters/Logger/LoggerAdapter.js index 9bfca5fae5..21df5ccd48 100644 --- a/src/Adapters/Logger/LoggerAdapter.js +++ b/src/Adapters/Logger/LoggerAdapter.js @@ -16,7 +16,7 @@ export class LoggerAdapter { * @param {String} message * @param {Object} metadata */ - log(level, message, /* meta */) {} + log(level, message /* meta */) {} } export default LoggerAdapter; diff --git a/src/Adapters/Logger/WinstonLogger.js b/src/Adapters/Logger/WinstonLogger.js index 4cf75cb7ae..ba834bcc3c 100644 --- a/src/Adapters/Logger/WinstonLogger.js +++ b/src/Adapters/Logger/WinstonLogger.js @@ -3,7 +3,7 @@ import fs from 'fs'; import path from 'path'; import DailyRotateFile from 'winston-daily-rotate-file'; import _ from 'lodash'; -import defaults from '../../defaults'; +import defaults from '../../defaults'; const logger = new winston.Logger(); const additionalTransports = []; @@ -17,31 +17,47 @@ function updateTransports(options) { delete transports['parse-server']; delete transports['parse-server-error']; } else if (!_.isUndefined(options.dirname)) { - transports['parse-server'] = new (DailyRotateFile)( - Object.assign({}, { - filename: 'parse-server.info', - name: 'parse-server', - }, options, { timestamp: true })); - transports['parse-server-error'] = new (DailyRotateFile)( - Object.assign({}, { - filename: 'parse-server.err', - name: 'parse-server-error', - }, options, { level: 'error', timestamp: true })); + transports['parse-server'] = new DailyRotateFile( + Object.assign( + {}, + { + filename: 'parse-server.info', + name: 'parse-server', + }, + options, + { timestamp: true } + ) + ); + transports['parse-server-error'] = new DailyRotateFile( + Object.assign( + {}, + { + filename: 'parse-server.err', + name: 'parse-server-error', + }, + options, + { level: 'error', timestamp: true } + ) + ); } - transports.console = new (winston.transports.Console)( - Object.assign({ - colorize: true, - name: 'console', - silent - }, options)); + transports.console = new winston.transports.Console( + Object.assign( + { + colorize: true, + name: 'console', + silent, + }, + options + ) + ); } // Mount the additional transports - additionalTransports.forEach((transport) => { + additionalTransports.forEach(transport => { transports[transport.name] = transport; }); logger.configure({ - transports: _.values(transports) + transports: _.values(transports), }); } @@ -50,8 +66,8 @@ export function configureLogger({ jsonLogs = defaults.jsonLogs, logLevel = winston.level, verbose = defaults.verbose, - silent = defaults.silent } = {}) { - + silent = defaults.silent, +} = {}) { if (verbose) { logLevel = 'verbose'; } @@ -65,7 +81,9 @@ export function configureLogger({ } try { fs.mkdirSync(logsFolder); - } catch (e) { /* */ } + } catch (e) { + /* */ + } } options.dirname = logsFolder; options.level = logLevel; @@ -84,13 +102,14 @@ export function addTransport(transport) { } export function removeTransport(transport) { - const transportName = typeof transport == 'string' ? transport : transport.name; + const transportName = + typeof transport == 'string' ? transport : transport.name; const transports = Object.assign({}, logger.transports); delete transports[transportName]; logger.configure({ - transports: _.values(transports) + transports: _.values(transports), }); - _.remove(additionalTransports, (transport) => { + _.remove(additionalTransports, transport => { return transport.name === transportName; }); } diff --git a/src/Adapters/Logger/WinstonLoggerAdapter.js b/src/Adapters/Logger/WinstonLoggerAdapter.js index 70616bbd56..7914f41fd6 100644 --- a/src/Adapters/Logger/WinstonLoggerAdapter.js +++ b/src/Adapters/Logger/WinstonLoggerAdapter.js @@ -28,7 +28,8 @@ export class WinstonLoggerAdapter extends LoggerAdapter { options = {}; } // defaults to 7 days prior - const from = options.from || new Date(Date.now() - (7 * MILLISECONDS_IN_A_DAY)); + const from = + options.from || new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); const until = options.until || new Date(); const limit = options.size || 10; const order = options.order || 'desc'; @@ -38,7 +39,7 @@ export class WinstonLoggerAdapter extends LoggerAdapter { from, until, limit, - order + order, }; return new Promise((resolve, reject) => { @@ -54,7 +55,7 @@ export class WinstonLoggerAdapter extends LoggerAdapter { callback(res['parse-server']); resolve(res['parse-server']); } - }) + }); }); } } diff --git a/src/Adapters/MessageQueue/EventEmitterMQ.js b/src/Adapters/MessageQueue/EventEmitterMQ.js index e23bc83d59..1f0081aad5 100644 --- a/src/Adapters/MessageQueue/EventEmitterMQ.js +++ b/src/Adapters/MessageQueue/EventEmitterMQ.js @@ -35,9 +35,9 @@ class Consumer extends events.EventEmitter { subscribe(channel: string): void { unsubscribe(channel); - const handler = (message) => { + const handler = message => { this.emit('message', channel, message); - } + }; subscriptions.set(channel, handler); this.emitter.on(channel, handler); } @@ -57,9 +57,7 @@ function createSubscriber(): any { const EventEmitterMQ = { createPublisher, - createSubscriber -} + createSubscriber, +}; -export { - EventEmitterMQ -} +export { EventEmitterMQ }; diff --git a/src/Adapters/PubSub/EventEmitterPubSub.js b/src/Adapters/PubSub/EventEmitterPubSub.js index f81388192b..1ecc006e0c 100644 --- a/src/Adapters/PubSub/EventEmitterPubSub.js +++ b/src/Adapters/PubSub/EventEmitterPubSub.js @@ -25,9 +25,9 @@ class Subscriber extends events.EventEmitter { } subscribe(channel: string): void { - const handler = (message) => { + const handler = message => { this.emit('message', channel, message); - } + }; this.subscriptions.set(channel, handler); this.emitter.on(channel, handler); } @@ -51,9 +51,7 @@ function createSubscriber(): any { const EventEmitterPubSub = { createPublisher, - createSubscriber -} + createSubscriber, +}; -export { - EventEmitterPubSub -} +export { EventEmitterPubSub }; diff --git a/src/Adapters/PubSub/PubSubAdapter.js b/src/Adapters/PubSub/PubSubAdapter.js index 67cc1a54c4..9e6b13dfc7 100644 --- a/src/Adapters/PubSub/PubSubAdapter.js +++ b/src/Adapters/PubSub/PubSubAdapter.js @@ -25,7 +25,7 @@ interface Publisher { * @param {String} channel the channel in which to publish * @param {String} message the message to publish */ - publish(channel: string, message: string):void; + publish(channel: string, message: string): void; } /** diff --git a/src/Adapters/PubSub/RedisPubSub.js b/src/Adapters/PubSub/RedisPubSub.js index 7fb62dfca2..2e7452a938 100644 --- a/src/Adapters/PubSub/RedisPubSub.js +++ b/src/Adapters/PubSub/RedisPubSub.js @@ -1,18 +1,16 @@ import redis from 'redis'; -function createPublisher({redisURL}): any { +function createPublisher({ redisURL }): any { return redis.createClient(redisURL, { no_ready_check: true }); } -function createSubscriber({redisURL}): any { +function createSubscriber({ redisURL }): any { return redis.createClient(redisURL, { no_ready_check: true }); } const RedisPubSub = { createPublisher, - createSubscriber -} + createSubscriber, +}; -export { - RedisPubSub -} +export { RedisPubSub }; diff --git a/src/Adapters/Push/PushAdapter.js b/src/Adapters/Push/PushAdapter.js index e1f400f84b..191fa15b40 100644 --- a/src/Adapters/Push/PushAdapter.js +++ b/src/Adapters/Push/PushAdapter.js @@ -31,7 +31,7 @@ export class PushAdapter { * @returns {Array} An array of valid push types */ getValidPushTypes(): string[] { - return [] + return []; } } diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 128395121b..55ef7868f4 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -2,9 +2,9 @@ const mongodb = require('mongodb'); const Collection = mongodb.Collection; export default class MongoCollection { - _mongoCollection:Collection; + _mongoCollection: Collection; - constructor(mongoCollection:Collection) { + constructor(mongoCollection: Collection) { this._mongoCollection = mongoCollection; } @@ -15,33 +15,58 @@ export default class MongoCollection { // idea. Or even if this behavior is a good idea. find(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) { // Support for Full Text Search - $text - if(keys && keys.$score) { + if (keys && keys.$score) { delete keys.$score; - keys.score = {$meta: 'textScore'}; + keys.score = { $meta: 'textScore' }; } - return this._rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference }) - .catch(error => { - // Check for "no geoindex" error - if (error.code != 17007 && !error.message.match(/unable to find index for .geoNear/)) { - throw error; - } - // Figure out what key needs an index - const key = error.message.match(/field=([A-Za-z_0-9]+) /)[1]; - if (!key) { - throw error; - } - - var index = {}; - index[key] = '2d'; - return this._mongoCollection.createIndex(index) + return this._rawFind(query, { + skip, + limit, + sort, + keys, + maxTimeMS, + readPreference, + }).catch(error => { + // Check for "no geoindex" error + if ( + error.code != 17007 && + !error.message.match(/unable to find index for .geoNear/) + ) { + throw error; + } + // Figure out what key needs an index + const key = error.message.match(/field=([A-Za-z_0-9]+) /)[1]; + if (!key) { + throw error; + } + + var index = {}; + index[key] = '2d'; + return ( + this._mongoCollection + .createIndex(index) // Retry, but just once. - .then(() => this._rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference })); - }); + .then(() => + this._rawFind(query, { + skip, + limit, + sort, + keys, + maxTimeMS, + readPreference, + }) + ) + ); + }); } _rawFind(query, { skip, limit, sort, keys, maxTimeMS, readPreference } = {}) { - let findOperation = this._mongoCollection - .find(query, { skip, limit, sort, readPreference }) + let findOperation = this._mongoCollection.find(query, { + skip, + limit, + sort, + readPreference, + }); if (keys) { findOperation = findOperation.project(keys); @@ -55,7 +80,13 @@ export default class MongoCollection { } count(query, { skip, limit, sort, maxTimeMS, readPreference } = {}) { - const countOperation = this._mongoCollection.count(query, { skip, limit, sort, maxTimeMS, readPreference }); + const countOperation = this._mongoCollection.count(query, { + skip, + limit, + sort, + maxTimeMS, + readPreference, + }); return countOperation; } @@ -65,7 +96,9 @@ export default class MongoCollection { } aggregate(pipeline, { maxTimeMS, readPreference } = {}) { - return this._mongoCollection.aggregate(pipeline, { maxTimeMS, readPreference }).toArray(); + return this._mongoCollection + .aggregate(pipeline, { maxTimeMS, readPreference }) + .toArray(); } insertOne(object) { @@ -76,7 +109,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) { @@ -93,13 +126,17 @@ export default class MongoCollection { _ensureSparseUniqueIndexInBackground(indexRequest) { return new Promise((resolve, reject) => { - this._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, (error) => { - if (error) { - reject(error); - } else { - resolve(); + this._mongoCollection.ensureIndex( + indexRequest, + { unique: true, background: true, sparse: true }, + error => { + if (error) { + reject(error); + } else { + resolve(); + } } - }); + ); }); } diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index db52fd4479..e2b2b2cbd1 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -1,5 +1,5 @@ import MongoCollection from './MongoCollection'; -import Parse from 'parse/node'; +import Parse from 'parse/node'; function mongoFieldToParseSchemaField(type) { if (type[0] === '*') { @@ -15,31 +15,43 @@ function mongoFieldToParseSchemaField(type) { }; } 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'}; - case 'polygon': return {type: 'Polygon'}; + 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' }; + case 'polygon': + return { type: 'Polygon' }; } } const nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions']; function mongoSchemaFieldsToParseSchemaFields(schema) { - var fieldNames = Object.keys(schema).filter(key => nonFieldSchemaKeys.indexOf(key) === -1); + var fieldNames = Object.keys(schema).filter( + key => nonFieldSchemaKeys.indexOf(key) === -1 + ); var response = fieldNames.reduce((obj, fieldName) => { - obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName]) + obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName]); return obj; }, {}); - response.ACL = {type: 'ACL'}; - response.createdAt = {type: 'Date'}; - response.updatedAt = {type: 'Date'}; - response.objectId = {type: 'String'}; + response.ACL = { type: 'ACL' }; + response.createdAt = { type: 'Date' }; + response.updatedAt = { type: 'Date' }; + response.objectId = { type: 'String' }; return response; } @@ -53,23 +65,23 @@ const emptyCLPS = Object.freeze({ }); const defaultCLPS = Object.freeze({ - find: {'*': true}, - get: {'*': true}, - create: {'*': true}, - update: {'*': true}, - delete: {'*': true}, - addField: {'*': true}, + find: { '*': true }, + get: { '*': true }, + create: { '*': true }, + update: { '*': true }, + delete: { '*': true }, + addField: { '*': true }, }); function mongoSchemaToParseSchema(mongoSchema) { let clps = defaultCLPS; - let indexes = {} + let indexes = {}; if (mongoSchema._metadata) { if (mongoSchema._metadata.class_permissions) { - clps = {...emptyCLPS, ...mongoSchema._metadata.class_permissions}; + clps = { ...emptyCLPS, ...mongoSchema._metadata.class_permissions }; } if (mongoSchema._metadata.indexes) { - indexes = {...mongoSchema._metadata.indexes}; + indexes = { ...mongoSchema._metadata.indexes }; } } return { @@ -90,23 +102,34 @@ function _mongoSchemaQueryFromNameQuery(name: string, query) { return object; } - // 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'; - case 'Bytes': return 'bytes'; - case 'Polygon': return 'polygon'; + 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'; + case 'Bytes': + return 'bytes'; + case 'Polygon': + return 'polygon'; } } @@ -118,43 +141,60 @@ class MongoSchemaCollection { } _fetchAllSchemasFrom_SCHEMA() { - return this._collection._rawFind({}) + return this._collection + ._rawFind({}) .then(schemas => schemas.map(mongoSchemaToParseSchema)); } _fetchOneSchemaFrom_SCHEMA(name: string) { - return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(results => { - if (results.length === 1) { - return mongoSchemaToParseSchema(results[0]); - } else { - throw undefined; - } - }); + return this._collection + ._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }) + .then(results => { + if (results.length === 1) { + return mongoSchemaToParseSchema(results[0]); + } else { + throw undefined; + } + }); } // Atomically find and delete an object based on query. findAndDeleteSchema(name: string) { - return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []); + return this._collection._mongoCollection.findAndRemove( + _mongoSchemaQueryFromNameQuery(name), + [] + ); } insertSchema(schema: any) { - return this._collection.insertOne(schema) + return this._collection + .insertOne(schema) .then(result => mongoSchemaToParseSchema(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.'); + if (error.code === 11000) { + //Mongo's duplicate key error + throw new Parse.Error( + Parse.Error.DUPLICATE_VALUE, + 'Class already exists.' + ); } else { throw error; } - }) + }); } updateSchema(name: string, update) { - return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update); + return this._collection.updateOne( + _mongoSchemaQueryFromNameQuery(name), + update + ); } upsertSchema(name: string, query: string, update) { - return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update); + return this._collection.upsertOne( + _mongoSchemaQueryFromNameQuery(name, query), + update + ); } // Add a field to the schema. If database does not support the field @@ -170,34 +210,45 @@ class MongoSchemaCollection { // 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._fetchOneSchemaFrom_SCHEMA(className) - .then(schema => { - // If a field with this name already exists, it will be handled elsewhere. - if (schema.fields[fieldName] != undefined) { - return; - } - // 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.'); + .then( + schema => { + // If a field with this name already exists, it will be handled elsewhere. + if (schema.fields[fieldName] != undefined) { + return; + } + // 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; + }, + 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; } - 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) + // 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) } } + { [fieldName]: { $exists: false } }, + { $set: { [fieldName]: parseFieldTypeToMongoFieldType(type) } } ); }); } @@ -205,7 +256,7 @@ class MongoSchemaCollection { // 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 +MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema; +MongoSchemaCollection.parseFieldTypeToMongoFieldType = parseFieldTypeToMongoFieldType; -export default MongoSchemaCollection +export default MongoSchemaCollection; diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 0e62d2e90b..6cb537cbad 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -1,11 +1,13 @@ // @flow -import MongoCollection from './MongoCollection'; +import MongoCollection from './MongoCollection'; import MongoSchemaCollection from './MongoSchemaCollection'; -import { StorageAdapter } from '../StorageAdapter'; -import type { SchemaType, +import { StorageAdapter } from '../StorageAdapter'; +import type { + SchemaType, QueryType, StorageClass, - QueryOptions } from '../StorageAdapter'; + QueryOptions, +} from '../StorageAdapter'; import { parse as parseUrl, format as formatUrl, @@ -19,11 +21,11 @@ import { transformPointerString, } from './MongoTransform'; // @flow-disable-next -import Parse from 'parse/node'; +import Parse from 'parse/node'; // @flow-disable-next -import _ from 'lodash'; -import defaults from '../../../defaults'; -import logger from '../../../logger'; +import _ from 'lodash'; +import defaults from '../../../defaults'; +import logger from '../../../logger'; // @flow-disable-next const mongodb = require('mongodb'); @@ -33,7 +35,8 @@ const ReadPreference = mongodb.ReadPreference; const MongoSchemaCollectionName = '_SCHEMA'; const storageAdapterAllCollections = mongoAdapter => { - return mongoAdapter.connect() + return mongoAdapter + .connect() .then(() => mongoAdapter.database.collections()) .then(collections => { return collections.filter(collection => { @@ -42,12 +45,14 @@ const storageAdapterAllCollections = mongoAdapter => { } // 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); + return ( + collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0 + ); }); }); -} +}; -const convertParseSchemaToMongoSchema = ({...schema}) => { +const convertParseSchemaToMongoSchema = ({ ...schema }) => { delete schema.fields._rperm; delete schema.fields._wperm; @@ -60,11 +65,16 @@ const convertParseSchemaToMongoSchema = ({...schema}) => { } return schema; -} +}; // Returns { code, error } if invalid, or { result }, an object // suitable for inserting into _SCHEMA collection, otherwise. -const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPermissions, indexes) => { +const mongoSchemaFromFieldsAndClassNameAndCLP = ( + fields, + className, + classLevelPermissions, + indexes +) => { const mongoObject = { _id: className, objectId: 'string', @@ -74,7 +84,9 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe }; for (const fieldName in fields) { - mongoObject[fieldName] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]); + mongoObject[ + fieldName + ] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]); } if (typeof classLevelPermissions !== 'undefined') { @@ -86,18 +98,22 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = (fields, className, classLevelPe } } - if (indexes && typeof indexes === 'object' && Object.keys(indexes).length > 0) { + if ( + indexes && + typeof indexes === 'object' && + Object.keys(indexes).length > 0 + ) { mongoObject._metadata = mongoObject._metadata || {}; mongoObject._metadata.indexes = indexes; } - if (!mongoObject._metadata) { // cleanup the unused _metadata + if (!mongoObject._metadata) { + // cleanup the unused _metadata delete mongoObject._metadata; } return mongoObject; -} - +}; export class MongoStorageAdapter implements StorageAdapter { // Private @@ -135,34 +151,40 @@ export class MongoStorageAdapter implements StorageAdapter { // encoded const encodedUri = formatUrl(parseUrl(this._uri)); - this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions).then(client => { - // Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client - // Fortunately, we can get back the options and use them to select the proper DB. - // https://github.com/mongodb/node-mongodb-native/blob/2c35d76f08574225b8db02d7bef687123e6bb018/lib/mongo_client.js#L885 - const options = client.s.options; - const database = client.db(options.dbName); - if (!database) { - delete this.connectionPromise; - return; - } - database.on('error', () => { - delete this.connectionPromise; - }); - database.on('close', () => { + this.connectionPromise = MongoClient.connect( + encodedUri, + this._mongoOptions + ) + .then(client => { + // Starting mongoDB 3.0, the MongoClient.connect don't return a DB anymore but a client + // Fortunately, we can get back the options and use them to select the proper DB. + // https://github.com/mongodb/node-mongodb-native/blob/2c35d76f08574225b8db02d7bef687123e6bb018/lib/mongo_client.js#L885 + const options = client.s.options; + const database = client.db(options.dbName); + if (!database) { + delete this.connectionPromise; + return; + } + database.on('error', () => { + delete this.connectionPromise; + }); + database.on('close', () => { + delete this.connectionPromise; + }); + this.client = client; + this.database = database; + }) + .catch(err => { delete this.connectionPromise; + return Promise.reject(err); }); - this.client = client; - this.database = database; - }).catch((err) => { - delete this.connectionPromise; - return Promise.reject(err); - }); return this.connectionPromise; } handleError(error: ?(Error | Parse.Error)): Promise { - if (error && error.code === 13) { // Unauthorized error + if (error && error.code === 13) { + // Unauthorized error delete this.client; delete this.database; delete this.connectionPromise; @@ -192,36 +214,55 @@ export class MongoStorageAdapter implements StorageAdapter { } classExists(name: string) { - return this.connect().then(() => { - return this.database.listCollections({ name: this._collectionPrefix + name }).toArray(); - }).then(collections => { - return collections.length > 0; - }).catch(err => this.handleError(err)); + return this.connect() + .then(() => { + return this.database + .listCollections({ name: this._collectionPrefix + name }) + .toArray(); + }) + .then(collections => { + return collections.length > 0; + }) + .catch(err => this.handleError(err)); } setClassLevelPermissions(className: string, CLPs: any): Promise { return this._schemaCollection() - .then(schemaCollection => schemaCollection.updateSchema(className, { - $set: { '_metadata.class_permissions': CLPs } - })).catch(err => this.handleError(err)); + .then(schemaCollection => + schemaCollection.updateSchema(className, { + $set: { '_metadata.class_permissions': CLPs }, + }) + ) + .catch(err => this.handleError(err)); } - setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any = {}, fields: any): Promise { + setIndexesWithSchemaFormat( + className: string, + submittedIndexes: any, + existingIndexes: any = {}, + fields: any + ): Promise { if (submittedIndexes === undefined) { return Promise.resolve(); } if (Object.keys(existingIndexes).length === 0) { - existingIndexes = { _id_: { _id: 1} }; + existingIndexes = { _id_: { _id: 1 } }; } const deletePromises = []; const insertedIndexes = []; Object.keys(submittedIndexes).forEach(name => { const field = submittedIndexes[name]; if (existingIndexes[name] && field.__op !== 'Delete') { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Index ${name} exists, cannot update.` + ); } if (!existingIndexes[name] && field.__op === 'Delete') { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} does not exist, cannot delete.`); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Index ${name} does not exist, cannot delete.` + ); } if (field.__op === 'Delete') { const promise = this.dropIndex(className, name); @@ -230,7 +271,10 @@ export class MongoStorageAdapter implements StorageAdapter { } else { Object.keys(field).forEach(key => { if (!fields.hasOwnProperty(key)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Field ${key} does not exist, cannot add index.` + ); } }); existingIndexes[name] = field; @@ -247,30 +291,34 @@ export class MongoStorageAdapter implements StorageAdapter { return Promise.all(deletePromises) .then(() => insertPromise) .then(() => this._schemaCollection()) - .then(schemaCollection => schemaCollection.updateSchema(className, { - $set: { '_metadata.indexes': existingIndexes } - })) + .then(schemaCollection => + schemaCollection.updateSchema(className, { + $set: { '_metadata.indexes': existingIndexes }, + }) + ) .catch(err => this.handleError(err)); } setIndexesFromMongo(className: string) { - return this.getIndexes(className).then((indexes) => { - indexes = indexes.reduce((obj, index) => { - if (index.key._fts) { - delete index.key._fts; - delete index.key._ftsx; - for (const field in index.weights) { - index.key[field] = 'text'; + return this.getIndexes(className) + .then(indexes => { + indexes = indexes.reduce((obj, index) => { + if (index.key._fts) { + delete index.key._fts; + delete index.key._ftsx; + for (const field in index.weights) { + index.key[field] = 'text'; + } } - } - obj[index.name] = index.key; - return obj; - }, {}); - return this._schemaCollection() - .then(schemaCollection => schemaCollection.updateSchema(className, { - $set: { '_metadata.indexes': indexes } - })); - }) + obj[index.name] = index.key; + return obj; + }, {}); + return this._schemaCollection().then(schemaCollection => + schemaCollection.updateSchema(className, { + $set: { '_metadata.indexes': indexes }, + }) + ); + }) .catch(err => this.handleError(err)) .catch(() => { // Ignore if collection not found @@ -280,17 +328,33 @@ export class MongoStorageAdapter implements StorageAdapter { createClass(className: string, schema: SchemaType): Promise { schema = convertParseSchemaToMongoSchema(schema); - const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions, schema.indexes); + const mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP( + schema.fields, + className, + schema.classLevelPermissions, + schema.indexes + ); mongoObject._id = className; - return this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields) + return this.setIndexesWithSchemaFormat( + className, + schema.indexes, + {}, + schema.fields + ) .then(() => this._schemaCollection()) .then(schemaCollection => schemaCollection.insertSchema(mongoObject)) .catch(err => this.handleError(err)); } - addFieldIfNotExists(className: string, fieldName: string, type: any): Promise { + addFieldIfNotExists( + className: string, + fieldName: string, + type: any + ): Promise { return this._schemaCollection() - .then(schemaCollection => schemaCollection.addFieldIfNotExists(className, fieldName, type)) + .then(schemaCollection => + schemaCollection.addFieldIfNotExists(className, fieldName, type) + ) .then(() => this.createIndexesIfNeeded(className, fieldName, type)) .catch(err => this.handleError(err)); } @@ -298,24 +362,33 @@ export class MongoStorageAdapter implements StorageAdapter { // 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: string) { - 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)) - .catch(err => this.handleError(err)); + 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) + ) + .catch(err => this.handleError(err)) + ); } deleteAllClasses(fast: boolean) { - return storageAdapterAllCollections(this) - .then(collections => Promise.all(collections.map(collection => fast ? collection.remove({}) : collection.drop()))); + return storageAdapterAllCollections(this).then(collections => + Promise.all( + collections.map( + collection => (fast ? collection.remove({}) : collection.drop()) + ) + ) + ); } // Remove the column and all the data. For Relations, the _Join collection is handled @@ -341,17 +414,17 @@ export class MongoStorageAdapter implements StorageAdapter { deleteFields(className: string, schema: SchemaType, fieldNames: string[]) { const mongoFormatNames = fieldNames.map(fieldName => { if (schema.fields[fieldName].type === 'Pointer') { - return `_p_${fieldName}` + return `_p_${fieldName}`; } else { return fieldName; } }); - const collectionUpdate = { '$unset' : {} }; + const collectionUpdate = { $unset: {} }; mongoFormatNames.forEach(name => { collectionUpdate['$unset'][name] = null; }); - const schemaUpdate = { '$unset' : {} }; + const schemaUpdate = { $unset: {} }; fieldNames.forEach(name => { schemaUpdate['$unset'][name] = null; }); @@ -359,7 +432,9 @@ export class MongoStorageAdapter implements StorageAdapter { return this._adaptiveCollection(className) .then(collection => collection.updateMany({}, collectionUpdate)) .then(() => this._schemaCollection()) - .then(schemaCollection => schemaCollection.updateSchema(className, schemaUpdate)) + .then(schemaCollection => + schemaCollection.updateSchema(className, schemaUpdate) + ) .catch(err => this.handleError(err)); } @@ -367,7 +442,10 @@ export class MongoStorageAdapter implements StorageAdapter { // schemas cannot be retrieved, returns a promise that rejects. Requirements for the // rejection reason are TBD. getAllClasses(): Promise { - return this._schemaCollection().then(schemasCollection => schemasCollection._fetchAllSchemasFrom_SCHEMA()) + return this._schemaCollection() + .then(schemasCollection => + schemasCollection._fetchAllSchemasFrom_SCHEMA() + ) .catch(err => this.handleError(err)); } @@ -376,7 +454,9 @@ export class MongoStorageAdapter implements StorageAdapter { // undefined as the reason. getClass(className: string): Promise { return this._schemaCollection() - .then(schemasCollection => schemasCollection._fetchOneSchemaFrom_SCHEMA(className)) + .then(schemasCollection => + schemasCollection._fetchOneSchemaFrom_SCHEMA(className) + ) .catch(err => this.handleError(err)); } @@ -385,15 +465,25 @@ export class MongoStorageAdapter implements StorageAdapter { // the schema only for the legacy mongo format. We'll figure that out later. createObject(className: string, schema: SchemaType, object: any) { schema = convertParseSchemaToMongoSchema(schema); - const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema); + const mongoObject = parseObjectToMongoObjectForCreate( + className, + object, + schema + ); return this._adaptiveCollection(className) .then(collection => collection.insertOne(mongoObject)) .catch(error => { - if (error.code === 11000) { // Duplicate value - const err = new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + if (error.code === 11000) { + // Duplicate value + const err = new Parse.Error( + Parse.Error.DUPLICATE_VALUE, + 'A duplicate value for a field with unique values was provided' + ); err.underlyingError = error; if (error.message) { - const matches = error.message.match(/index:[\sa-zA-Z0-9_\-\.]+\$?([a-zA-Z_-]+)_1/); + const matches = error.message.match( + /index:[\sa-zA-Z0-9_\-\.]+\$?([a-zA-Z_-]+)_1/ + ); if (matches && Array.isArray(matches)) { err.userInfo = { duplicated_field: matches[1] }; } @@ -408,26 +498,44 @@ export class MongoStorageAdapter implements StorageAdapter { // 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: string, schema: SchemaType, query: QueryType) { + deleteObjectsByQuery( + className: string, + schema: SchemaType, + query: QueryType + ) { schema = convertParseSchemaToMongoSchema(schema); return this._adaptiveCollection(className) .then(collection => { const mongoWhere = transformWhere(className, query, schema); - return collection.deleteMany(mongoWhere) + return collection.deleteMany(mongoWhere); }) .catch(err => this.handleError(err)) - .then(({ result }) => { - if (result.n === 0) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); + .then( + ({ result }) => { + if (result.n === 0) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); + } + return Promise.resolve(); + }, + () => { + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + 'Database adapter error' + ); } - return Promise.resolve(); - }, () => { - 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: string, schema: SchemaType, query: QueryType, update: any) { + updateObjectsByQuery( + className: string, + schema: SchemaType, + query: QueryType, + update: any + ) { schema = convertParseSchemaToMongoSchema(schema); const mongoUpdate = transformUpdate(className, update, schema); const mongoWhere = transformWhere(className, query, schema); @@ -438,16 +546,28 @@ export class MongoStorageAdapter implements StorageAdapter { // Atomically finds and updates an object based on query. // Return value not currently well specified. - findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any) { + findOneAndUpdate( + className: string, + schema: SchemaType, + query: QueryType, + update: any + ) { 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(collection => + collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, { + new: true, + }) + ) .then(result => mongoObjectToParseObject(className, result.value, schema)) .catch(error => { if (error.code === 11000) { - throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + throw new Parse.Error( + Parse.Error.DUPLICATE_VALUE, + 'A duplicate value for a field with unique values was provided' + ); } throw error; }) @@ -455,7 +575,12 @@ export class MongoStorageAdapter implements StorageAdapter { } // Hopefully we can get rid of this. It's only used for config and hooks. - upsertOneObject(className: string, schema: SchemaType, query: QueryType, update: any) { + upsertOneObject( + className: string, + schema: SchemaType, + query: QueryType, + update: any + ) { schema = convertParseSchemaToMongoSchema(schema); const mongoUpdate = transformUpdate(className, update, schema); const mongoWhere = transformWhere(className, query, schema); @@ -465,32 +590,49 @@ export class MongoStorageAdapter implements StorageAdapter { } // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. - find(className: string, schema: SchemaType, query: QueryType, { skip, limit, sort, keys, readPreference }: QueryOptions): Promise { + find( + className: string, + schema: SchemaType, + query: QueryType, + { skip, limit, sort, keys, readPreference }: QueryOptions + ): Promise { schema = convertParseSchemaToMongoSchema(schema); const mongoWhere = transformWhere(className, query, schema); - const mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema)); - const mongoKeys = _.reduce(keys, (memo, key) => { - if (key === 'ACL') { - memo['_rperm'] = 1; - memo['_wperm'] = 1; - } else { - memo[transformKey(className, key, schema)] = 1; - } - return memo; - }, {}); + const mongoSort = _.mapKeys(sort, (value, fieldName) => + transformKey(className, fieldName, schema) + ); + const mongoKeys = _.reduce( + keys, + (memo, key) => { + if (key === 'ACL') { + memo['_rperm'] = 1; + memo['_wperm'] = 1; + } else { + memo[transformKey(className, key, schema)] = 1; + } + return memo; + }, + {} + ); readPreference = this._parseReadPreference(readPreference); return this.createTextIndexesIfNeeded(className, query, schema) .then(() => this._adaptiveCollection(className)) - .then(collection => collection.find(mongoWhere, { - skip, - limit, - sort: mongoSort, - keys: mongoKeys, - maxTimeMS: this._maxTimeMS, - readPreference, - })) - .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))) + .then(collection => + collection.find(mongoWhere, { + skip, + limit, + sort: mongoSort, + keys: mongoKeys, + maxTimeMS: this._maxTimeMS, + readPreference, + }) + ) + .then(objects => + objects.map(object => + mongoObjectToParseObject(className, object, schema) + ) + ) .catch(err => this.handleError(err)); } @@ -499,18 +641,29 @@ export class MongoStorageAdapter implements StorageAdapter { // 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: string, schema: SchemaType, fieldNames: string[]) { + ensureUniqueness( + className: string, + schema: SchemaType, + fieldNames: string[] + ) { schema = convertParseSchemaToMongoSchema(schema); const indexCreationRequest = {}; - const mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema)); + const mongoFieldNames = fieldNames.map(fieldName => + transformKey(className, fieldName, schema) + ); mongoFieldNames.forEach(fieldName => { indexCreationRequest[fieldName] = 1; }); return this._adaptiveCollection(className) - .then(collection => collection._ensureSparseUniqueIndexInBackground(indexCreationRequest)) + .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.'); + throw new Parse.Error( + Parse.Error.DUPLICATE_VALUE, + 'Tried to ensure field uniqueness for a class that already has duplicates.' + ); } throw error; }) @@ -519,33 +672,52 @@ export class MongoStorageAdapter implements StorageAdapter { // Used in tests _rawFind(className: string, query: QueryType) { - return this._adaptiveCollection(className).then(collection => collection.find(query, { - maxTimeMS: this._maxTimeMS, - })).catch(err => this.handleError(err)); + return this._adaptiveCollection(className) + .then(collection => + collection.find(query, { + maxTimeMS: this._maxTimeMS, + }) + ) + .catch(err => this.handleError(err)); } // Executes a count. - count(className: string, schema: SchemaType, query: QueryType, readPreference: ?string) { + count( + className: string, + schema: SchemaType, + query: QueryType, + readPreference: ?string + ) { schema = convertParseSchemaToMongoSchema(schema); readPreference = this._parseReadPreference(readPreference); return this._adaptiveCollection(className) - .then(collection => collection.count(transformWhere(className, query, schema), { - maxTimeMS: this._maxTimeMS, - readPreference, - })) + .then(collection => + collection.count(transformWhere(className, query, schema), { + maxTimeMS: this._maxTimeMS, + readPreference, + }) + ) .catch(err => this.handleError(err)); } - distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string) { + distinct( + className: string, + schema: SchemaType, + query: QueryType, + fieldName: string + ) { schema = convertParseSchemaToMongoSchema(schema); - const isPointerField = schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer'; + const isPointerField = + schema.fields[fieldName] && schema.fields[fieldName].type === 'Pointer'; if (isPointerField) { - fieldName = `_p_${fieldName}` + fieldName = `_p_${fieldName}`; } return this._adaptiveCollection(className) - .then(collection => collection.distinct(fieldName, transformWhere(className, query, schema))) + .then(collection => + collection.distinct(fieldName, transformWhere(className, query, schema)) + ) .then(objects => { - objects = objects.filter((obj) => obj != null); + objects = objects.filter(obj => obj != null); return objects.map(object => { if (isPointerField) { const field = fieldName.substring(3); @@ -557,12 +729,21 @@ export class MongoStorageAdapter implements StorageAdapter { .catch(err => this.handleError(err)); } - aggregate(className: string, schema: any, pipeline: any, readPreference: ?string) { + aggregate( + className: string, + schema: any, + pipeline: any, + readPreference: ?string + ) { let isPointerField = false; - pipeline = pipeline.map((stage) => { + pipeline = pipeline.map(stage => { if (stage.$group) { stage.$group = this._parseAggregateGroupArgs(schema, stage.$group); - if (stage.$group._id && (typeof stage.$group._id === 'string') && stage.$group._id.indexOf('$_p_') >= 0) { + if ( + stage.$group._id && + typeof stage.$group._id === 'string' && + stage.$group._id.indexOf('$_p_') >= 0 + ) { isPointerField = true; } } @@ -570,13 +751,21 @@ export class MongoStorageAdapter implements StorageAdapter { stage.$match = this._parseAggregateArgs(schema, stage.$match); } if (stage.$project) { - stage.$project = this._parseAggregateProjectArgs(schema, stage.$project); + stage.$project = this._parseAggregateProjectArgs( + schema, + stage.$project + ); } return stage; }); readPreference = this._parseReadPreference(readPreference); return this._adaptiveCollection(className) - .then(collection => collection.aggregate(pipeline, { readPreference, maxTimeMS: this._maxTimeMS })) + .then(collection => + collection.aggregate(pipeline, { + readPreference, + maxTimeMS: this._maxTimeMS, + }) + ) .catch(error => { if (error.code === 16006) { throw new Parse.Error(Parse.Error.INVALID_QUERY, error.message); @@ -598,7 +787,11 @@ export class MongoStorageAdapter implements StorageAdapter { }); return results; }) - .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))) + .then(objects => + objects.map(object => + mongoObjectToParseObject(className, object, schema) + ) + ) .catch(err => this.handleError(err)); } @@ -623,7 +816,7 @@ export class MongoStorageAdapter implements StorageAdapter { // down a tree to find a "leaf node" and checking to see if it needs to be converted. _parseAggregateArgs(schema: any, pipeline: any): any { if (Array.isArray(pipeline)) { - return pipeline.map((value) => this._parseAggregateArgs(schema, value)); + return pipeline.map(value => this._parseAggregateArgs(schema, value)); } else if (typeof pipeline === 'object') { const returnValue = {}; for (const field in pipeline) { @@ -632,12 +825,20 @@ export class MongoStorageAdapter implements StorageAdapter { // Pass objects down to MongoDB...this is more than likely an $exists operator. returnValue[`_p_${field}`] = pipeline[field]; } else { - returnValue[`_p_${field}`] = `${schema.fields[field].targetClass}$${pipeline[field]}`; + returnValue[`_p_${field}`] = `${schema.fields[field].targetClass}$${ + pipeline[field] + }`; } - } else if (schema.fields[field] && schema.fields[field].type === 'Date') { + } else if ( + schema.fields[field] && + schema.fields[field].type === 'Date' + ) { returnValue[field] = this._convertToDate(pipeline[field]); } else { - returnValue[field] = this._parseAggregateArgs(schema, pipeline[field]); + returnValue[field] = this._parseAggregateArgs( + schema, + pipeline[field] + ); } if (field === 'objectId') { @@ -690,11 +891,16 @@ export class MongoStorageAdapter implements StorageAdapter { // updatedAt or objectId and change it accordingly. _parseAggregateGroupArgs(schema: any, pipeline: any): any { if (Array.isArray(pipeline)) { - return pipeline.map((value) => this._parseAggregateGroupArgs(schema, value)); + return pipeline.map(value => + this._parseAggregateGroupArgs(schema, value) + ); } else if (typeof pipeline === 'object') { const returnValue = {}; for (const field in pipeline) { - returnValue[field] = this._parseAggregateGroupArgs(schema, pipeline[field]); + returnValue[field] = this._parseAggregateGroupArgs( + schema, + pipeline[field] + ); } return returnValue; } else if (typeof pipeline === 'string') { @@ -719,34 +925,37 @@ export class MongoStorageAdapter implements StorageAdapter { return new Date(value); } - const returnValue = {} + const returnValue = {}; for (const field in value) { - returnValue[field] = this._convertToDate(value[field]) + returnValue[field] = this._convertToDate(value[field]); } return returnValue; } _parseReadPreference(readPreference: ?string): ?string { switch (readPreference) { - case 'PRIMARY': - readPreference = ReadPreference.PRIMARY; - break; - case 'PRIMARY_PREFERRED': - readPreference = ReadPreference.PRIMARY_PREFERRED; - break; - case 'SECONDARY': - readPreference = ReadPreference.SECONDARY; - break; - case 'SECONDARY_PREFERRED': - readPreference = ReadPreference.SECONDARY_PREFERRED; - break; - case 'NEAREST': - readPreference = ReadPreference.NEAREST; - break; - case undefined: - break; - default: - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Not supported read preference.'); + case 'PRIMARY': + readPreference = ReadPreference.PRIMARY; + break; + case 'PRIMARY_PREFERRED': + readPreference = ReadPreference.PRIMARY_PREFERRED; + break; + case 'SECONDARY': + readPreference = ReadPreference.SECONDARY; + break; + case 'SECONDARY_PREFERRED': + readPreference = ReadPreference.SECONDARY_PREFERRED; + break; + case 'NEAREST': + readPreference = ReadPreference.NEAREST; + break; + case undefined: + break; + default: + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Not supported read preference.' + ); } return readPreference; } @@ -770,15 +979,19 @@ export class MongoStorageAdapter implements StorageAdapter { createIndexesIfNeeded(className: string, fieldName: string, type: any) { if (type && type.type === 'Polygon') { const index = { - [fieldName]: '2dsphere' + [fieldName]: '2dsphere', }; return this.createIndex(className, index); } return Promise.resolve(); } - createTextIndexesIfNeeded(className: string, query: QueryType, schema: any): Promise { - for(const fieldName in query) { + createTextIndexesIfNeeded( + className: string, + query: QueryType, + schema: any + ): Promise { + for (const fieldName in query) { if (!query[fieldName] || !query[fieldName].$text) { continue; } @@ -791,15 +1004,20 @@ export class MongoStorageAdapter implements StorageAdapter { } const indexName = `${fieldName}_text`; const textIndex = { - [indexName]: { [fieldName]: 'text' } + [indexName]: { [fieldName]: 'text' }, }; - return this.setIndexesWithSchemaFormat(className, textIndex, existingIndexes, schema.fields) - .catch((error) => { - if (error.code === 85) { // Index exist with different options - return this.setIndexesFromMongo(className); - } - throw error; - }); + return this.setIndexesWithSchemaFormat( + className, + textIndex, + existingIndexes, + schema.fields + ).catch(error => { + if (error.code === 85) { + // Index exist with different options + return this.setIndexesFromMongo(className); + } + throw error; + }); } return Promise.resolve(); } @@ -824,8 +1042,8 @@ export class MongoStorageAdapter implements StorageAdapter { updateSchemaWithIndexes(): Promise { return this.getAllClasses() - .then((classes) => { - const promises = classes.map((schema) => { + .then(classes => { + const promises = classes.map(schema => { return this.setIndexesFromMongo(schema.className); }); return Promise.all(promises); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index ebd980a6d9..2ffefebd61 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -1,131 +1,154 @@ import log from '../../../logger'; -import _ from 'lodash'; +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'; - case 'lastUsed': return '_last_used'; - case 'timesUsed': return 'times_used'; + switch (fieldName) { + case 'objectId': + return '_id'; + case 'createdAt': + return '_created_at'; + case 'updatedAt': + return '_updated_at'; + case 'sessionToken': + return '_session_token'; + case 'lastUsed': + return '_last_used'; + case 'timesUsed': + return 'times_used'; } - if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') { + if ( + schema.fields[fieldName] && + schema.fields[fieldName].__type == 'Pointer' + ) { fieldName = '_p_' + fieldName; - } else if (schema.fields[fieldName] && schema.fields[fieldName].type == 'Pointer') { + } else if ( + schema.fields[fieldName] && + schema.fields[fieldName].type == 'Pointer' + ) { fieldName = '_p_' + fieldName; } return fieldName; -} +}; -const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSchema) => { +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': - if (className === '_GlobalConfig') { - return { - key: key, - value: parseInt(restValue) + switch (key) { + case 'objectId': + case '_id': + if (className === '_GlobalConfig') { + return { + key: key, + value: parseInt(restValue), + }; } - } - 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 '_email_verify_token_expires_at': - key = '_email_verify_token_expires_at'; - timeField = true; - break; - case '_account_lockout_expires_at': - key = '_account_lockout_expires_at'; - timeField = true; - break; - case '_failed_login_count': - key = '_failed_login_count'; - break; - case '_perishable_token_expires_at': - key = '_perishable_token_expires_at'; - timeField = true; - break; - case '_password_changed_at': - key = '_password_changed_at'; - timeField = true; - break; - case '_rperm': - case '_wperm': - return {key: key, value: restValue}; - case 'lastUsed': - case '_last_used': - key = '_last_used'; - timeField = true; - break; - case 'timesUsed': - case 'times_used': - key = 'times_used'; - timeField = true; - break; + 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 '_email_verify_token_expires_at': + key = '_email_verify_token_expires_at'; + timeField = true; + break; + case '_account_lockout_expires_at': + key = '_account_lockout_expires_at'; + timeField = true; + break; + case '_failed_login_count': + key = '_failed_login_count'; + break; + case '_perishable_token_expires_at': + key = '_perishable_token_expires_at'; + timeField = true; + break; + case '_password_changed_at': + key = '_password_changed_at'; + timeField = true; + break; + case '_rperm': + case '_wperm': + return { key: key, value: restValue }; + case 'lastUsed': + case '_last_used': + key = '_last_used'; + timeField = true; + break; + case 'timesUsed': + case 'times_used': + key = 'times_used'; + timeField = true; + break; } - if ((parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer') || (!parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer')) { + 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')) { + if (timeField && typeof value === 'string') { value = new Date(value); } if (restKey.indexOf('.') > 0) { - return {key, value: restValue} + return { key, value: restValue }; } - return {key, value}; + return { key, value }; } // Handle arrays if (restValue instanceof Array) { value = restValue.map(transformInteriorValue); - return {key, value}; + return { key, value }; } // Handle update operators if (typeof restValue === 'object' && '__op' in restValue) { - return {key, value: transformUpdateOperator(restValue, false)}; + return { key, value: transformUpdateOperator(restValue, false) }; } // Handle normal objects by recursing value = mapValues(restValue, transformInteriorValue); - return {key, value}; -} + return { key, value }; +}; const isRegex = value => { - return value && (value instanceof RegExp) -} + return value && value instanceof RegExp; +}; const isStartsWithRegex = value => { if (!isRegex(value)) { @@ -134,7 +157,7 @@ const isStartsWithRegex = value => { const matches = value.toString().match(/\/\^\\Q.*\\E\//); return !!matches; -} +}; const isAllValuesRegexOrNone = values => { if (!values || !Array.isArray(values) || values.length === 0) { @@ -153,17 +176,24 @@ const isAllValuesRegexOrNone = values => { } return true; -} +}; const isAnyValueRegex = values => { - return values.some(function (value) { + return values.some(function(value) { return isRegex(value); }); -} +}; const transformInteriorValue = restValue => { - if (restValue !== null && typeof restValue === 'object' && Object.keys(restValue).some(key => key.includes('$') || key.includes('.'))) { - throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + 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); @@ -183,7 +213,7 @@ const transformInteriorValue = restValue => { // Handle normal objects by recursing return mapValues(restValue, transformInteriorValue); -} +}; const valueAsDate = value => { if (typeof value === 'string') { @@ -192,95 +222,110 @@ const valueAsDate = value => { 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 '_email_verify_token_expires_at': - if (valueAsDate(value)) { - return {key: '_email_verify_token_expires_at', value: valueAsDate(value)} - } - break; - case 'objectId': { - if (className === '_GlobalConfig') { - value = parseInt(value); - } - return {key: '_id', value} - } - case '_account_lockout_expires_at': - if (valueAsDate(value)) { - return {key: '_account_lockout_expires_at', value: valueAsDate(value)} - } - break; - case '_failed_login_count': - return {key, value}; - case 'sessionToken': return {key: '_session_token', value} - case '_perishable_token_expires_at': - if (valueAsDate(value)) { - return { key: '_perishable_token_expires_at', value: valueAsDate(value) } - } - break; - case '_password_changed_at': - if (valueAsDate(value)) { - return { key: '_password_changed_at', value: valueAsDate(value) } - } - break; - case '_rperm': - case '_wperm': - case '_perishable_token': - case '_email_verify_token': return {key, value} - case '$or': - case '$and': - case '$nor': - return {key: key, value: value.map(subQuery => transformWhere(className, subQuery, schema))}; - case 'lastUsed': - if (valueAsDate(value)) { - return {key: '_last_used', value: valueAsDate(value)} + 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 '_email_verify_token_expires_at': + if (valueAsDate(value)) { + return { + key: '_email_verify_token_expires_at', + value: valueAsDate(value), + }; + } + break; + case 'objectId': { + if (className === '_GlobalConfig') { + value = parseInt(value); + } + return { key: '_id', value }; } - key = '_last_used'; - break; - case 'timesUsed': - return {key: 'times_used', value: value}; - 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}; + case '_account_lockout_expires_at': + if (valueAsDate(value)) { + return { + key: '_account_lockout_expires_at', + value: valueAsDate(value), + }; + } + break; + case '_failed_login_count': + return { key, value }; + case 'sessionToken': + return { key: '_session_token', value }; + case '_perishable_token_expires_at': + if (valueAsDate(value)) { + return { + key: '_perishable_token_expires_at', + value: valueAsDate(value), + }; + } + break; + case '_password_changed_at': + if (valueAsDate(value)) { + return { key: '_password_changed_at', value: valueAsDate(value) }; + } + break; + case '_rperm': + case '_wperm': + case '_perishable_token': + case '_email_verify_token': + return { key, value }; + case '$or': + case '$and': + case '$nor': + return { + key: key, + value: value.map(subQuery => + transformWhere(className, subQuery, schema) + ), + }; + case 'lastUsed': + if (valueAsDate(value)) { + return { key: '_last_used', value: valueAsDate(value) }; + } + key = '_last_used'; + break; + case 'timesUsed': + return { key: 'times_used', value: value }; + 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'; + schema && schema.fields[key] && schema.fields[key].type === 'Array'; const expectedTypeIsPointer = - schema && - schema.fields[key] && - schema.fields[key].type === 'Pointer'; + schema && schema.fields[key] && schema.fields[key].type === 'Pointer'; const field = schema && schema.fields[key]; - if (expectedTypeIsPointer || !schema && value && value.__type === 'Pointer') { + if ( + expectedTypeIsPointer || + (!schema && value && value.__type === 'Pointer') + ) { key = '_p_' + key; } @@ -288,23 +333,26 @@ function transformQueryKeyValue(className, key, value, schema) { const transformedConstraint = transformConstraint(value, field); if (transformedConstraint !== CannotTransform) { if (transformedConstraint.$text) { - return {key: '$text', value: transformedConstraint.$text}; + return { key: '$text', value: transformedConstraint.$text }; } if (transformedConstraint.$elemMatch) { return { key: '$nor', value: [{ [key]: transformedConstraint }] }; } - return {key, value: transformedConstraint}; + return { key, value: transformedConstraint }; } if (expectedTypeIsArray && !(value instanceof Array)) { - return {key, value: { '$all' : [transformInteriorAtom(value)] }}; + return { key, value: { $all: [transformInteriorAtom(value)] } }; } // Handle atomic values if (transformTopLevelAtom(value) !== CannotTransform) { - return {key, value: transformTopLevelAtom(value)}; + return { key, value: transformTopLevelAtom(value) }; } else { - throw new Parse.Error(Parse.Error.INVALID_JSON, `You cannot use ${value} as a query parameter.`); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `You cannot use ${value} as a query parameter.` + ); } } @@ -314,60 +362,93 @@ function transformQueryKeyValue(className, key, value, schema) { function transformWhere(className, restWhere, schema) { const mongoWhere = {}; for (const restKey in restWhere) { - const out = transformQueryKeyValue(className, restKey, restWhere[restKey], schema); + const out = transformQueryKeyValue( + className, + restKey, + restWhere[restKey], + schema + ); mongoWhere[out.key] = out.value; } return mongoWhere; } -const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) => { +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 '_email_verify_token_expires_at': - transformedValue = transformTopLevelAtom(restValue); - coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue - return {key: '_email_verify_token_expires_at', value: coercedToDate}; - case '_account_lockout_expires_at': - transformedValue = transformTopLevelAtom(restValue); - coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue - return {key: '_account_lockout_expires_at', value: coercedToDate}; - case '_perishable_token_expires_at': - transformedValue = transformTopLevelAtom(restValue); - coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue - return { key: '_perishable_token_expires_at', value: coercedToDate }; - case '_password_changed_at': - transformedValue = transformTopLevelAtom(restValue); - coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue - return { key: '_password_changed_at', value: coercedToDate }; - case '_failed_login_count': - 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}; - } + 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 '_email_verify_token_expires_at': + transformedValue = transformTopLevelAtom(restValue); + coercedToDate = + typeof transformedValue === 'string' + ? new Date(transformedValue) + : transformedValue; + return { key: '_email_verify_token_expires_at', value: coercedToDate }; + case '_account_lockout_expires_at': + transformedValue = transformTopLevelAtom(restValue); + coercedToDate = + typeof transformedValue === 'string' + ? new Date(transformedValue) + : transformedValue; + return { key: '_account_lockout_expires_at', value: coercedToDate }; + case '_perishable_token_expires_at': + transformedValue = transformTopLevelAtom(restValue); + coercedToDate = + typeof transformedValue === 'string' + ? new Date(transformedValue) + : transformedValue; + return { key: '_perishable_token_expires_at', value: coercedToDate }; + case '_password_changed_at': + transformedValue = transformTopLevelAtom(restValue); + coercedToDate = + typeof transformedValue === 'string' + ? new Date(transformedValue) + : transformedValue; + return { key: '_password_changed_at', value: coercedToDate }; + case '_failed_login_count': + 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') { + if ( + (schema.fields[restKey] && schema.fields[restKey].type == 'Pointer') || + restValue.__type == 'Pointer' + ) { restKey = '_p_' + restKey; } } @@ -375,7 +456,7 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) => // Handle atomic values var value = transformTopLevelAtom(restValue); if (value !== CannotTransform) { - return {key: restKey, value: value}; + return { key: restKey, value: value }; } // ACLs are handled before this method is called @@ -387,20 +468,25 @@ const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) => // Handle arrays if (restValue instanceof Array) { value = restValue.map(transformInteriorValue); - return {key: restKey, value: value}; + 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"); + 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}; -} + return { key: restKey, value }; +}; const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => { restCreate = addLegacyACL(restCreate); - const mongoCreate = {} + const mongoCreate = {}; for (const restKey in restCreate) { if (restCreate[restKey] && restCreate[restKey].__type === 'Relation') { continue; @@ -417,16 +503,20 @@ const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => { // Use the legacy mongo format for createdAt and updatedAt if (mongoCreate.createdAt) { - mongoCreate._created_at = new Date(mongoCreate.createdAt.iso || 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); + 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) => { @@ -448,7 +538,12 @@ const transformUpdate = (className, restUpdate, parseFormatSchema) => { if (restUpdate[restKey] && restUpdate[restKey].__type === 'Relation') { continue; } - var out = transformKeyValueForUpdate(className, restKey, restUpdate[restKey], parseFormatSchema); + 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 @@ -463,11 +558,11 @@ const transformUpdate = (className, restUpdate, parseFormatSchema) => { } return mongoUpdate; -} +}; // Add the legacy _acl format. const addLegacyACL = restObject => { - const restObjectCopy = {...restObject}; + const restObjectCopy = { ...restObject }; const _acl = {}; if (restObject._wperm) { @@ -489,23 +584,30 @@ const addLegacyACL = restObject => { } return restObjectCopy; -} - +}; // A sentinel value that helper transformations return when they // cannot perform a transformation function CannotTransform() {} -const transformInteriorAtom = (atom) => { +const transformInteriorAtom = atom => { // TODO: check validity harder for the __type-defined types - if (typeof atom === 'object' && atom && !(atom instanceof Date) && atom.__type === 'Pointer') { + if ( + typeof atom === 'object' && + atom && + !(atom instanceof Date) && + atom.__type === 'Pointer' + ) { return { __type: 'Pointer', className: atom.className, - objectId: atom.objectId + objectId: atom.objectId, }; } else if (typeof atom === 'function' || typeof atom === 'symbol') { - throw new Parse.Error(Parse.Error.INVALID_JSON, `cannot transform value: ${atom}`); + 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)) { @@ -515,7 +617,7 @@ const transformInteriorAtom = (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 @@ -525,54 +627,60 @@ const transformInteriorAtom = (atom) => { // Raises an error if this cannot possibly be valid REST format. // Returns CannotTransform if it's just not an atom function transformTopLevelAtom(atom, field) { - switch(typeof atom) { - case 'number': - case 'boolean': - case 'undefined': - return atom; - case 'string': - if (field && field.type === 'Pointer') { - return `${field.targetClass}$${atom}`; - } - 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. + switch (typeof atom) { + case 'number': + case 'boolean': + case 'undefined': return atom; - } - - if (atom === null) { + case 'string': + if (field && field.type === 'Pointer') { + return `${field.targetClass}$${atom}`; + } 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; + } - // 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 (PolygonCoder.isValidJSON(atom)) { - return PolygonCoder.JSONToDatabase(atom); - } - if (FileCoder.isValidJSON(atom)) { - return FileCoder.JSONToDatabase(atom); - } - return CannotTransform; + if (atom === null) { + return atom; + } - 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}`); + // 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 (PolygonCoder.isValidJSON(atom)) { + return PolygonCoder.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}` + ); } } @@ -582,13 +690,16 @@ function relativeTimeToDate(text, now = new Date()) { let parts = text.split(' '); // Filter out whitespace - parts = parts.filter((part) => part !== ''); + parts = parts.filter(part => part !== ''); const future = parts[0] === 'in'; const past = parts[parts.length - 1] === 'ago'; if (!future && !past && text !== 'now') { - return { status: 'error', info: "Time should either start with 'in' or end with 'ago'" }; + return { + status: 'error', + info: "Time should either start with 'in' or end with 'ago'", + }; } if (future && past) { @@ -601,7 +712,8 @@ function relativeTimeToDate(text, now = new Date()) { // strip the 'ago' or 'in' if (future) { parts = parts.slice(1); - } else { // past + } else { + // past parts = parts.slice(0, parts.length - 1); } @@ -613,8 +725,8 @@ function relativeTimeToDate(text, now = new Date()) { } const pairs = []; - while(parts.length) { - pairs.push([ parts.shift(), parts.shift() ]); + while (parts.length) { + pairs.push([parts.shift(), parts.shift()]); } let seconds = 0; @@ -627,53 +739,53 @@ function relativeTimeToDate(text, now = new Date()) { }; } - switch(interval) { - case 'yr': - case 'yrs': - case 'year': - case 'years': - seconds += val * 31536000; // 365 * 24 * 60 * 60 - break; + switch (interval) { + case 'yr': + case 'yrs': + case 'year': + case 'years': + seconds += val * 31536000; // 365 * 24 * 60 * 60 + break; - case 'wk': - case 'wks': - case 'week': - case 'weeks': - seconds += val * 604800; // 7 * 24 * 60 * 60 - break; + case 'wk': + case 'wks': + case 'week': + case 'weeks': + seconds += val * 604800; // 7 * 24 * 60 * 60 + break; - case 'd': - case 'day': - case 'days': - seconds += val * 86400; // 24 * 60 * 60 - break; + case 'd': + case 'day': + case 'days': + seconds += val * 86400; // 24 * 60 * 60 + break; - case 'hr': - case 'hrs': - case 'hour': - case 'hours': - seconds += val * 3600; // 60 * 60 - break; + case 'hr': + case 'hrs': + case 'hour': + case 'hours': + seconds += val * 3600; // 60 * 60 + break; - case 'min': - case 'mins': - case 'minute': - case 'minutes': - seconds += val * 60; - break; + case 'min': + case 'mins': + case 'minute': + case 'minutes': + seconds += val * 60; + break; - case 'sec': - case 'secs': - case 'second': - case 'seconds': - seconds += val; - break; + case 'sec': + case 'secs': + case 'second': + case 'seconds': + seconds += val; + break; - default: - return { - status: 'error', - info: `Invalid interval: '${interval}'`, - }; + default: + return { + status: 'error', + info: `Invalid interval: '${interval}'`, + }; } } @@ -682,20 +794,20 @@ function relativeTimeToDate(text, now = new Date()) { return { status: 'success', info: 'future', - result: new Date(now.valueOf() + milliseconds) + result: new Date(now.valueOf() + milliseconds), }; } else if (past) { return { status: 'success', info: 'past', - result: new Date(now.valueOf() - milliseconds) + result: new Date(now.valueOf() - milliseconds), }; } else { return { status: 'success', info: 'present', - result: new Date(now.valueOf()) - } + result: new Date(now.valueOf()), + }; } } @@ -709,293 +821,334 @@ function transformConstraint(constraint, field) { if (typeof constraint !== 'object' || !constraint) { return CannotTransform; } - const transformFunction = inArray ? transformInteriorAtom : transformTopLevelAtom; - const transformer = (atom) => { + const transformFunction = inArray + ? transformInteriorAtom + : transformTopLevelAtom; + const transformer = atom => { const result = transformFunction(atom, field); if (result === CannotTransform) { - throw new Parse.Error(Parse.Error.INVALID_JSON, `bad atom: ${JSON.stringify(atom)}`); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad atom: ${JSON.stringify(atom)}` + ); } return result; - } + }; // 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 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': { - const val = constraint[key]; - if (val && typeof val === 'object' && val.$relativeTime) { - if (field && field.type !== 'Date') { - throw new Parse.Error(Parse.Error.INVALID_JSON, '$relativeTime can only be used with Date field'); - } + switch (key) { + case '$lt': + case '$lte': + case '$gt': + case '$gte': + case '$exists': + case '$ne': + case '$eq': { + const val = constraint[key]; + if (val && typeof val === 'object' && val.$relativeTime) { + if (field && field.type !== 'Date') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + '$relativeTime can only be used with Date field' + ); + } - switch (key) { - case '$exists': - case '$ne': - case '$eq': - throw new Parse.Error(Parse.Error.INVALID_JSON, '$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators'); - } + switch (key) { + case '$exists': + case '$ne': + case '$eq': + throw new Parse.Error( + Parse.Error.INVALID_JSON, + '$relativeTime can only be used with the $lt, $lte, $gt, and $gte operators' + ); + } - const parserResult = relativeTimeToDate(val.$relativeTime); - if (parserResult.status === 'success') { - answer[key] = parserResult.result; - break; + const parserResult = relativeTimeToDate(val.$relativeTime); + if (parserResult.status === 'success') { + answer[key] = parserResult.result; + break; + } + + log.info('Error while parsing relative date', parserResult); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $relativeTime (${key}) value. ${parserResult.info}` + ); } - log.info('Error while parsing relative date', parserResult); - throw new Parse.Error(Parse.Error.INVALID_JSON, `bad $relativeTime (${key}) value. ${parserResult.info}`); + answer[key] = transformer(val); + break; } - answer[key] = transformer(val); - break; - } - - case '$in': - case '$nin': { - const arr = constraint[key]; - if (!(arr instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); - } - answer[key] = _.flatMap(arr, value => { - return ((atom) => { - if (Array.isArray(atom)) { - return value.map(transformer); - } else { - return transformer(atom); - } - })(value); - }); - break; - } - case '$all': { - const arr = constraint[key]; - if (!(arr instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'bad ' + key + ' value'); + case '$in': + case '$nin': { + const arr = constraint[key]; + if (!(arr instanceof Array)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad ' + key + ' value' + ); + } + answer[key] = _.flatMap(arr, value => { + return (atom => { + if (Array.isArray(atom)) { + return value.map(transformer); + } else { + return transformer(atom); + } + })(value); + }); + break; } - answer[key] = arr.map(transformInteriorAtom); + case '$all': { + const arr = constraint[key]; + if (!(arr instanceof Array)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad ' + key + ' value' + ); + } + answer[key] = arr.map(transformInteriorAtom); - const values = answer[key]; - if (isAnyValueRegex(values) && !isAllValuesRegexOrNone(values)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'All $all values must be of regex type or none: ' - + values); - } + const values = answer[key]; + if (isAnyValueRegex(values) && !isAllValuesRegexOrNone(values)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'All $all values must be of regex type or none: ' + values + ); + } - break; - } - case '$regex': - var s = constraint[key]; - if (typeof s !== 'string') { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad regex: ' + s); + break; } - answer[key] = s; - 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 '$containedBy': { - const arr = constraint[key]; - if (!(arr instanceof Array)) { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - `bad $containedBy: should be an array` - ); + case '$containedBy': { + const arr = constraint[key]; + if (!(arr instanceof Array)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $containedBy: should be an array` + ); + } + answer.$elemMatch = { + $nin: arr.map(transformer), + }; + break; } - answer.$elemMatch = { - $nin: arr.map(transformer) - }; - break; - } - case '$options': - answer[key] = constraint[key]; - break; + case '$options': + answer[key] = constraint[key]; + break; - case '$text': { - const search = constraint[key].$search; - if (typeof search !== 'object') { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - `bad $text: $search, should be object` - ); - } - if (!search.$term || typeof search.$term !== 'string') { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - `bad $text: $term, should be string` - ); - } else { - answer[key] = { - '$search': search.$term + case '$text': { + const search = constraint[key].$search; + if (typeof search !== 'object') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $search, should be object` + ); } + if (!search.$term || typeof search.$term !== 'string') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $term, should be string` + ); + } else { + answer[key] = { + $search: search.$term, + }; + } + if (search.$language && typeof search.$language !== 'string') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $language, should be string` + ); + } else if (search.$language) { + answer[key].$language = search.$language; + } + if ( + search.$caseSensitive && + typeof search.$caseSensitive !== 'boolean' + ) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $caseSensitive, should be boolean` + ); + } else if (search.$caseSensitive) { + answer[key].$caseSensitive = search.$caseSensitive; + } + if ( + search.$diacriticSensitive && + typeof search.$diacriticSensitive !== 'boolean' + ) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `bad $text: $diacriticSensitive, should be boolean` + ); + } else if (search.$diacriticSensitive) { + answer[key].$diacriticSensitive = search.$diacriticSensitive; + } + break; } - if (search.$language && typeof search.$language !== 'string') { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - `bad $text: $language, should be string` - ); - } else if (search.$language) { - answer[key].$language = search.$language; - } - if (search.$caseSensitive && typeof search.$caseSensitive !== 'boolean') { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - `bad $text: $caseSensitive, should be boolean` - ); - } else if (search.$caseSensitive) { - answer[key].$caseSensitive = search.$caseSensitive; - } - if (search.$diacriticSensitive && typeof search.$diacriticSensitive !== 'boolean') { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - `bad $text: $diacriticSensitive, should be boolean` - ); - } else if (search.$diacriticSensitive) { - answer[key].$diacriticSensitive = search.$diacriticSensitive; - } - break; - } - case '$nearSphere': - var point = constraint[key]; - answer[key] = [point.longitude, point.latitude]; - break; - - case '$maxDistance': - answer[key] = constraint[key]; - break; + case '$nearSphere': + var point = constraint[key]; + answer[key] = [point.longitude, point.latitude]; + 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 '$maxDistance': + answer[key] = constraint[key]; + break; - case '$select': - case '$dontSelect': - throw new Parse.Error( - Parse.Error.COMMAND_UNAVAILABLE, - 'the ' + key + ' constraint is not supported yet'); + // 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 '$within': - var box = constraint[key]['$box']; - if (!box || box.length != 2) { + case '$select': + case '$dontSelect': 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; + 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; - case '$geoWithin': { - const polygon = constraint[key]['$polygon']; - const centerSphere = constraint[key]['$centerSphere']; - if (polygon !== undefined) { - let points; - if (typeof polygon === 'object' && polygon.__type === 'Polygon') { - if (!polygon.coordinates || polygon.coordinates.length < 3) { + case '$geoWithin': { + const polygon = constraint[key]['$polygon']; + const centerSphere = constraint[key]['$centerSphere']; + if (polygon !== undefined) { + let points; + if (typeof polygon === 'object' && polygon.__type === 'Polygon') { + if (!polygon.coordinates || polygon.coordinates.length < 3) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value; Polygon.coordinates should contain at least 3 lon/lat pairs' + ); + } + points = polygon.coordinates; + } else if (polygon instanceof Array) { + if (polygon.length < 3) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value; $polygon should contain at least 3 GeoPoints' + ); + } + points = polygon; + } else { throw new Parse.Error( Parse.Error.INVALID_JSON, - 'bad $geoWithin value; Polygon.coordinates should contain at least 3 lon/lat pairs' + "bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint's" ); } - points = polygon.coordinates; - } else if (polygon instanceof Array) { - if (polygon.length < 3) { + points = points.map(point => { + if (point instanceof Array && point.length === 2) { + Parse.GeoPoint._validate(point[1], point[0]); + return point; + } + if (!GeoPointCoder.isValidJSON(point)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value' + ); + } else { + Parse.GeoPoint._validate(point.latitude, point.longitude); + } + return [point.longitude, point.latitude]; + }); + answer[key] = { + $polygon: points, + }; + } else if (centerSphere !== undefined) { + if (!(centerSphere instanceof Array) || centerSphere.length < 2) { throw new Parse.Error( Parse.Error.INVALID_JSON, - 'bad $geoWithin value; $polygon should contain at least 3 GeoPoints' + 'bad $geoWithin value; $centerSphere should be an array of Parse.GeoPoint and distance' ); } - points = polygon; - } else { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - 'bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint\'s' - ); - } - points = points.map((point) => { + // Get point, convert to geo point if necessary and validate + let point = centerSphere[0]; if (point instanceof Array && point.length === 2) { - Parse.GeoPoint._validate(point[1], point[0]); - return point; + point = new Parse.GeoPoint(point[1], point[0]); + } else if (!GeoPointCoder.isValidJSON(point)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value; $centerSphere geo point invalid' + ); } - if (!GeoPointCoder.isValidJSON(point)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value'); - } else { - Parse.GeoPoint._validate(point.latitude, point.longitude); + Parse.GeoPoint._validate(point.latitude, point.longitude); + // Get distance and validate + const distance = centerSphere[1]; + if (isNaN(distance) || distance < 0) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value; $centerSphere distance invalid' + ); } - return [point.longitude, point.latitude]; - }); - answer[key] = { - '$polygon': points - }; - } else if (centerSphere !== undefined) { - if (!(centerSphere instanceof Array) || centerSphere.length < 2) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere should be an array of Parse.GeoPoint and distance'); - } - // Get point, convert to geo point if necessary and validate - let point = centerSphere[0]; - if (point instanceof Array && point.length === 2) { - point = new Parse.GeoPoint(point[1], point[0]); - } else if (!GeoPointCoder.isValidJSON(point)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere geo point invalid'); + answer[key] = { + $centerSphere: [[point.longitude, point.latitude], distance], + }; } - Parse.GeoPoint._validate(point.latitude, point.longitude); - // Get distance and validate - const distance = centerSphere[1]; - if(isNaN(distance) || distance < 0) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere distance invalid'); + break; + } + case '$geoIntersects': { + const point = constraint[key]['$point']; + if (!GeoPointCoder.isValidJSON(point)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoIntersect value; $point should be GeoPoint' + ); + } else { + Parse.GeoPoint._validate(point.latitude, point.longitude); } answer[key] = { - '$centerSphere': [ - [point.longitude, point.latitude], - distance - ] + $geometry: { + type: 'Point', + coordinates: [point.longitude, point.latitude], + }, }; + break; } - break; - } - case '$geoIntersects': { - const point = constraint[key]['$point']; - if (!GeoPointCoder.isValidJSON(point)) { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - 'bad $geoIntersect value; $point should be GeoPoint' - ); - } else { - Parse.GeoPoint._validate(point.latitude, point.longitude); - } - answer[key] = { - $geometry: { - type: 'Point', - coordinates: [point.longitude, point.latitude] + default: + if (key.match(/^\$+/)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad constraint: ' + key + ); } - }; - break; - } - default: - if (key.match(/^\$+/)) { - throw new Parse.Error( - Parse.Error.INVALID_JSON, - 'bad constraint: ' + key); - } - return CannotTransform; + return CannotTransform; } } return answer; @@ -1011,112 +1164,124 @@ function transformConstraint(constraint, field) { // 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: ''}; - } +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 '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 '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}; - } + 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.`); + default: + throw new Parse.Error( + Parse.Error.COMMAND_UNAVAILABLE, + `The ${__op} operator is not supported yet.` + ); } } function mapValues(object, iterator) { const result = {}; - Object.keys(object).forEach((key) => { + Object.keys(object).forEach(key => { result[key] = iterator(object[key]); }); return result; } const nestedMongoObjectToNestedParseObject = mongoObject => { - switch(typeof mongoObject) { - case 'string': - case 'number': - case 'boolean': - case 'undefined': - return mongoObject; - case 'symbol': - case 'function': - throw 'bad value in nestedMongoObjectToNestedParseObject'; - case 'object': - if (mongoObject === null) { - return null; - } - if (mongoObject instanceof Array) { - return mongoObject.map(nestedMongoObjectToNestedParseObject); - } + switch (typeof mongoObject) { + case 'string': + case 'number': + case 'boolean': + case 'undefined': + return mongoObject; + case 'symbol': + case 'function': + throw 'bad value in nestedMongoObjectToNestedParseObject'; + 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 Date) { + return Parse._encode(mongoObject); + } - if (mongoObject instanceof mongodb.Long) { - return mongoObject.toNumber(); - } + if (mongoObject instanceof mongodb.Long) { + return mongoObject.toNumber(); + } - if (mongoObject instanceof mongodb.Double) { - return mongoObject.value; - } + if (mongoObject instanceof mongodb.Double) { + return mongoObject.value; + } - if (BytesCoder.isValidDatabaseObject(mongoObject)) { - return BytesCoder.databaseToJSON(mongoObject); - } + if (BytesCoder.isValidDatabaseObject(mongoObject)) { + return BytesCoder.databaseToJSON(mongoObject); + } - if (mongoObject.hasOwnProperty('__type') && mongoObject.__type == 'Date' && mongoObject.iso instanceof Date) { - mongoObject.iso = mongoObject.iso.toJSON(); - return mongoObject; - } + if ( + mongoObject.hasOwnProperty('__type') && + mongoObject.__type == 'Date' && + mongoObject.iso instanceof Date + ) { + mongoObject.iso = mongoObject.iso.toJSON(); + return mongoObject; + } - return mapValues(mongoObject, nestedMongoObjectToNestedParseObject); - default: - throw 'unknown js type'; + return mapValues(mongoObject, nestedMongoObjectToNestedParseObject); + default: + throw 'unknown js type'; } -} +}; const transformPointerString = (schema, field, pointerString) => { const objData = pointerString.split('$'); @@ -1126,164 +1291,204 @@ const transformPointerString = (schema, field, pointerString) => { return { __type: 'Pointer', className: objData[0], - objectId: objData[1] + objectId: objData[1], }; -} +}; // 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': - case 'undefined': - return mongoObject; - 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); - } + switch (typeof mongoObject) { + case 'string': + case 'number': + case 'boolean': + case 'undefined': + return mongoObject; + 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 mongodb.Long) { - return mongoObject.toNumber(); - } + if (mongoObject instanceof Date) { + return Parse._encode(mongoObject); + } - if (mongoObject instanceof mongodb.Double) { - return mongoObject.value; - } + if (mongoObject instanceof mongodb.Long) { + return mongoObject.toNumber(); + } - if (BytesCoder.isValidDatabaseObject(mongoObject)) { - return BytesCoder.databaseToJSON(mongoObject); - } + if (mongoObject instanceof mongodb.Double) { + return mongoObject.value; + } - const restObject = {}; - if (mongoObject._rperm || mongoObject._wperm) { - restObject._rperm = mongoObject._rperm || []; - restObject._wperm = mongoObject._wperm || []; - delete mongoObject._rperm; - delete mongoObject._wperm; - } + if (BytesCoder.isValidDatabaseObject(mongoObject)) { + return BytesCoder.databaseToJSON(mongoObject); + } - 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': - break; - case '_email_verify_token': - case '_perishable_token': - case '_perishable_token_expires_at': - case '_password_changed_at': - case '_tombstone': - case '_email_verify_token_expires_at': - case '_account_lockout_expires_at': - case '_failed_login_count': - case '_password_history': - // Those keys will be deleted if needed in the DB Controller - restObject[key] = mongoObject[key]; - 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; - case 'lastUsed': - case '_last_used': - restObject['lastUsed'] = Parse._encode(new Date(mongoObject[key])).iso; - break; - case 'timesUsed': - case 'times_used': - restObject['timesUsed'] = 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; - } + const restObject = {}; + if (mongoObject._rperm || mongoObject._wperm) { + restObject._rperm = mongoObject._rperm || []; + restObject._wperm = mongoObject._wperm || []; + delete mongoObject._rperm; + delete mongoObject._wperm; + } - 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); + for (var key in mongoObject) { + switch (key) { + case '_id': + restObject['objectId'] = '' + mongoObject[key]; break; - } - if (schema.fields[newKey].type !== 'Pointer') { - log.info('transform.js', 'Found a pointer in a non-pointer column, dropping it.', className, key); + case '_hashed_password': + restObject._hashed_password = mongoObject[key]; break; - } - if (mongoObject[key] === null) { + case '_acl': break; - } - restObject[newKey] = transformPointerString(schema, newKey, mongoObject[key]); - 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); + case '_email_verify_token': + case '_perishable_token': + case '_perishable_token_expires_at': + case '_password_changed_at': + case '_tombstone': + case '_email_verify_token_expires_at': + case '_account_lockout_expires_at': + case '_failed_login_count': + case '_password_history': + // Those keys will be deleted if needed in the DB Controller + restObject[key] = mongoObject[key]; break; - } - if (schema.fields[key] && schema.fields[key].type === 'GeoPoint' && GeoPointCoder.isValidDatabaseObject(value)) { - restObject[key] = GeoPointCoder.databaseToJSON(value); + case '_session_token': + restObject['sessionToken'] = mongoObject[key]; break; - } - if (schema.fields[key] && schema.fields[key].type === 'Polygon' && PolygonCoder.isValidDatabaseObject(value)) { - restObject[key] = PolygonCoder.databaseToJSON(value); + case 'updatedAt': + case '_updated_at': + restObject['updatedAt'] = Parse._encode( + new Date(mongoObject[key]) + ).iso; break; - } - if (schema.fields[key] && schema.fields[key].type === 'Bytes' && BytesCoder.isValidDatabaseObject(value)) { - restObject[key] = BytesCoder.databaseToJSON(value); + 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; + case 'lastUsed': + case '_last_used': + restObject['lastUsed'] = Parse._encode( + new Date(mongoObject[key]) + ).iso; + break; + case 'timesUsed': + case 'times_used': + restObject['timesUsed'] = 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; + } + restObject[newKey] = transformPointerString( + schema, + newKey, + mongoObject[key] + ); + 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; + } + if ( + schema.fields[key] && + schema.fields[key].type === 'Polygon' && + PolygonCoder.isValidDatabaseObject(value) + ) { + restObject[key] = PolygonCoder.databaseToJSON(value); + break; + } + if ( + schema.fields[key] && + schema.fields[key].type === 'Bytes' && + BytesCoder.isValidDatabaseObject(value) + ) { + restObject[key] = BytesCoder.databaseToJSON(value); + break; + } + } + restObject[key] = nestedMongoObjectToNestedParseObject( + mongoObject[key] + ); } - restObject[key] = nestedMongoObjectToNestedParseObject(mongoObject[key]); } - } - const relationFieldNames = Object.keys(schema.fields).filter(fieldName => schema.fields[fieldName].type === 'Relation'); - const relationFields = {}; - relationFieldNames.forEach(relationFieldName => { - relationFields[relationFieldName] = { - __type: 'Relation', - className: schema.fields[relationFieldName].targetClass, - } - }); + const relationFieldNames = Object.keys(schema.fields).filter( + fieldName => schema.fields[fieldName].type === 'Relation' + ); + const relationFields = {}; + relationFieldNames.forEach(relationFieldName => { + relationFields[relationFieldName] = { + __type: 'Relation', + className: schema.fields[relationFieldName].targetClass, + }; + }); - return { ...restObject, ...relationFields }; - } - default: - throw 'unknown js type'; + return { ...restObject, ...relationFields }; + } + default: + throw 'unknown js type'; } -} +}; var DateCoder = { JSONToDatabase(json) { @@ -1291,15 +1496,16 @@ var DateCoder = { }, isValidJSON(value) { - return (typeof value === 'object' && - value !== null && - value.__type === 'Date' + return ( + typeof value === 'object' && value !== null && value.__type === 'Date' ); - } + }, }; var BytesCoder = { - base64Pattern: new RegExp("^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$"), + base64Pattern: new RegExp( + '^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$' + ), isBase64Value(object) { if (typeof object !== 'string') { return false; @@ -1316,12 +1522,12 @@ var BytesCoder = { } return { __type: 'Bytes', - base64: value + base64: value, }; }, isValidDatabaseObject(object) { - return (object instanceof mongodb.Binary) || this.isBase64Value(object); + return object instanceof mongodb.Binary || this.isBase64Value(object); }, JSONToDatabase(json) { @@ -1329,11 +1535,10 @@ var BytesCoder = { }, isValidJSON(value) { - return (typeof value === 'object' && - value !== null && - value.__type === 'Bytes' + return ( + typeof value === 'object' && value !== null && value.__type === 'Bytes' ); - } + }, }; var GeoPointCoder = { @@ -1341,38 +1546,35 @@ var GeoPointCoder = { return { __type: 'GeoPoint', latitude: object[1], - longitude: object[0] - } + longitude: object[0], + }; }, isValidDatabaseObject(object) { - return (object instanceof Array && - object.length == 2 - ); + return object instanceof Array && object.length == 2; }, JSONToDatabase(json) { - return [ json.longitude, json.latitude ]; + return [json.longitude, json.latitude]; }, isValidJSON(value) { - return (typeof value === 'object' && - value !== null && - value.__type === 'GeoPoint' + return ( + typeof value === 'object' && value !== null && value.__type === 'GeoPoint' ); - } + }, }; var PolygonCoder = { databaseToJSON(object) { // Convert lng/lat -> lat/lng - const coords = object.coordinates[0].map((coord) => { + const coords = object.coordinates[0].map(coord => { return [coord[1], coord[0]]; }); return { __type: 'Polygon', - coordinates: coords - } + coordinates: coords, + }; }, isValidDatabaseObject(object) { @@ -1393,16 +1595,17 @@ var PolygonCoder = { JSONToDatabase(json) { let coords = json.coordinates; // Add first point to the end to close polygon - if (coords[0][0] !== coords[coords.length - 1][0] || - coords[0][1] !== coords[coords.length - 1][1]) { + if ( + coords[0][0] !== coords[coords.length - 1][0] || + coords[0][1] !== coords[coords.length - 1][1] + ) { coords.push(coords[0]); } const unique = coords.filter((item, index, ar) => { let foundIndex = -1; for (let i = 0; i < ar.length; i += 1) { const pt = ar[i]; - if (pt[0] === item[0] && - pt[1] === item[1]) { + if (pt[0] === item[0] && pt[1] === item[1]) { foundIndex = i; break; } @@ -1416,30 +1619,29 @@ var PolygonCoder = { ); } // Convert lat/long -> long/lat - coords = coords.map((coord) => { + coords = coords.map(coord => { return [coord[1], coord[0]]; }); return { type: 'Polygon', coordinates: [coords] }; }, isValidJSON(value) { - return (typeof value === 'object' && - value !== null && - value.__type === 'Polygon' + return ( + typeof value === 'object' && value !== null && value.__type === 'Polygon' ); - } + }, }; var FileCoder = { databaseToJSON(object) { return { __type: 'File', - name: object - } + name: object, + }; }, isValidDatabaseObject(object) { - return (typeof object === 'string'); + return typeof object === 'string'; }, JSONToDatabase(json) { @@ -1447,11 +1649,10 @@ var FileCoder = { }, isValidJSON(value) { - return (typeof value === 'object' && - value !== null && - value.__type === 'File' + return ( + typeof value === 'object' && value !== null && value.__type === 'File' ); - } + }, }; module.exports = { diff --git a/src/Adapters/Storage/Postgres/PostgresClient.js b/src/Adapters/Storage/Postgres/PostgresClient.js index 47cafbefa7..052be57fb1 100644 --- a/src/Adapters/Storage/Postgres/PostgresClient.js +++ b/src/Adapters/Storage/Postgres/PostgresClient.js @@ -1,4 +1,3 @@ - const parser = require('./PostgresConfigParser'); export function createClient(uri, databaseOptions) { diff --git a/src/Adapters/Storage/Postgres/PostgresConfigParser.js b/src/Adapters/Storage/Postgres/PostgresConfigParser.js index 6bcce676a3..bc95e71cea 100644 --- a/src/Adapters/Storage/Postgres/PostgresConfigParser.js +++ b/src/Adapters/Storage/Postgres/PostgresConfigParser.js @@ -19,11 +19,14 @@ function getDatabaseOptionsFromURI(uri) { databaseOptions.ssl = queryParams.ssl && queryParams.ssl.toLowerCase() === 'true' ? true : false; databaseOptions.binary = - queryParams.binary && queryParams.binary.toLowerCase() === 'true' ? true : false; + queryParams.binary && queryParams.binary.toLowerCase() === 'true' + ? true + : false; databaseOptions.client_encoding = queryParams.client_encoding; databaseOptions.application_name = queryParams.application_name; - databaseOptions.fallback_application_name = queryParams.fallback_application_name; + databaseOptions.fallback_application_name = + queryParams.fallback_application_name; if (queryParams.poolSize) { databaseOptions.poolSize = parseInt(queryParams.poolSize) || 10; @@ -35,19 +38,15 @@ function getDatabaseOptionsFromURI(uri) { function parseQueryParams(queryString) { queryString = queryString || ''; - return queryString - .split('&') - .reduce((p, c) => { - const parts = c.split('='); - p[decodeURIComponent(parts[0])] = - parts.length > 1 - ? decodeURIComponent(parts.slice(1).join('=')) - : ''; - return p; - }, {}); + return queryString.split('&').reduce((p, c) => { + const parts = c.split('='); + p[decodeURIComponent(parts[0])] = + parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : ''; + return p; + }, {}); } module.exports = { parseQueryParams: parseQueryParams, - getDatabaseOptionsFromURI: getDatabaseOptionsFromURI + getDatabaseOptionsFromURI: getDatabaseOptionsFromURI, }; diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index c7a686f8aa..f996367143 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1,10 +1,10 @@ // @flow import { createClient } from './PostgresClient'; // @flow-disable-next -import Parse from 'parse/node'; +import Parse from 'parse/node'; // @flow-disable-next -import _ from 'lodash'; -import sql from './sql'; +import _ from 'lodash'; +import sql from './sql'; const PostgresRelationDoesNotExistError = '42P01'; const PostgresDuplicateRelationError = '42P07'; @@ -19,48 +19,57 @@ const debug = function(...args: any) { args = ['PG: ' + arguments[0]].concat(args.slice(1, args.length)); const log = logger.getLogger(); log.debug.apply(log, args); -} +}; -import { StorageAdapter } from '../StorageAdapter'; -import type { SchemaType, - QueryType, - QueryOptions } from '../StorageAdapter'; +import { StorageAdapter } from '../StorageAdapter'; +import type { SchemaType, QueryType, QueryOptions } from '../StorageAdapter'; const parseTypeToPostgresType = type => { switch (type.type) { - case 'String': return 'text'; - case 'Date': return 'timestamp with time zone'; - case 'Object': return 'jsonb'; - case 'File': return 'text'; - case 'Boolean': return 'boolean'; - case 'Pointer': return 'char(10)'; - case 'Number': return 'double precision'; - case 'GeoPoint': return 'point'; - case 'Bytes': return 'jsonb'; - case 'Polygon': return 'polygon'; - case 'Array': - if (type.contents && type.contents.type === 'String') { - return 'text[]'; - } else { + case 'String': + return 'text'; + case 'Date': + return 'timestamp with time zone'; + case 'Object': return 'jsonb'; - } - default: throw `no type for ${JSON.stringify(type)} yet`; + case 'File': + return 'text'; + case 'Boolean': + return 'boolean'; + case 'Pointer': + return 'char(10)'; + case 'Number': + return 'double precision'; + case 'GeoPoint': + return 'point'; + case 'Bytes': + return 'jsonb'; + case 'Polygon': + return 'polygon'; + case 'Array': + if (type.contents && type.contents.type === 'String') { + return 'text[]'; + } else { + return 'jsonb'; + } + default: + throw `no type for ${JSON.stringify(type)} yet`; } }; const ParseToPosgresComparator = { - '$gt': '>', - '$lt': '<', - '$gte': '>=', - '$lte': '<=' -} + $gt: '>', + $lt: '<', + $gte: '>=', + $lte: '<=', +}; const mongoAggregateToPostgres = { $dayOfMonth: 'DAY', $dayOfWeek: 'DOW', $dayOfYear: 'DOY', $isoDayOfWeek: 'ISODOW', - $isoWeekYear:'ISOYEAR', + $isoWeekYear: 'ISOYEAR', $hour: 'HOUR', $minute: 'MINUTE', $second: 'SECOND', @@ -80,15 +89,14 @@ const toPostgresValue = value => { } } return value; -} +}; const transformValue = value => { - if (typeof value === 'object' && - value.__type === 'Pointer') { + if (typeof value === 'object' && value.__type === 'Pointer') { return value.objectId; } return value; -} +}; // Duplicate from then mongo adapter... const emptyCLPS = Object.freeze({ @@ -101,15 +109,15 @@ const emptyCLPS = Object.freeze({ }); const defaultCLPS = Object.freeze({ - find: {'*': true}, - get: {'*': true}, - create: {'*': true}, - update: {'*': true}, - delete: {'*': true}, - addField: {'*': true}, + find: { '*': true }, + get: { '*': true }, + create: { '*': true }, + update: { '*': true }, + delete: { '*': true }, + addField: { '*': true }, }); -const toParseSchema = (schema) => { +const toParseSchema = schema => { if (schema.className === '_User') { delete schema.fields._hashed_password; } @@ -119,11 +127,11 @@ const toParseSchema = (schema) => { } let clps = defaultCLPS; if (schema.classLevelPermissions) { - clps = {...emptyCLPS, ...schema.classLevelPermissions}; + clps = { ...emptyCLPS, ...schema.classLevelPermissions }; } let indexes = {}; if (schema.indexes) { - indexes = {...schema.indexes}; + indexes = { ...schema.indexes }; } return { className: schema.className, @@ -131,23 +139,23 @@ const toParseSchema = (schema) => { classLevelPermissions: clps, indexes, }; -} +}; -const toPostgresSchema = (schema) => { +const toPostgresSchema = schema => { if (!schema) { return schema; } schema.fields = schema.fields || {}; - schema.fields._wperm = {type: 'Array', contents: {type: 'String'}} - schema.fields._rperm = {type: 'Array', contents: {type: 'String'}} + schema.fields._wperm = { type: 'Array', contents: { type: 'String' } }; + schema.fields._rperm = { type: 'Array', contents: { type: 'String' } }; if (schema.className === '_User') { - schema.fields._hashed_password = {type: 'String'}; - schema.fields._password_history = {type: 'Array'}; + schema.fields._hashed_password = { type: 'String' }; + schema.fields._password_history = { type: 'Array' }; } return schema; -} +}; -const handleDotFields = (object) => { +const handleDotFields = object => { Object.keys(object).forEach(fieldName => { if (fieldName.indexOf('.') > -1) { const components = fieldName.split('.'); @@ -160,8 +168,8 @@ const handleDotFields = (object) => { value = undefined; } /* eslint-disable no-cond-assign */ - while(next = components.shift()) { - /* eslint-enable no-cond-assign */ + while ((next = components.shift())) { + /* eslint-enable no-cond-assign */ currentObj[next] = currentObj[next] || {}; if (components.length === 0) { currentObj[next] = value; @@ -172,18 +180,18 @@ const handleDotFields = (object) => { } }); return object; -} +}; -const transformDotFieldToComponents = (fieldName) => { +const transformDotFieldToComponents = fieldName => { return fieldName.split('.').map((cmpt, index) => { if (index === 0) { return `"${cmpt}"`; } return `'${cmpt}'`; }); -} +}; -const transformDotField = (fieldName) => { +const transformDotField = fieldName => { if (fieldName.indexOf('.') === -1) { return `"${fieldName}"`; } @@ -191,9 +199,9 @@ const transformDotField = (fieldName) => { let name = components.slice(0, components.length - 1).join('->'); name += '->>' + components[components.length - 1]; return name; -} +}; -const transformAggregateField = (fieldName) => { +const transformAggregateField = fieldName => { if (typeof fieldName !== 'string') { return fieldName; } @@ -204,34 +212,37 @@ const transformAggregateField = (fieldName) => { return 'updatedAt'; } return fieldName.substr(1); -} +}; -const validateKeys = (object) => { +const validateKeys = object => { if (typeof object == 'object') { for (const key in object) { if (typeof object[key] == 'object') { validateKeys(object[key]); } - if(key.includes('$') || key.includes('.')){ - throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + if (key.includes('$') || key.includes('.')) { + throw new Parse.Error( + Parse.Error.INVALID_NESTED_KEY, + "Nested keys should not contain the '$' or '.' characters" + ); } } } -} +}; // Returns the list of join tables on a schema -const joinTablesForSchema = (schema) => { +const joinTablesForSchema = schema => { const list = []; if (schema) { - Object.keys(schema.fields).forEach((field) => { + Object.keys(schema.fields).forEach(field => { if (schema.fields[field].type === 'Relation') { list.push(`_Join:${field}:${schema.className}`); } }); } return list; -} +}; interface WhereClause { pattern: string; @@ -246,9 +257,10 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { schema = toPostgresSchema(schema); for (const fieldName in query) { - const isArrayField = schema.fields - && schema.fields[fieldName] - && schema.fields[fieldName].type === 'Array'; + const isArrayField = + schema.fields && + schema.fields[fieldName] && + schema.fields[fieldName].type === 'Array'; const initialPatternsLength = patterns.length; const fieldValue = query[fieldName]; @@ -268,7 +280,7 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { if (fieldValue.$in) { const inPatterns = []; name = transformDotFieldToComponents(fieldName).join('->'); - fieldValue.$in.forEach((listElem) => { + fieldValue.$in.forEach(listElem => { if (typeof listElem === 'string') { inPatterns.push(`"${listElem}"`); } else { @@ -294,7 +306,10 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { } else if (typeof fieldValue === 'boolean') { patterns.push(`$${index}:name = $${index + 1}`); // Can't cast boolean to double precision - if (schema.fields[fieldName] && schema.fields[fieldName].type === 'Number') { + if ( + schema.fields[fieldName] && + schema.fields[fieldName].type === 'Number' + ) { // Should always return zero results const MAX_INT_PLUS_ONE = 9223372036854775808; values.push(fieldName, MAX_INT_PLUS_ONE); @@ -309,7 +324,7 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { } else if (['$or', '$nor', '$and'].includes(fieldName)) { const clauses = []; const clauseValues = []; - fieldValue.forEach((subQuery) => { + fieldValue.forEach(subQuery => { const clause = buildWhereClause({ schema, query: subQuery, index }); if (clause.pattern.length > 0) { clauses.push(clause.pattern); @@ -337,7 +352,9 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { continue; } else { // if not null, we need to manually exclude null - patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`); + patterns.push( + `($${index}:name <> $${index + 1} OR $${index}:name IS NULL)` + ); } } @@ -356,11 +373,14 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { index += 2; } } - const isInOrNin = Array.isArray(fieldValue.$in) || Array.isArray(fieldValue.$nin); - if (Array.isArray(fieldValue.$in) && - isArrayField && - schema.fields[fieldName].contents && - schema.fields[fieldName].contents.type === 'String') { + const isInOrNin = + Array.isArray(fieldValue.$in) || Array.isArray(fieldValue.$nin); + if ( + Array.isArray(fieldValue.$in) && + isArrayField && + schema.fields[fieldName].contents && + schema.fields[fieldName].contents.type === 'String' + ) { const inPatterns = []; let allowNull = false; values.push(fieldName); @@ -373,7 +393,9 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { } }); if (allowNull) { - patterns.push(`($${index}:name IS NULL OR $${index}:name && ARRAY[${inPatterns.join()}])`); + patterns.push( + `($${index}:name IS NULL OR $${index}:name && ARRAY[${inPatterns.join()}])` + ); } else { patterns.push(`$${index}:name && ARRAY[${inPatterns.join()}]`); } @@ -383,7 +405,9 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { if (baseArray.length > 0) { const not = notIn ? ' NOT ' : ''; if (isArrayField) { - patterns.push(`${not} array_contains($${index}:name, $${index + 1})`); + patterns.push( + `${not} array_contains($${index}:name, $${index + 1})` + ); values.push(fieldName, JSON.stringify(baseArray)); index += 2; } else { @@ -407,14 +431,14 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { patterns.push(`$${index}:name IS NULL`); index = index + 1; } - } + }; if (fieldValue.$in) { createConstraint(_.flatMap(fieldValue.$in, elt => elt), false); } if (fieldValue.$nin) { createConstraint(_.flatMap(fieldValue.$nin, elt => elt), true); } - } else if(typeof fieldValue.$in !== 'undefined') { + } else if (typeof fieldValue.$in !== 'undefined') { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $in value'); } else if (typeof fieldValue.$nin !== 'undefined') { throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $nin value'); @@ -423,17 +447,23 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { if (Array.isArray(fieldValue.$all) && isArrayField) { if (isAnyValueRegexStartsWith(fieldValue.$all)) { if (!isAllValuesRegexOrNone(fieldValue.$all)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'All $all values must be of regex type or none: ' - + fieldValue.$all); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'All $all values must be of regex type or none: ' + fieldValue.$all + ); } for (let i = 0; i < fieldValue.$all.length; i += 1) { const value = processRegexPattern(fieldValue.$all[i].$regex); fieldValue.$all[i] = value.substring(1) + '%'; } - patterns.push(`array_contains_all_regex($${index}:name, $${index + 1}::jsonb)`); + patterns.push( + `array_contains_all_regex($${index}:name, $${index + 1}::jsonb)` + ); } else { - patterns.push(`array_contains_all($${index}:name, $${index + 1}::jsonb)`); + patterns.push( + `array_contains_all($${index}:name, $${index + 1}::jsonb)` + ); } values.push(fieldName, JSON.stringify(fieldValue.$all)); index += 2; @@ -497,7 +527,10 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { `bad $text: $caseSensitive not supported, please use $regex or create a separate lower case column.` ); } - if (search.$diacriticSensitive && typeof search.$diacriticSensitive !== 'boolean') { + if ( + search.$diacriticSensitive && + typeof search.$diacriticSensitive !== 'boolean' + ) { throw new Parse.Error( Parse.Error.INVALID_JSON, `bad $text: $diacriticSensitive, should be boolean` @@ -508,7 +541,10 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { `bad $text: $diacriticSensitive - false not supported, install Postgres Unaccent Extension` ); } - patterns.push(`to_tsvector($${index}, $${index + 1}:name) @@ to_tsquery($${index + 2}, $${index + 3})`); + patterns.push( + `to_tsvector($${index}, $${index + 1}:name) @@ to_tsquery($${index + + 2}, $${index + 3})` + ); values.push(language, fieldName, language, search.$term); index += 4; } @@ -517,8 +553,14 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { const point = fieldValue.$nearSphere; const distance = fieldValue.$maxDistance; const distanceInKM = distance * 6371 * 1000; - patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) <= $${index + 3}`); - sorts.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) ASC`) + patterns.push( + `ST_distance_sphere($${index}:name::geometry, POINT($${index + + 1}, $${index + 2})::geometry) <= $${index + 3}` + ); + sorts.push( + `ST_distance_sphere($${index}:name::geometry, POINT($${index + + 1}, $${index + 2})::geometry) ASC` + ); values.push(fieldName, point.longitude, point.latitude, distanceInKM); index += 4; } @@ -538,23 +580,35 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { if (fieldValue.$geoWithin && fieldValue.$geoWithin.$centerSphere) { const centerSphere = fieldValue.$geoWithin.$centerSphere; if (!(centerSphere instanceof Array) || centerSphere.length < 2) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere should be an array of Parse.GeoPoint and distance'); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value; $centerSphere should be an array of Parse.GeoPoint and distance' + ); } // Get point, convert to geo point if necessary and validate let point = centerSphere[0]; if (point instanceof Array && point.length === 2) { point = new Parse.GeoPoint(point[1], point[0]); } else if (!GeoPointCoder.isValidJSON(point)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere geo point invalid'); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value; $centerSphere geo point invalid' + ); } Parse.GeoPoint._validate(point.latitude, point.longitude); // Get distance and validate const distance = centerSphere[1]; - if(isNaN(distance) || distance < 0) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value; $centerSphere distance invalid'); + if (isNaN(distance) || distance < 0) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value; $centerSphere distance invalid' + ); } const distanceInKM = distance * 6371 * 1000; - patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) <= $${index + 3}`); + patterns.push( + `ST_distance_sphere($${index}:name::geometry, POINT($${index + + 1}, $${index + 2})::geometry) <= $${index + 3}` + ); values.push(fieldName, point.longitude, point.latitude, distanceInKM); index += 4; } @@ -570,7 +624,7 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { ); } points = polygon.coordinates; - } else if ((polygon instanceof Array)) { + } else if (polygon instanceof Array) { if (polygon.length < 3) { throw new Parse.Error( Parse.Error.INVALID_JSON, @@ -581,21 +635,26 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { } else { throw new Parse.Error( Parse.Error.INVALID_JSON, - 'bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint\'s' + "bad $geoWithin value; $polygon should be Polygon object or Array of Parse.GeoPoint's" ); } - points = points.map((point) => { - if (point instanceof Array && point.length === 2) { - Parse.GeoPoint._validate(point[1], point[0]); - return `(${point[0]}, ${point[1]})`; - } - if (typeof point !== 'object' || point.__type !== 'GeoPoint') { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value'); - } else { - Parse.GeoPoint._validate(point.latitude, point.longitude); - } - return `(${point.longitude}, ${point.latitude})`; - }).join(', '); + points = points + .map(point => { + if (point instanceof Array && point.length === 2) { + Parse.GeoPoint._validate(point[1], point[0]); + return `(${point[0]}, ${point[1]})`; + } + if (typeof point !== 'object' || point.__type !== 'GeoPoint') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad $geoWithin value' + ); + } else { + Parse.GeoPoint._validate(point.latitude, point.longitude); + } + return `(${point.longitude}, ${point.latitude})`; + }) + .join(', '); patterns.push(`$${index}:name::point <@ $${index + 1}::polygon`); values.push(fieldName, `(${points})`); @@ -656,7 +715,15 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { } if (fieldValue.__type === 'GeoPoint') { - patterns.push('$' + index + ':name ~= POINT($' + (index + 1) + ', $' + (index + 2) + ')'); + patterns.push( + '$' + + index + + ':name ~= POINT($' + + (index + 1) + + ', $' + + (index + 2) + + ')' + ); values.push(fieldName, fieldValue.longitude, fieldValue.latitude); index += 3; } @@ -678,15 +745,19 @@ const buildWhereClause = ({ schema, query, index }): WhereClause => { }); if (initialPatternsLength === patterns.length) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support this query type yet ${JSON.stringify(fieldValue)}`); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + `Postgres doesn't support this query type yet ${JSON.stringify( + fieldValue + )}` + ); } } values = values.map(transformValue); return { pattern: patterns.join(' AND '), values, sorts }; -} +}; export class PostgresStorageAdapter implements StorageAdapter { - canSortOnJoinTables: boolean; // Private @@ -694,11 +765,7 @@ export class PostgresStorageAdapter implements StorageAdapter { _client: any; _pgp: any; - constructor({ - uri, - collectionPrefix = '', - databaseOptions - }: any) { + constructor({ uri, collectionPrefix = '', databaseOptions }: any) { this._collectionPrefix = collectionPrefix; const { client, pgp } = createClient(uri, databaseOptions); this._client = client; @@ -715,12 +782,17 @@ export class PostgresStorageAdapter implements StorageAdapter { _ensureSchemaCollectionExists(conn: any) { conn = conn || this._client; - return conn.none('CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )') + return conn + .none( + 'CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )' + ) .catch(error => { - if (error.code === PostgresDuplicateRelationError - || error.code === PostgresUniqueIndexViolationError - || error.code === PostgresDuplicateObjectError) { - // Table already exists, must have been created by a different request. Ignore error. + if ( + error.code === PostgresDuplicateRelationError || + error.code === PostgresUniqueIndexViolationError || + error.code === PostgresDuplicateObjectError + ) { + // Table already exists, must have been created by a different request. Ignore error. } else { throw error; } @@ -728,36 +800,60 @@ export class PostgresStorageAdapter implements StorageAdapter { } classExists(name: string) { - return this._client.one('SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1)', [name], a => a.exists); + return this._client.one( + 'SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1)', + [name], + a => a.exists + ); } setClassLevelPermissions(className: string, CLPs: any) { const self = this; - return this._client.task('set-class-level-permissions', function * (t) { + return this._client.task('set-class-level-permissions', function*(t) { yield self._ensureSchemaCollectionExists(t); - const values = [className, 'schema', 'classLevelPermissions', JSON.stringify(CLPs)]; - yield t.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1`, values); + const values = [ + className, + 'schema', + 'classLevelPermissions', + JSON.stringify(CLPs), + ]; + yield t.none( + `UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1`, + values + ); }); } - setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any = {}, fields: any, conn: ?any): Promise { + setIndexesWithSchemaFormat( + className: string, + submittedIndexes: any, + existingIndexes: any = {}, + fields: any, + conn: ?any + ): Promise { conn = conn || this._client; const self = this; if (submittedIndexes === undefined) { return Promise.resolve(); } if (Object.keys(existingIndexes).length === 0) { - existingIndexes = { _id_: { _id: 1} }; + existingIndexes = { _id_: { _id: 1 } }; } const deletedIndexes = []; const insertedIndexes = []; Object.keys(submittedIndexes).forEach(name => { const field = submittedIndexes[name]; if (existingIndexes[name] && field.__op !== 'Delete') { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} exists, cannot update.`); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Index ${name} exists, cannot update.` + ); } if (!existingIndexes[name] && field.__op === 'Delete') { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Index ${name} does not exist, cannot delete.`); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Index ${name} does not exist, cannot delete.` + ); } if (field.__op === 'Delete') { deletedIndexes.push(name); @@ -765,7 +861,10 @@ export class PostgresStorageAdapter implements StorageAdapter { } else { Object.keys(field).forEach(key => { if (!fields.hasOwnProperty(key)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Field ${key} does not exist, cannot add index.`); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Field ${key} does not exist, cannot add index.` + ); } }); existingIndexes[name] = field; @@ -775,7 +874,7 @@ export class PostgresStorageAdapter implements StorageAdapter { }); } }); - return conn.tx('set-indexes-with-schema-format', function * (t) { + return conn.tx('set-indexes-with-schema-format', function*(t) { if (insertedIndexes.length > 0) { yield self.createIndexes(className, insertedIndexes, t); } @@ -783,18 +882,31 @@ export class PostgresStorageAdapter implements StorageAdapter { yield self.dropIndexes(className, deletedIndexes, t); } yield self._ensureSchemaCollectionExists(t); - yield t.none('UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1', [className, 'schema', 'indexes', JSON.stringify(existingIndexes)]); + yield t.none( + 'UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1', + [className, 'schema', 'indexes', JSON.stringify(existingIndexes)] + ); }); } createClass(className: string, schema: SchemaType, conn: ?any) { conn = conn || this._client; - return conn.tx('create-class', t => { - const q1 = this.createTable(className, schema, t); - const q2 = t.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($, $, true)', { className, schema }); - const q3 = this.setIndexesWithSchemaFormat(className, schema.indexes, {}, schema.fields, t); - return t.batch([q1, q2, q3]); - }) + return conn + .tx('create-class', t => { + const q1 = this.createTable(className, schema, t); + const q2 = t.none( + 'INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($, $, true)', + { className, schema } + ); + const q3 = this.setIndexesWithSchemaFormat( + className, + schema.indexes, + {}, + schema.fields, + t + ); + return t.batch([q1, q2, q3]); + }) .then(() => { return toParseSchema(schema); }) @@ -802,11 +914,17 @@ export class PostgresStorageAdapter implements StorageAdapter { if (err.data[0].result.code === PostgresTransactionAbortedError) { err = err.data[1].result; } - if (err.code === PostgresUniqueIndexViolationError && err.detail.includes(className)) { - throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, `Class ${className} already exists.`) + if ( + err.code === PostgresUniqueIndexViolationError && + err.detail.includes(className) + ) { + throw new Parse.Error( + Parse.Error.DUPLICATE_VALUE, + `Class ${className} already exists.` + ); } throw err; - }) + }); } // Just create a table, do not insert in schema @@ -818,23 +936,23 @@ export class PostgresStorageAdapter implements StorageAdapter { const patternsArray = []; const fields = Object.assign({}, schema.fields); if (className === '_User') { - fields._email_verify_token_expires_at = {type: 'Date'}; - fields._email_verify_token = {type: 'String'}; - fields._account_lockout_expires_at = {type: 'Date'}; - fields._failed_login_count = {type: 'Number'}; - fields._perishable_token = {type: 'String'}; - fields._perishable_token_expires_at = {type: 'Date'}; - fields._password_changed_at = {type: 'Date'}; - fields._password_history = { type: 'Array'}; + fields._email_verify_token_expires_at = { type: 'Date' }; + fields._email_verify_token = { type: 'String' }; + fields._account_lockout_expires_at = { type: 'Date' }; + fields._failed_login_count = { type: 'Number' }; + fields._perishable_token = { type: 'String' }; + fields._perishable_token_expires_at = { type: 'Date' }; + fields._password_changed_at = { type: 'Date' }; + fields._password_history = { type: 'Array' }; } let index = 2; const relations = []; - Object.keys(fields).forEach((fieldName) => { + Object.keys(fields).forEach(fieldName => { const parseType = fields[fieldName]; // Skip when it's a relation // We'll create the tables later if (parseType.type === 'Relation') { - relations.push(fieldName) + relations.push(fieldName); return; } if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) { @@ -844,27 +962,32 @@ export class PostgresStorageAdapter implements StorageAdapter { valuesArray.push(parseTypeToPostgresType(parseType)); patternsArray.push(`$${index}:name $${index + 1}:raw`); if (fieldName === 'objectId') { - patternsArray.push(`PRIMARY KEY ($${index}:name)`) + patternsArray.push(`PRIMARY KEY ($${index}:name)`); } index = index + 2; }); const qs = `CREATE TABLE IF NOT EXISTS $1:name (${patternsArray.join()})`; const values = [className, ...valuesArray]; - return conn.task('create-table', function * (t) { + return conn.task('create-table', function*(t) { try { yield self._ensureSchemaCollectionExists(t); yield t.none(qs, values); - } catch(error) { + } catch (error) { if (error.code !== PostgresDuplicateRelationError) { throw error; } // ELSE: Table already exists, must have been created by a different request. Ignore the error. } yield t.tx('create-table-tx', tx => { - return tx.batch(relations.map(fieldName => { - return tx.none('CREATE TABLE IF NOT EXISTS $ ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', {joinTable: `_Join:${fieldName}:${className}`}); - })); + return tx.batch( + relations.map(fieldName => { + return tx.none( + 'CREATE TABLE IF NOT EXISTS $ ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', + { joinTable: `_Join:${fieldName}:${className}` } + ); + }) + ); }); }); } @@ -874,32 +997,55 @@ export class PostgresStorageAdapter implements StorageAdapter { conn = conn || this._client; const self = this; - return conn.tx('schema-upgrade', function * (t) { - const columns = yield t.map('SELECT column_name FROM information_schema.columns WHERE table_name = $', { className }, a => a.column_name); + return conn.tx('schema-upgrade', function*(t) { + const columns = yield t.map( + 'SELECT column_name FROM information_schema.columns WHERE table_name = $', + { className }, + a => a.column_name + ); const newColumns = Object.keys(schema.fields) .filter(item => columns.indexOf(item) === -1) - .map(fieldName => self.addFieldIfNotExists(className, fieldName, schema.fields[fieldName], t)); + .map(fieldName => + self.addFieldIfNotExists( + className, + fieldName, + schema.fields[fieldName], + t + ) + ); yield t.batch(newColumns); }); } - addFieldIfNotExists(className: string, fieldName: string, type: any, conn: any) { + addFieldIfNotExists( + className: string, + fieldName: string, + type: any, + conn: any + ) { // TODO: Must be revised for invalid logic... - debug('addFieldIfNotExists', {className, fieldName, type}); + debug('addFieldIfNotExists', { className, fieldName, type }); conn = conn || this._client; const self = this; - return conn.tx('add-field-if-not-exists', function * (t) { + return conn.tx('add-field-if-not-exists', function*(t) { if (type.type !== 'Relation') { try { - yield t.none('ALTER TABLE $ ADD COLUMN $ $', { - className, - fieldName, - postgresType: parseTypeToPostgresType(type) - }); - } catch(error) { + yield t.none( + 'ALTER TABLE $ ADD COLUMN $ $', + { + className, + fieldName, + postgresType: parseTypeToPostgresType(type), + } + ); + } catch (error) { if (error.code === PostgresRelationDoesNotExistError) { - return yield self.createClass(className, {fields: {[fieldName]: type}}, t); + return yield self.createClass( + className, + { fields: { [fieldName]: type } }, + t + ); } if (error.code !== PostgresDuplicateColumnError) { throw error; @@ -907,16 +1053,25 @@ export class PostgresStorageAdapter implements StorageAdapter { // Column already exists, created by other request. Carry on to see if it's the right type. } } else { - yield t.none('CREATE TABLE IF NOT EXISTS $ ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', {joinTable: `_Join:${fieldName}:${className}`}); + yield t.none( + 'CREATE TABLE IF NOT EXISTS $ ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', + { joinTable: `_Join:${fieldName}:${className}` } + ); } - const result = yield t.any('SELECT "schema" FROM "_SCHEMA" WHERE "className" = $ and ("schema"::json->\'fields\'->$) is not null', {className, fieldName}); + const result = yield t.any( + 'SELECT "schema" FROM "_SCHEMA" WHERE "className" = $ and ("schema"::json->\'fields\'->$) is not null', + { className, fieldName } + ); if (result[0]) { throw 'Attempted to add a field that already exists'; } else { const path = `{fields,${fieldName}}`; - yield t.none('UPDATE "_SCHEMA" SET "schema"=jsonb_set("schema", $, $) WHERE "className"=$', {path, type, className}); + yield t.none( + 'UPDATE "_SCHEMA" SET "schema"=jsonb_set("schema", $, $) WHERE "className"=$', + { path, type, className } + ); } }); } @@ -925,10 +1080,14 @@ export class PostgresStorageAdapter implements StorageAdapter { // and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible. deleteClass(className: string) { const operations = [ - {query: `DROP TABLE IF EXISTS $1:name`, values: [className]}, - {query: `DELETE FROM "_SCHEMA" WHERE "className" = $1`, values: [className]} + { query: `DROP TABLE IF EXISTS $1:name`, values: [className] }, + { + query: `DELETE FROM "_SCHEMA" WHERE "className" = $1`, + values: [className], + }, ]; - return this._client.tx(t => t.none(this._pgp.helpers.concat(operations))) + return this._client + .tx(t => t.none(this._pgp.helpers.concat(operations))) .then(() => className.indexOf('_Join:') != 0); // resolves with false when _Join table } @@ -938,22 +1097,36 @@ export class PostgresStorageAdapter implements StorageAdapter { const helpers = this._pgp.helpers; debug('deleteAllClasses'); - return this._client.task('delete-all-classes', function * (t) { - try { - const results = yield t.any('SELECT * FROM "_SCHEMA"'); - const joins = results.reduce((list: Array, schema: any) => { - return list.concat(joinTablesForSchema(schema.schema)); - }, []); - const classes = ['_SCHEMA', '_PushStatus', '_JobStatus', '_JobSchedule', '_Hooks', '_GlobalConfig', '_Audience', ...results.map(result => result.className), ...joins]; - const queries = classes.map(className => ({query: 'DROP TABLE IF EXISTS $', values: {className}})); - yield t.tx(tx => tx.none(helpers.concat(queries))); - } catch(error) { - if (error.code !== PostgresRelationDoesNotExistError) { - throw error; + return this._client + .task('delete-all-classes', function*(t) { + try { + const results = yield t.any('SELECT * FROM "_SCHEMA"'); + const joins = results.reduce((list: Array, schema: any) => { + return list.concat(joinTablesForSchema(schema.schema)); + }, []); + const classes = [ + '_SCHEMA', + '_PushStatus', + '_JobStatus', + '_JobSchedule', + '_Hooks', + '_GlobalConfig', + '_Audience', + ...results.map(result => result.className), + ...joins, + ]; + const queries = classes.map(className => ({ + query: 'DROP TABLE IF EXISTS $', + values: { className }, + })); + yield t.tx(tx => tx.none(helpers.concat(queries))); + } catch (error) { + if (error.code !== PostgresRelationDoesNotExistError) { + throw error; + } + // No _SCHEMA collection. Don't delete anything. } - // No _SCHEMA collection. Don't delete anything. - } - }) + }) .then(() => { debug(`deleteAllClasses done in ${new Date().getTime() - now}`); }); @@ -972,10 +1145,14 @@ export class PostgresStorageAdapter implements StorageAdapter { // may do so. // Returns a Promise. - deleteFields(className: string, schema: SchemaType, fieldNames: string[]): Promise { + deleteFields( + className: string, + schema: SchemaType, + fieldNames: string[] + ): Promise { debug('deleteFields', className, fieldNames); fieldNames = fieldNames.reduce((list: Array, fieldName: string) => { - const field = schema.fields[fieldName] + const field = schema.fields[fieldName]; if (field.type !== 'Relation') { list.push(fieldName); } @@ -984,12 +1161,17 @@ export class PostgresStorageAdapter implements StorageAdapter { }, []); const values = [className, ...fieldNames]; - const columns = fieldNames.map((name, idx) => { - return `$${idx + 2}:name`; - }).join(', DROP COLUMN'); + const columns = fieldNames + .map((name, idx) => { + return `$${idx + 2}:name`; + }) + .join(', DROP COLUMN'); - return this._client.tx('delete-fields', function * (t) { - yield t.none('UPDATE "_SCHEMA" SET "schema"=$ WHERE "className"=$', {schema, className}); + return this._client.tx('delete-fields', function*(t) { + yield t.none( + 'UPDATE "_SCHEMA" SET "schema"=$ WHERE "className"=$', + { schema, className } + ); if (values.length > 1) { yield t.none(`ALTER TABLE $1:name DROP COLUMN ${columns}`, values); } @@ -1001,9 +1183,11 @@ export class PostgresStorageAdapter implements StorageAdapter { // rejection reason are TBD. getAllClasses() { const self = this; - return this._client.task('get-all-classes', function * (t) { + return this._client.task('get-all-classes', function*(t) { yield self._ensureSchemaCollectionExists(t); - return yield t.map('SELECT * FROM "_SCHEMA"', null, row => toParseSchema({ className: row.className, ...row.schema })); + return yield t.map('SELECT * FROM "_SCHEMA"', null, row => + toParseSchema({ className: row.className, ...row.schema }) + ); }); } @@ -1012,7 +1196,10 @@ export class PostgresStorageAdapter implements StorageAdapter { // undefined as the reason. getClass(className: string) { debug('getClass', className); - return this._client.any('SELECT * FROM "_SCHEMA" WHERE "className"=$', { className }) + return this._client + .any('SELECT * FROM "_SCHEMA" WHERE "className"=$', { + className, + }) .then(result => { if (result.length !== 1) { throw undefined; @@ -1049,10 +1236,12 @@ export class PostgresStorageAdapter implements StorageAdapter { columnsArray.push(fieldName); if (!schema.fields[fieldName] && className === '_User') { - if (fieldName === '_email_verify_token' || - fieldName === '_failed_login_count' || - fieldName === '_perishable_token' || - fieldName === '_password_history'){ + if ( + fieldName === '_email_verify_token' || + fieldName === '_failed_login_count' || + fieldName === '_perishable_token' || + fieldName === '_password_history' + ) { valuesArray.push(object[fieldName]); } @@ -1064,9 +1253,11 @@ export class PostgresStorageAdapter implements StorageAdapter { } } - if (fieldName === '_account_lockout_expires_at' || - fieldName === '_perishable_token_expires_at' || - fieldName === '_password_changed_at') { + if ( + fieldName === '_account_lockout_expires_at' || + fieldName === '_perishable_token_expires_at' || + fieldName === '_password_changed_at' + ) { if (object[fieldName]) { valuesArray.push(object[fieldName].iso); } else { @@ -1076,45 +1267,45 @@ export class PostgresStorageAdapter implements StorageAdapter { return; } switch (schema.fields[fieldName].type) { - case 'Date': - if (object[fieldName]) { - valuesArray.push(object[fieldName].iso); - } else { - valuesArray.push(null); - } - break; - case 'Pointer': - valuesArray.push(object[fieldName].objectId); - break; - case 'Array': - if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) { + case 'Date': + if (object[fieldName]) { + valuesArray.push(object[fieldName].iso); + } else { + valuesArray.push(null); + } + break; + case 'Pointer': + valuesArray.push(object[fieldName].objectId); + break; + case 'Array': + if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) { + valuesArray.push(object[fieldName]); + } else { + valuesArray.push(JSON.stringify(object[fieldName])); + } + break; + case 'Object': + case 'Bytes': + case 'String': + case 'Number': + case 'Boolean': valuesArray.push(object[fieldName]); - } else { - valuesArray.push(JSON.stringify(object[fieldName])); + break; + case 'File': + valuesArray.push(object[fieldName].name); + break; + case 'Polygon': { + const value = convertPolygonToSQL(object[fieldName].coordinates); + valuesArray.push(value); + break; } - break; - case 'Object': - case 'Bytes': - case 'String': - case 'Number': - case 'Boolean': - valuesArray.push(object[fieldName]); - break; - case 'File': - valuesArray.push(object[fieldName].name); - break; - case 'Polygon': { - const value = convertPolygonToSQL(object[fieldName].coordinates); - valuesArray.push(value); - break; - } - case 'GeoPoint': - // pop the point and process later - geoPoints[fieldName] = object[fieldName]; - columnsArray.pop(); - break; - default: - throw `Type ${schema.fields[fieldName].type} not supported yet`; + case 'GeoPoint': + // pop the point and process later + geoPoints[fieldName] = object[fieldName]; + columnsArray.pop(); + break; + default: + throw `Type ${schema.fields[fieldName].type} not supported yet`; } }); @@ -1122,31 +1313,40 @@ export class PostgresStorageAdapter implements StorageAdapter { const initialValues = valuesArray.map((val, index) => { let termination = ''; const fieldName = columnsArray[index]; - if (['_rperm','_wperm'].indexOf(fieldName) >= 0) { + if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) { termination = '::text[]'; - } else if (schema.fields[fieldName] && schema.fields[fieldName].type === 'Array') { + } else if ( + schema.fields[fieldName] && + schema.fields[fieldName].type === 'Array' + ) { termination = '::jsonb'; } return `$${index + 2 + columnsArray.length}${termination}`; }); - const geoPointsInjects = Object.keys(geoPoints).map((key) => { + const geoPointsInjects = Object.keys(geoPoints).map(key => { const value = geoPoints[key]; valuesArray.push(value.longitude, value.latitude); const l = valuesArray.length + columnsArray.length; return `POINT($${l}, $${l + 1})`; }); - const columnsPattern = columnsArray.map((col, index) => `$${index + 2}:name`).join(); - const valuesPattern = initialValues.concat(geoPointsInjects).join() + const columnsPattern = columnsArray + .map((col, index) => `$${index + 2}:name`) + .join(); + const valuesPattern = initialValues.concat(geoPointsInjects).join(); - const qs = `INSERT INTO $1:name (${columnsPattern}) VALUES (${valuesPattern})` - const values = [className, ...columnsArray, ...valuesArray] + const qs = `INSERT INTO $1:name (${columnsPattern}) VALUES (${valuesPattern})`; + const values = [className, ...columnsArray, ...valuesArray]; debug(qs, values); - return this._client.none(qs, values) + return this._client + .none(qs, values) .then(() => ({ ops: [object] })) .catch(error => { if (error.code === PostgresUniqueIndexViolationError) { - const err = new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + const err = new Parse.Error( + Parse.Error.DUPLICATE_VALUE, + 'A duplicate value for a field with unique values was provided' + ); err.underlyingError = error; if (error.constraint) { const matches = error.constraint.match(/unique_([a-zA-Z]+)/); @@ -1163,21 +1363,31 @@ export class PostgresStorageAdapter implements StorageAdapter { // 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: string, schema: SchemaType, query: QueryType) { + deleteObjectsByQuery( + className: string, + schema: SchemaType, + query: QueryType + ) { debug('deleteObjectsByQuery', className, query); const values = [className]; const index = 2; - const where = buildWhereClause({ schema, index, query }) + const where = buildWhereClause({ schema, index, query }); values.push(...where.values); if (Object.keys(query).length === 0) { where.pattern = 'TRUE'; } - const qs = `WITH deleted AS (DELETE FROM $1:name WHERE ${where.pattern} RETURNING *) SELECT count(*) FROM deleted`; + const qs = `WITH deleted AS (DELETE FROM $1:name WHERE ${ + where.pattern + } RETURNING *) SELECT count(*) FROM deleted`; debug(qs, values); - return this._client.one(qs, values , a => +a.count) + return this._client + .one(qs, values, a => +a.count) .then(count => { if (count === 0) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); } else { return count; } @@ -1190,21 +1400,32 @@ export class PostgresStorageAdapter implements StorageAdapter { }); } // Return value not currently well specified. - findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any): Promise { + findOneAndUpdate( + className: string, + schema: SchemaType, + query: QueryType, + update: any + ): Promise { debug('findOneAndUpdate', className, query, update); - return this.updateObjectsByQuery(className, schema, query, update) - .then((val) => val[0]); + return this.updateObjectsByQuery(className, schema, query, update).then( + val => val[0] + ); } // Apply the update to all objects that match the given Parse Query. - updateObjectsByQuery(className: string, schema: SchemaType, query: QueryType, update: any): Promise<[any]> { + updateObjectsByQuery( + className: string, + schema: SchemaType, + query: QueryType, + update: any + ): Promise<[any]> { debug('updateObjectsByQuery', className, query, update); const updatePatterns = []; - const values = [className] + const values = [className]; let index = 2; schema = toPostgresSchema(schema); - const originalUpdate = {...update}; + const originalUpdate = { ...update }; update = handleDotFields(update); // Resolve authData first, // So we don't end up with multiple key updates @@ -1230,48 +1451,67 @@ export class PostgresStorageAdapter implements StorageAdapter { // Only 1 level deep const generate = (jsonb: string, key: string, value: any) => { return `json_object_set_key(COALESCE(${jsonb}, '{}'::jsonb), ${key}, ${value})::jsonb`; - } + }; const lastKey = `$${index}:name`; const fieldNameIndex = index; index += 1; values.push(fieldName); - const update = Object.keys(fieldValue).reduce((lastKey: string, key: string) => { - const str = generate(lastKey, `$${index}::text`, `$${index + 1}::jsonb`) - index += 2; - let value = fieldValue[key]; - if (value) { - if (value.__op === 'Delete') { - value = null; - } else { - value = JSON.stringify(value) + const update = Object.keys(fieldValue).reduce( + (lastKey: string, key: string) => { + const str = generate( + lastKey, + `$${index}::text`, + `$${index + 1}::jsonb` + ); + index += 2; + let value = fieldValue[key]; + if (value) { + if (value.__op === 'Delete') { + value = null; + } else { + value = JSON.stringify(value); + } } - } - values.push(key, value); - return str; - }, lastKey); + values.push(key, value); + return str; + }, + lastKey + ); updatePatterns.push(`$${fieldNameIndex}:name = ${update}`); } else if (fieldValue.__op === 'Increment') { - updatePatterns.push(`$${index}:name = COALESCE($${index}:name, 0) + $${index + 1}`); + 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 = array_add(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`); + updatePatterns.push( + `$${index}:name = array_add(COALESCE($${index}:name, '[]'::jsonb), $${index + + 1}::jsonb)` + ); values.push(fieldName, JSON.stringify(fieldValue.objects)); index += 2; } else if (fieldValue.__op === 'Delete') { - updatePatterns.push(`$${index}:name = $${index + 1}`) + updatePatterns.push(`$${index}:name = $${index + 1}`); values.push(fieldName, null); index += 2; } else if (fieldValue.__op === 'Remove') { - updatePatterns.push(`$${index}:name = array_remove(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`) + updatePatterns.push( + `$${index}:name = array_remove(COALESCE($${index}:name, '[]'::jsonb), $${index + + 1}::jsonb)` + ); values.push(fieldName, JSON.stringify(fieldValue.objects)); index += 2; } else if (fieldValue.__op === 'AddUnique') { - updatePatterns.push(`$${index}:name = array_add_unique(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`); + updatePatterns.push( + `$${index}:name = array_add_unique(COALESCE($${index}:name, '[]'::jsonb), $${index + + 1}::jsonb)` + ); values.push(fieldName, JSON.stringify(fieldValue.objects)); index += 2; - } else if (fieldName === 'updatedAt') { //TODO: stop special casing this. It should check for __type === 'Date' and use .iso - updatePatterns.push(`$${index}:name = $${index + 1}`) + } 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, fieldValue); index += 2; } else if (typeof fieldValue === 'string') { @@ -1299,7 +1539,9 @@ export class PostgresStorageAdapter implements StorageAdapter { values.push(fieldName, toPostgresValue(fieldValue)); index += 2; } else if (fieldValue.__type === 'GeoPoint') { - updatePatterns.push(`$${index}:name = POINT($${index + 1}, $${index + 2})`); + updatePatterns.push( + `$${index}:name = POINT($${index + 1}, $${index + 2})` + ); values.push(fieldName, fieldValue.longitude, fieldValue.latitude); index += 3; } else if (fieldValue.__type === 'Polygon') { @@ -1313,48 +1555,77 @@ export class PostgresStorageAdapter implements StorageAdapter { updatePatterns.push(`$${index}:name = $${index + 1}`); values.push(fieldName, fieldValue); index += 2; - } else if (typeof fieldValue === 'object' - && schema.fields[fieldName] - && schema.fields[fieldName].type === 'Object') { + } else if ( + typeof fieldValue === 'object' && + schema.fields[fieldName] && + schema.fields[fieldName].type === 'Object' + ) { // Gather keys to increment - const keysToIncrement = Object.keys(originalUpdate).filter(k => { - // choose top level fields that have a delete operation set - // Note that Object.keys is iterating over the **original** update object - // and that some of the keys of the original update could be null or undefined: - // (See the above check `if (fieldValue === null || typeof fieldValue == "undefined")`) - const value = originalUpdate[k]; - return value && value.__op === 'Increment' && k.split('.').length === 2 && k.split(".")[0] === fieldName; - }).map(k => k.split('.')[1]); + const keysToIncrement = Object.keys(originalUpdate) + .filter(k => { + // choose top level fields that have a delete operation set + // Note that Object.keys is iterating over the **original** update object + // and that some of the keys of the original update could be null or undefined: + // (See the above check `if (fieldValue === null || typeof fieldValue == "undefined")`) + const value = originalUpdate[k]; + return ( + value && + value.__op === 'Increment' && + k.split('.').length === 2 && + k.split('.')[0] === fieldName + ); + }) + .map(k => k.split('.')[1]); let incrementPatterns = ''; if (keysToIncrement.length > 0) { - incrementPatterns = ' || ' + keysToIncrement.map((c) => { - const amount = fieldValue[c].amount; - return `CONCAT('{"${c}":', COALESCE($${index}:name->>'${c}','0')::int + ${amount}, '}')::jsonb`; - }).join(' || '); + incrementPatterns = + ' || ' + + keysToIncrement + .map(c => { + const amount = fieldValue[c].amount; + return `CONCAT('{"${c}":', COALESCE($${index}:name->>'${c}','0')::int + ${amount}, '}')::jsonb`; + }) + .join(' || '); // Strip the keys - keysToIncrement.forEach((key) => { + keysToIncrement.forEach(key => { delete fieldValue[key]; }); } - const keysToDelete: Array = Object.keys(originalUpdate).filter(k => { - // choose top level fields that have a delete operation set. - const value = originalUpdate[k]; - return value && value.__op === 'Delete' && k.split('.').length === 2 && k.split(".")[0] === fieldName; - }).map(k => k.split('.')[1]); - - const deletePatterns = keysToDelete.reduce((p: string, c: string, i: number) => { - return p + ` - '$${index + 1 + i}:value'`; - }, ''); + const keysToDelete: Array = Object.keys(originalUpdate) + .filter(k => { + // choose top level fields that have a delete operation set. + const value = originalUpdate[k]; + return ( + value && + value.__op === 'Delete' && + k.split('.').length === 2 && + k.split('.')[0] === fieldName + ); + }) + .map(k => k.split('.')[1]); + + const deletePatterns = keysToDelete.reduce( + (p: string, c: string, i: number) => { + return p + ` - '$${index + 1 + i}:value'`; + }, + '' + ); - updatePatterns.push(`$${index}:name = ('{}'::jsonb ${deletePatterns} ${incrementPatterns} || $${index + 1 + keysToDelete.length}::jsonb )`); + updatePatterns.push( + `$${index}:name = ('{}'::jsonb ${deletePatterns} ${incrementPatterns} || $${index + + 1 + + keysToDelete.length}::jsonb )` + ); values.push(fieldName, ...keysToDelete, JSON.stringify(fieldValue)); index += 2 + keysToDelete.length; - } else if (Array.isArray(fieldValue) - && schema.fields[fieldName] - && schema.fields[fieldName].type === 'Array') { + } else if ( + Array.isArray(fieldValue) && + schema.fields[fieldName] && + schema.fields[fieldName].type === 'Array' + ) { const expectedType = parseTypeToPostgresType(schema.fields[fieldName]); if (expectedType === 'text[]') { updatePatterns.push(`$${index}:name = $${index + 1}::text[]`); @@ -1366,48 +1637,66 @@ export class PostgresStorageAdapter implements StorageAdapter { break; } } - updatePatterns.push(`$${index}:name = array_to_json($${index + 1}::${type}[])::jsonb`); + updatePatterns.push( + `$${index}:name = array_to_json($${index + 1}::${type}[])::jsonb` + ); } values.push(fieldName, fieldValue); index += 2; } else { debug('Not supported update', fieldName, fieldValue); - return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support update ${JSON.stringify(fieldValue)} yet`)); + return Promise.reject( + new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + `Postgres doesn't support update ${JSON.stringify(fieldValue)} yet` + ) + ); } } - const where = buildWhereClause({ schema, index, query }) + const where = buildWhereClause({ schema, index, query }); values.push(...where.values); - const whereClause = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; + const whereClause = + where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; const qs = `UPDATE $1:name SET ${updatePatterns.join()} ${whereClause} RETURNING *`; debug('update: ', qs, values); return this._client.any(qs, values); } // Hopefully, we can get rid of this. It's only used for config and hooks. - upsertOneObject(className: string, schema: SchemaType, query: QueryType, update: any) { - debug('upsertOneObject', {className, query, update}); + upsertOneObject( + className: string, + schema: SchemaType, + query: QueryType, + update: any + ) { + debug('upsertOneObject', { className, query, update }); const createValue = Object.assign({}, query, update); - return this.createObject(className, schema, createValue) - .catch(error => { - // ignore duplicate value errors as it's upsert - if (error.code !== Parse.Error.DUPLICATE_VALUE) { - throw error; - } - return this.findOneAndUpdate(className, schema, query, update); - }); + return this.createObject(className, schema, createValue).catch(error => { + // ignore duplicate value errors as it's upsert + if (error.code !== Parse.Error.DUPLICATE_VALUE) { + throw error; + } + return this.findOneAndUpdate(className, schema, query, update); + }); } - find(className: string, schema: SchemaType, query: QueryType, { skip, limit, sort, keys }: QueryOptions) { - debug('find', className, query, {skip, limit, sort, keys }); + find( + className: string, + schema: SchemaType, + query: QueryType, + { skip, limit, sort, keys }: QueryOptions + ) { + debug('find', className, query, { skip, limit, sort, keys }); const hasLimit = limit !== undefined; const hasSkip = skip !== undefined; let values = [className]; - const where = buildWhereClause({ schema, query, index: 2 }) + const where = buildWhereClause({ schema, query, index: 2 }); values.push(...where.values); - const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; + const wherePattern = + where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; const limitPattern = hasLimit ? `LIMIT $${values.length + 1}` : ''; if (hasLimit) { values.push(limit); @@ -1420,15 +1709,20 @@ export class PostgresStorageAdapter implements StorageAdapter { let sortPattern = ''; if (sort) { const sortCopy: any = sort; - const sorting = Object.keys(sort).map((key) => { - const transformKey = transformDotFieldToComponents(key).join('->'); - // Using $idx pattern gives: non-integer constant in ORDER BY - if (sortCopy[key] === 1) { - return `${transformKey} ASC`; - } - return `${transformKey} DESC`; - }).join(); - sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : ''; + const sorting = Object.keys(sort) + .map(key => { + const transformKey = transformDotFieldToComponents(key).join('->'); + // Using $idx pattern gives: non-integer constant in ORDER BY + if (sortCopy[key] === 1) { + return `${transformKey} ASC`; + } + return `${transformKey} DESC`; + }) + .join(); + sortPattern = + sort !== undefined && Object.keys(sort).length > 0 + ? `ORDER BY ${sorting}` + : ''; } if (where.sorts && Object.keys((where.sorts: any)).length > 0) { sortPattern = `ORDER BY ${where.sorts.join()}`; @@ -1447,18 +1741,21 @@ export class PostgresStorageAdapter implements StorageAdapter { } return memo; }, []); - columns = keys.map((key, index) => { - if (key === '$score') { - return `ts_rank_cd(to_tsvector($${2}, $${3}:name), to_tsquery($${4}, $${5}), 32) as score`; - } - return `$${index + values.length + 1}:name`; - }).join(); + columns = keys + .map((key, index) => { + if (key === '$score') { + return `ts_rank_cd(to_tsvector($${2}, $${3}:name), to_tsquery($${4}, $${5}), 32) as score`; + } + return `$${index + values.length + 1}:name`; + }) + .join(); values = values.concat(keys); } const qs = `SELECT ${columns} FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern}`; debug(qs, values); - return this._client.any(qs, values) + return this._client + .any(qs, values) .catch(error => { // Query on non existing table, don't crash if (error.code !== PostgresRelationDoesNotExistError) { @@ -1466,7 +1763,11 @@ export class PostgresStorageAdapter implements StorageAdapter { } return []; }) - .then(results => results.map(object => this.postgresObjectToParseObject(className, object, schema))); + .then(results => + results.map(object => + this.postgresObjectToParseObject(className, object, schema) + ) + ); } // Converts from a postgres-format object to a REST-format object. @@ -1474,40 +1775,44 @@ export class PostgresStorageAdapter implements StorageAdapter { postgresObjectToParseObject(className: string, object: any, schema: any) { Object.keys(schema.fields).forEach(fieldName => { if (schema.fields[fieldName].type === 'Pointer' && object[fieldName]) { - object[fieldName] = { objectId: object[fieldName], __type: 'Pointer', className: schema.fields[fieldName].targetClass }; + object[fieldName] = { + objectId: object[fieldName], + __type: 'Pointer', + className: schema.fields[fieldName].targetClass, + }; } if (schema.fields[fieldName].type === 'Relation') { object[fieldName] = { - __type: "Relation", - className: schema.fields[fieldName].targetClass - } + __type: 'Relation', + className: schema.fields[fieldName].targetClass, + }; } if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') { object[fieldName] = { - __type: "GeoPoint", + __type: 'GeoPoint', latitude: object[fieldName].y, - longitude: object[fieldName].x - } + longitude: object[fieldName].x, + }; } if (object[fieldName] && schema.fields[fieldName].type === 'Polygon') { let coords = object[fieldName]; coords = coords.substr(2, coords.length - 4).split('),('); - coords = coords.map((point) => { + coords = coords.map(point => { return [ parseFloat(point.split(',')[1]), - parseFloat(point.split(',')[0]) + parseFloat(point.split(',')[0]), ]; }); object[fieldName] = { - __type: "Polygon", - coordinates: coords - } + __type: 'Polygon', + coordinates: coords, + }; } if (object[fieldName] && schema.fields[fieldName].type === 'File') { object[fieldName] = { __type: 'File', - name: object[fieldName] - } + name: object[fieldName], + }; } }); //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. @@ -1518,19 +1823,34 @@ export class PostgresStorageAdapter implements StorageAdapter { object.updatedAt = object.updatedAt.toISOString(); } if (object.expiresAt) { - object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() }; + object.expiresAt = { + __type: 'Date', + iso: object.expiresAt.toISOString(), + }; } if (object._email_verify_token_expires_at) { - object._email_verify_token_expires_at = { __type: 'Date', iso: object._email_verify_token_expires_at.toISOString() }; + object._email_verify_token_expires_at = { + __type: 'Date', + iso: object._email_verify_token_expires_at.toISOString(), + }; } if (object._account_lockout_expires_at) { - object._account_lockout_expires_at = { __type: 'Date', iso: object._account_lockout_expires_at.toISOString() }; + object._account_lockout_expires_at = { + __type: 'Date', + iso: object._account_lockout_expires_at.toISOString(), + }; } if (object._perishable_token_expires_at) { - object._perishable_token_expires_at = { __type: 'Date', iso: object._perishable_token_expires_at.toISOString() }; + object._perishable_token_expires_at = { + __type: 'Date', + iso: object._perishable_token_expires_at.toISOString(), + }; } if (object._password_changed_at) { - object._password_changed_at = { __type: 'Date', iso: object._password_changed_at.toISOString() }; + object._password_changed_at = { + __type: 'Date', + iso: object._password_changed_at.toISOString(), + }; } for (const fieldName in object) { @@ -1538,7 +1858,10 @@ export class PostgresStorageAdapter implements StorageAdapter { delete object[fieldName]; } if (object[fieldName] instanceof Date) { - object[fieldName] = { __type: 'Date', iso: object[fieldName].toISOString() }; + object[fieldName] = { + __type: 'Date', + iso: object[fieldName].toISOString(), + }; } } @@ -1550,19 +1873,35 @@ export class PostgresStorageAdapter implements StorageAdapter { // 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: string, schema: SchemaType, fieldNames: string[]) { + ensureUniqueness( + className: string, + schema: SchemaType, + fieldNames: string[] + ) { // 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 constraintPatterns = fieldNames.map( + (fieldName, index) => `$${index + 3}:name` + ); const qs = `ALTER TABLE $1:name ADD CONSTRAINT $2:name UNIQUE (${constraintPatterns.join()})`; - return this._client.none(qs, [className, constraintName, ...fieldNames]) + return this._client + .none(qs, [className, constraintName, ...fieldNames]) .catch(error => { - if (error.code === PostgresDuplicateRelationError && error.message.includes(constraintName)) { - // Index already exists. Ignore error. - } else if (error.code === PostgresUniqueIndexViolationError && error.message.includes(constraintName)) { - // Cast the error into the proper parse error - throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + if ( + error.code === PostgresDuplicateRelationError && + error.message.includes(constraintName) + ) { + // Index already exists. Ignore error. + } else if ( + error.code === PostgresUniqueIndexViolationError && + error.message.includes(constraintName) + ) { + // Cast the error into the proper parse error + throw new Parse.Error( + Parse.Error.DUPLICATE_VALUE, + 'A duplicate value for a field with unique values was provided' + ); } else { throw error; } @@ -1576,18 +1915,23 @@ export class PostgresStorageAdapter implements StorageAdapter { const where = buildWhereClause({ schema, query, index: 2 }); values.push(...where.values); - const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; + const wherePattern = + where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; const qs = `SELECT count(*) FROM $1:name ${wherePattern}`; - return this._client.one(qs, values, a => +a.count) - .catch(error => { - if (error.code !== PostgresRelationDoesNotExistError) { - throw error; - } - return 0; - }); + return this._client.one(qs, values, a => +a.count).catch(error => { + if (error.code !== PostgresRelationDoesNotExistError) { + throw error; + } + return 0; + }); } - distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string) { + distinct( + className: string, + schema: SchemaType, + query: QueryType, + fieldName: string + ) { debug('distinct', className, query); let field = fieldName; let column = fieldName; @@ -1596,48 +1940,56 @@ export class PostgresStorageAdapter implements StorageAdapter { field = transformDotFieldToComponents(fieldName).join('->'); column = fieldName.split('.')[0]; } - const isArrayField = schema.fields - && schema.fields[fieldName] - && schema.fields[fieldName].type === 'Array'; - const isPointerField = schema.fields - && schema.fields[fieldName] - && schema.fields[fieldName].type === 'Pointer'; + const isArrayField = + schema.fields && + schema.fields[fieldName] && + schema.fields[fieldName].type === 'Array'; + const isPointerField = + schema.fields && + schema.fields[fieldName] && + schema.fields[fieldName].type === 'Pointer'; const values = [field, column, className]; const where = buildWhereClause({ schema, query, index: 4 }); values.push(...where.values); - const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; + const wherePattern = + where.pattern.length > 0 ? `WHERE ${where.pattern}` : ''; const transformer = isArrayField ? 'jsonb_array_elements' : 'ON'; let qs = `SELECT DISTINCT ${transformer}($1:name) $2:name FROM $3:name ${wherePattern}`; if (isNested) { qs = `SELECT DISTINCT ${transformer}($1:raw) $2:raw FROM $3:name ${wherePattern}`; } debug(qs, values); - return this._client.any(qs, values) - .catch((error) => { + return this._client + .any(qs, values) + .catch(error => { if (error.code === PostgresMissingColumnError) { return []; } throw error; }) - .then((results) => { + .then(results => { if (!isNested) { - results = results.filter((object) => object[field] !== null); + results = results.filter(object => object[field] !== null); return results.map(object => { if (!isPointerField) { return object[field]; } return { __type: 'Pointer', - className: schema.fields[fieldName].targetClass, - objectId: object[field] + className: schema.fields[fieldName].targetClass, + objectId: object[field], }; }); } const child = fieldName.split('.')[1]; return results.map(object => object[column][child]); }) - .then(results => results.map(object => this.postgresObjectToParseObject(className, object, schema))); + .then(results => + results.map(object => + this.postgresObjectToParseObject(className, object, schema) + ) + ); } aggregate(className: string, schema: any, pipeline: any) { @@ -1660,14 +2012,18 @@ export class PostgresStorageAdapter implements StorageAdapter { if (value === null || value === undefined) { continue; } - if (field === '_id' && (typeof value === 'string') && value !== '') { + if (field === '_id' && typeof value === 'string' && value !== '') { columns.push(`$${index}:name AS "objectId"`); groupPattern = `GROUP BY $${index}:name`; values.push(transformAggregateField(value)); index += 1; continue; } - if (field === '_id' && (typeof value === 'object') && Object.keys(value).length !== 0) { + if ( + field === '_id' && + typeof value === 'object' && + Object.keys(value).length !== 0 + ) { groupValues = value; const groupByFields = []; for (const alias in value) { @@ -1677,7 +2033,12 @@ export class PostgresStorageAdapter implements StorageAdapter { if (!groupByFields.includes(`"${source}"`)) { groupByFields.push(`"${source}"`); } - columns.push(`EXTRACT(${mongoAggregateToPostgres[operation]} FROM $${index}:name AT TIME ZONE 'UTC') AS $${index + 1}:name`); + columns.push( + `EXTRACT(${ + mongoAggregateToPostgres[operation] + } FROM $${index}:name AT TIME ZONE 'UTC') AS $${index + + 1}:name` + ); values.push(source, alias); index += 2; } @@ -1724,7 +2085,7 @@ export class PostgresStorageAdapter implements StorageAdapter { } for (const field in stage.$project) { const value = stage.$project[field]; - if ((value === 1 || value === true)) { + if (value === 1 || value === true) { columns.push(`$${index}:name`); values.push(field); index += 1; @@ -1737,7 +2098,7 @@ export class PostgresStorageAdapter implements StorageAdapter { if (stage.$match.$or) { const collapse = {}; - stage.$match.$or.forEach((element) => { + stage.$match.$or.forEach(element => { for (const key in element) { collapse[key] = element[key]; } @@ -1747,10 +2108,12 @@ export class PostgresStorageAdapter implements StorageAdapter { for (const field in stage.$match) { const value = stage.$match[field]; const matchPatterns = []; - Object.keys(ParseToPosgresComparator).forEach((cmp) => { + Object.keys(ParseToPosgresComparator).forEach(cmp => { if (value[cmp]) { const pgComparator = ParseToPosgresComparator[cmp]; - matchPatterns.push(`$${index}:name ${pgComparator} $${index + 1}`); + matchPatterns.push( + `$${index}:name ${pgComparator} $${index + 1}` + ); values.push(field, toPostgresValue(value[cmp])); index += 2; } @@ -1758,13 +2121,18 @@ export class PostgresStorageAdapter implements StorageAdapter { if (matchPatterns.length > 0) { patterns.push(`(${matchPatterns.join(' AND ')})`); } - if (schema.fields[field] && schema.fields[field].type && matchPatterns.length === 0) { + if ( + schema.fields[field] && + schema.fields[field].type && + matchPatterns.length === 0 + ) { patterns.push(`$${index}:name = $${index + 1}`); values.push(field, value); index += 2; } } - wherePattern = patterns.length > 0 ? `WHERE ${patterns.join(` ${orOrAnd} `)}` : ''; + wherePattern = + patterns.length > 0 ? `WHERE ${patterns.join(` ${orOrAnd} `)}` : ''; } if (stage.$limit) { limitPattern = `LIMIT $${index}`; @@ -1779,20 +2147,26 @@ export class PostgresStorageAdapter implements StorageAdapter { if (stage.$sort) { const sort = stage.$sort; const keys = Object.keys(sort); - const sorting = keys.map((key) => { - const transformer = sort[key] === 1 ? 'ASC' : 'DESC'; - const order = `$${index}:name ${transformer}`; - index += 1; - return order; - }).join(); + const sorting = keys + .map(key => { + const transformer = sort[key] === 1 ? 'ASC' : 'DESC'; + const order = `$${index}:name ${transformer}`; + index += 1; + return order; + }) + .join(); values.push(...keys); - sortPattern = sort !== undefined && sorting.length > 0 ? `ORDER BY ${sorting}` : ''; + sortPattern = + sort !== undefined && sorting.length > 0 ? `ORDER BY ${sorting}` : ''; } } const qs = `SELECT ${columns.join()} FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern} ${groupPattern}`; debug(qs, values); - return this._client.map(qs, values, a => this.postgresObjectToParseObject(className, a, schema)) + return this._client + .map(qs, values, a => + this.postgresObjectToParseObject(className, a, schema) + ) .then(results => { results.forEach(result => { if (!result.hasOwnProperty('objectId')) { @@ -1816,10 +2190,13 @@ export class PostgresStorageAdapter implements StorageAdapter { performInitialization({ VolatileClassesSchemas }: any) { // TODO: This method needs to be rewritten to make proper use of connections (@vitaly-t) debug('performInitialization'); - const promises = VolatileClassesSchemas.map((schema) => { + const promises = VolatileClassesSchemas.map(schema => { return this.createTable(schema.className, schema) - .catch((err) => { - if (err.code === PostgresDuplicateRelationError || err.code === Parse.Error.INVALID_CLASS_NAME) { + .catch(err => { + if ( + err.code === PostgresDuplicateRelationError || + err.code === Parse.Error.INVALID_CLASS_NAME + ) { return Promise.resolve(); } throw err; @@ -1836,7 +2213,7 @@ export class PostgresStorageAdapter implements StorageAdapter { t.none(sql.array.remove), t.none(sql.array.containsAll), t.none(sql.array.containsAllRegex), - t.none(sql.array.contains) + t.none(sql.array.contains), ]); }); }) @@ -1850,23 +2227,44 @@ export class PostgresStorageAdapter implements StorageAdapter { } createIndexes(className: string, indexes: any, conn: ?any): Promise { - return (conn || this._client).tx(t => t.batch(indexes.map(i => { - return t.none('CREATE INDEX $1:name ON $2:name ($3:name)', [i.name, className, i.key]); - }))); + return (conn || this._client).tx(t => + t.batch( + indexes.map(i => { + return t.none('CREATE INDEX $1:name ON $2:name ($3:name)', [ + i.name, + className, + i.key, + ]); + }) + ) + ); } - createIndexesIfNeeded(className: string, fieldName: string, type: any, conn: ?any): Promise { - return (conn || this._client).none('CREATE INDEX $1:name ON $2:name ($3:name)', [fieldName, className, type]); + createIndexesIfNeeded( + className: string, + fieldName: string, + type: any, + conn: ?any + ): Promise { + return (conn || this._client).none( + 'CREATE INDEX $1:name ON $2:name ($3:name)', + [fieldName, className, type] + ); } dropIndexes(className: string, indexes: any, conn: any): Promise { - const queries = indexes.map(i => ({query: 'DROP INDEX $1:name', values: i})); - return (conn || this._client).tx(t => t.none(this._pgp.helpers.concat(queries))); + const queries = indexes.map(i => ({ + query: 'DROP INDEX $1:name', + values: i, + })); + return (conn || this._client).tx(t => + t.none(this._pgp.helpers.concat(queries)) + ); } getIndexes(className: string) { const qs = 'SELECT * FROM pg_indexes WHERE tablename = ${className}'; - return this._client.any(qs, {className}); + return this._client.any(qs, { className }); } updateSchemaWithIndexes(): Promise { @@ -1881,16 +2279,17 @@ function convertPolygonToSQL(polygon) { `Polygon must have at least 3 values` ); } - if (polygon[0][0] !== polygon[polygon.length - 1][0] || - polygon[0][1] !== polygon[polygon.length - 1][1]) { + if ( + polygon[0][0] !== polygon[polygon.length - 1][0] || + polygon[0][1] !== polygon[polygon.length - 1][1] + ) { polygon.push(polygon[0]); } const unique = polygon.filter((item, index, ar) => { let foundIndex = -1; for (let i = 0; i < ar.length; i += 1) { const pt = ar[i]; - if (pt[0] === item[0] && - pt[1] === item[1]) { + if (pt[0] === item[0] && pt[1] === item[1]) { foundIndex = i; break; } @@ -1903,34 +2302,38 @@ function convertPolygonToSQL(polygon) { 'GeoJSON: Loop must have at least 3 different vertices' ); } - const points = polygon.map((point) => { - Parse.GeoPoint._validate(parseFloat(point[1]), parseFloat(point[0])); - return `(${point[1]}, ${point[0]})`; - }).join(', '); + const points = polygon + .map(point => { + Parse.GeoPoint._validate(parseFloat(point[1]), parseFloat(point[0])); + return `(${point[1]}, ${point[0]})`; + }) + .join(', '); return `(${points})`; } function removeWhiteSpace(regex) { - if (!regex.endsWith('\n')){ + if (!regex.endsWith('\n')) { regex += '\n'; } // remove non escaped comments - return regex.replace(/([^\\])#.*\n/gmi, '$1') - // remove lines starting with a comment - .replace(/^#.*\n/gmi, '') - // remove non escaped whitespace - .replace(/([^\\])\s+/gmi, '$1') - // remove whitespace at the beginning of a line - .replace(/^\s+/, '') - .trim(); + return ( + regex + .replace(/([^\\])#.*\n/gim, '$1') + // remove lines starting with a comment + .replace(/^#.*\n/gim, '') + // remove non escaped whitespace + .replace(/([^\\])\s+/gim, '$1') + // remove whitespace at the beginning of a line + .replace(/^\s+/, '') + .trim() + ); } function processRegexPattern(s) { - if (s && s.startsWith('^')){ + if (s && s.startsWith('^')) { // regex for startsWith return '^' + literalizeRegexPart(s.slice(1)); - } else if (s && s.endsWith('$')) { // regex for endsWith return literalizeRegexPart(s.slice(0, s.length - 1)) + '$'; @@ -1969,26 +2372,29 @@ function isAllValuesRegexOrNone(values) { } function isAnyValueRegexStartsWith(values) { - return values.some(function (value) { + return values.some(function(value) { return isStartsWithRegex(value.$regex); }); } function createLiteralRegex(remaining) { - return remaining.split('').map(c => { - if (c.match(/[0-9a-zA-Z]/) !== null) { - // don't escape alphanumeric characters - return c; - } - // escape everything else (single quotes with single quotes, everything else with a backslash) - return c === `'` ? `''` : `\\${c}`; - }).join(''); + return remaining + .split('') + .map(c => { + if (c.match(/[0-9a-zA-Z]/) !== null) { + // don't escape alphanumeric characters + return c; + } + // escape everything else (single quotes with single quotes, everything else with a backslash) + return c === `'` ? `''` : `\\${c}`; + }) + .join(''); } function literalizeRegexPart(s: string) { - const matcher1 = /\\Q((?!\\E).*)\\E$/ + const matcher1 = /\\Q((?!\\E).*)\\E$/; const result1: any = s.match(matcher1); - if(result1 && result1.length > 1 && result1.index > -1) { + if (result1 && result1.length > 1 && result1.index > -1) { // process regex that has a beginning and an end specified for the literal text const prefix = s.substr(0, result1.index); const remaining = result1[1]; @@ -1997,9 +2403,9 @@ function literalizeRegexPart(s: string) { } // process regex that has a beginning specified for the literal text - const matcher2 = /\\Q((?!\\E).*)$/ + const matcher2 = /\\Q((?!\\E).*)$/; const result2: any = s.match(matcher2); - if(result2 && result2.length > 1 && result2.index > -1){ + if (result2 && result2.length > 1 && result2.index > -1) { const prefix = s.substr(0, result2.index); const remaining = result2[1]; @@ -2007,23 +2413,21 @@ function literalizeRegexPart(s: string) { } // remove all instances of \Q and \E from the remaining text & escape single quotes - return ( - s.replace(/([^\\])(\\E)/, '$1') - .replace(/([^\\])(\\Q)/, '$1') - .replace(/^\\E/, '') - .replace(/^\\Q/, '') - .replace(/([^'])'/, `$1''`) - .replace(/^'([^'])/, `''$1`) - ); + return s + .replace(/([^\\])(\\E)/, '$1') + .replace(/([^\\])(\\Q)/, '$1') + .replace(/^\\E/, '') + .replace(/^\\Q/, '') + .replace(/([^'])'/, `$1''`) + .replace(/^'([^'])/, `''$1`); } var GeoPointCoder = { isValidJSON(value) { - return (typeof value === 'object' && - value !== null && - value.__type === 'GeoPoint' + return ( + typeof value === 'object' && value !== null && value.__type === 'GeoPoint' ); - } + }, }; export default PostgresStorageAdapter; diff --git a/src/Adapters/Storage/Postgres/sql/index.js b/src/Adapters/Storage/Postgres/sql/index.js index 6bcd560d13..ad151f2170 100644 --- a/src/Adapters/Storage/Postgres/sql/index.js +++ b/src/Adapters/Storage/Postgres/sql/index.js @@ -10,20 +10,19 @@ module.exports = { contains: sql('array/contains.sql'), containsAll: sql('array/contains-all.sql'), containsAllRegex: sql('array/contains-all-regex.sql'), - remove: sql('array/remove.sql') + remove: sql('array/remove.sql'), }, misc: { - jsonObjectSetKeys: sql('misc/json-object-set-keys.sql') - } + jsonObjectSetKeys: sql('misc/json-object-set-keys.sql'), + }, }; /////////////////////////////////////////////// // Helper for linking to external query files; function sql(file) { - var fullPath = path.join(__dirname, file); // generating full path; - var qf = new QueryFile(fullPath, {minify: true}); + var qf = new QueryFile(fullPath, { minify: true }); if (qf.error) { throw qf.error; diff --git a/src/Adapters/Storage/StorageAdapter.js b/src/Adapters/Storage/StorageAdapter.js index ed2c2e0701..d901452dcd 100644 --- a/src/Adapters/Storage/StorageAdapter.js +++ b/src/Adapters/Storage/StorageAdapter.js @@ -7,7 +7,7 @@ export type QueryOptions = { skip?: number, limit?: number, acl?: string[], - sort?: {[string]: number}, + sort?: { [string]: number }, count?: boolean | number, keys?: string[], op?: string, @@ -18,8 +18,8 @@ export type QueryOptions = { export type UpdateQueryOptions = { many?: boolean, - upsert?: boolean -} + upsert?: boolean, +}; export type FullQueryOptions = QueryOptions & UpdateQueryOptions; @@ -29,27 +29,88 @@ export interface StorageAdapter { classExists(className: string): Promise; setClassLevelPermissions(className: string, clps: any): Promise; createClass(className: string, schema: SchemaType): Promise; - addFieldIfNotExists(className: string, fieldName: string, type: any): Promise; + addFieldIfNotExists( + className: string, + fieldName: string, + type: any + ): Promise; deleteClass(className: string): Promise; deleteAllClasses(fast: boolean): Promise; - deleteFields(className: string, schema: SchemaType, fieldNames: Array): Promise; + deleteFields( + className: string, + schema: SchemaType, + fieldNames: Array + ): Promise; getAllClasses(): Promise; getClass(className: string): Promise; - createObject(className: string, schema: SchemaType, object: any): Promise; - deleteObjectsByQuery(className: string, schema: SchemaType, query: QueryType): Promise; - updateObjectsByQuery(className: string, schema: SchemaType, query: QueryType, update: any): Promise<[any]>; - findOneAndUpdate(className: string, schema: SchemaType, query: QueryType, update: any): Promise; - upsertOneObject(className: string, schema: SchemaType, query: QueryType, update: any): Promise; - find(className: string, schema: SchemaType, query: QueryType, options: QueryOptions): Promise<[any]>; - ensureUniqueness(className: string, schema: SchemaType, fieldNames: Array): Promise; - count(className: string, schema: SchemaType, query: QueryType, readPreference: ?string): Promise; - distinct(className: string, schema: SchemaType, query: QueryType, fieldName: string): Promise; - aggregate(className: string, schema: any, pipeline: any, readPreference: ?string): Promise; + createObject( + className: string, + schema: SchemaType, + object: any + ): Promise; + deleteObjectsByQuery( + className: string, + schema: SchemaType, + query: QueryType + ): Promise; + updateObjectsByQuery( + className: string, + schema: SchemaType, + query: QueryType, + update: any + ): Promise<[any]>; + findOneAndUpdate( + className: string, + schema: SchemaType, + query: QueryType, + update: any + ): Promise; + upsertOneObject( + className: string, + schema: SchemaType, + query: QueryType, + update: any + ): Promise; + find( + className: string, + schema: SchemaType, + query: QueryType, + options: QueryOptions + ): Promise<[any]>; + ensureUniqueness( + className: string, + schema: SchemaType, + fieldNames: Array + ): Promise; + count( + className: string, + schema: SchemaType, + query: QueryType, + readPreference: ?string + ): Promise; + distinct( + className: string, + schema: SchemaType, + query: QueryType, + fieldName: string + ): Promise; + aggregate( + className: string, + schema: any, + pipeline: any, + readPreference: ?string + ): Promise; performInitialization(options: ?any): Promise; // Indexing createIndexes(className: string, indexes: any, conn: ?any): Promise; getIndexes(className: string, connection: ?any): Promise; updateSchemaWithIndexes(): Promise; - setIndexesWithSchemaFormat(className: string, submittedIndexes: any, existingIndexes: any, fields: any, conn: ?any): Promise; + setIndexesWithSchemaFormat( + className: string, + submittedIndexes: any, + existingIndexes: any, + fields: any, + conn: ?any + ): Promise; } diff --git a/src/Auth.js b/src/Auth.js index c4ec748b7a..64c69cfd9c 100644 --- a/src/Auth.js +++ b/src/Auth.js @@ -5,7 +5,14 @@ const Parse = require('parse/node'); // 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. -function Auth({ config, cacheController = undefined, isMaster = false, isReadOnly = false, user, installationId }) { +function Auth({ + config, + cacheController = undefined, + isMaster = false, + isReadOnly = false, + user, + installationId, +}) { this.config = config; this.cacheController = cacheController || (config && config.cacheController); this.installationId = installationId; @@ -47,15 +54,27 @@ function nobody(config) { return new Auth({ config, isMaster: false }); } - // Returns a promise that resolves to an Auth object -const getAuthForSessionToken = async function({ config, cacheController, sessionToken, installationId }) { +const getAuthForSessionToken = async function({ + config, + cacheController, + sessionToken, + installationId, +}) { cacheController = cacheController || (config && config.cacheController); if (cacheController) { const userJSON = await cacheController.user.get(sessionToken); if (userJSON) { const cachedUser = Parse.Object.fromJSON(userJSON); - return Promise.resolve(new Auth({config, cacheController, isMaster: false, installationId, user: cachedUser})); + return Promise.resolve( + new Auth({ + config, + cacheController, + isMaster: false, + installationId, + user: cachedUser, + }) + ); } } @@ -63,27 +82,40 @@ const getAuthForSessionToken = async function({ config, cacheController, session if (config) { const restOptions = { limit: 1, - include: 'user' + include: 'user', }; - const query = new RestQuery(config, master(config), '_Session', { sessionToken }, restOptions); + const query = new RestQuery( + config, + master(config), + '_Session', + { sessionToken }, + restOptions + ); results = (await query.execute()).results; } else { results = (await new Parse.Query(Parse.Session) .limit(1) .include('user') .equalTo('sessionToken', sessionToken) - .find({ useMasterKey: true })).map((obj) => obj.toJSON()) + .find({ useMasterKey: true })).map(obj => obj.toJSON()); } if (results.length !== 1 || !results[0]['user']) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); } const now = new Date(), - expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined; + 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.'); + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Session token is expired.' + ); } const obj = results[0]['user']; delete obj.password; @@ -93,25 +125,49 @@ const getAuthForSessionToken = async function({ config, cacheController, session cacheController.user.put(sessionToken, obj); } const userObject = Parse.Object.fromJSON(obj); - return new Auth({ config, cacheController, isMaster: false, installationId, user: userObject }); + return new Auth({ + config, + cacheController, + isMaster: false, + installationId, + user: userObject, + }); }; -var getAuthForLegacySessionToken = function({ config, sessionToken, installationId }) { +var getAuthForLegacySessionToken = function({ + config, + sessionToken, + installationId, +}) { var restOptions = { - limit: 1 + limit: 1, }; - var query = new RestQuery(config, master(config), '_User', { sessionToken }, restOptions); - return query.execute().then((response) => { + var query = new RestQuery( + config, + master(config), + '_User', + { sessionToken }, + restOptions + ); + return query.execute().then(response => { var results = response.results; if (results.length !== 1) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid legacy session token'); + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'invalid legacy session token' + ); } const obj = results[0]; obj.className = '_User'; const userObject = Parse.Object.fromJSON(obj); - return new Auth({ config, isMaster: false, installationId, user: userObject }); + return new Auth({ + config, + isMaster: false, + installationId, + user: userObject, + }); }); -} +}; // Returns a promise that resolves to an array of role names Auth.prototype.getUserRoles = function() { @@ -131,21 +187,27 @@ Auth.prototype.getUserRoles = function() { Auth.prototype.getRolesForUser = function() { if (this.config) { const restWhere = { - 'users': { + users: { __type: 'Pointer', className: '_User', - objectId: this.user.id - } + objectId: this.user.id, + }, }; - const query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); + const query = new RestQuery( + this.config, + master(this.config), + '_Role', + restWhere, + {} + ); return query.execute().then(({ results }) => results); } return new Parse.Query(Parse.Role) .equalTo('users', this.user) .find({ useMasterKey: true }) - .then((results) => results.map((obj) => obj.toJSON())); -} + .then(results => results.map(obj => obj.toJSON())); +}; // Iterates through the role tree and compiles a user's roles Auth.prototype._loadRoles = async function() { @@ -169,15 +231,21 @@ Auth.prototype._loadRoles = async function() { return this.userRoles; } - const rolesMap = results.reduce((m, r) => { - m.names.push(r.name); - m.ids.push(r.objectId); - return m; - }, {ids: [], names: []}); + const rolesMap = results.reduce( + (m, r) => { + m.names.push(r.name); + m.ids.push(r.objectId); + return m; + }, + { ids: [], names: [] } + ); // run the recursive finding - const roleNames = await this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names); - this.userRoles = roleNames.map((r) => { + const roleNames = await this._getAllRolesNamesForRoleIds( + rolesMap.ids, + rolesMap.names + ); + this.userRoles = roleNames.map(r => { return 'role:' + r; }); this.fetchedRoles = true; @@ -192,38 +260,45 @@ Auth.prototype.cacheRoles = function() { } this.cacheController.role.put(this.user.id, Array(...this.userRoles)); return true; -} +}; Auth.prototype.getRolesByIds = function(ins) { - const roles = ins.map((id) => { + const roles = ins.map(id => { return { __type: 'Pointer', className: '_Role', - objectId: id - } + objectId: id, + }; }); - const restWhere = { 'roles': { '$in': roles }}; + const restWhere = { roles: { $in: roles } }; // Build an OR query across all parentRoles if (!this.config) { return new Parse.Query(Parse.Role) - .containedIn('roles', ins.map((id) => { - const role = new Parse.Object(Parse.Role); - role.id = id; - return role; - })) + .containedIn( + 'roles', + ins.map(id => { + const role = new Parse.Object(Parse.Role); + role.id = id; + return role; + }) + ) .find({ useMasterKey: true }) - .then((results) => results.map((obj) => obj.toJSON())); + .then(results => results.map(obj => obj.toJSON())); } return new RestQuery(this.config, master(this.config), '_Role', restWhere, {}) .execute() .then(({ results }) => results); -} +}; // Given a list of roleIds, find all the parent roles, returns a promise with all names -Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queriedRoles = {}) { - const ins = roleIDs.filter((roleID) => { +Auth.prototype._getAllRolesNamesForRoleIds = function( + roleIDs, + names = [], + queriedRoles = {} +) { + const ins = roleIDs.filter(roleID => { const wasQueried = queriedRoles[roleID] !== true; queriedRoles[roleID] = true; return wasQueried; @@ -234,32 +309,39 @@ Auth.prototype._getAllRolesNamesForRoleIds = function(roleIDs, names = [], queri return Promise.resolve([...new Set(names)]); } - return this.getRolesByIds(ins).then((results) => { - // Nothing found - if (!results.length) { - return Promise.resolve(names); - } - // Map the results with all Ids and names - const 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)]) - }) -} + return this.getRolesByIds(ins) + .then(results => { + // Nothing found + if (!results.length) { + return Promise.resolve(names); + } + // Map the results with all Ids and names + const 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)]); + }); +}; -const createSession = function(config, { - userId, - createdWith, - installationId, - additionalSessionData, -}) { +const createSession = function( + config, + { userId, createdWith, installationId, additionalSessionData } +) { const token = 'r:' + cryptoUtils.newToken(); const expiresAt = config.generateSessionExpiresAt(); const sessionData = { @@ -267,15 +349,15 @@ const createSession = function(config, { user: { __type: 'Pointer', className: '_User', - objectId: userId + objectId: userId, }, createdWith, restricted: false, - expiresAt: Parse._encode(expiresAt) + expiresAt: Parse._encode(expiresAt), }; if (installationId) { - sessionData.installationId = installationId + sessionData.installationId = installationId; } Object.assign(sessionData, additionalSessionData); @@ -284,9 +366,16 @@ const createSession = function(config, { return { sessionData, - createSession: () => new RestWrite(config, master(config), '_Session', null, sessionData).execute() - } -} + createSession: () => + new RestWrite( + config, + master(config), + '_Session', + null, + sessionData + ).execute(), + }; +}; module.exports = { Auth, diff --git a/src/ClientSDK.js b/src/ClientSDK.js index 9a23b0c6ff..e4a716ab86 100644 --- a/src/ClientSDK.js +++ b/src/ClientSDK.js @@ -12,12 +12,12 @@ function compatible(compatibleSDK) { const clientVersion = clientSDK.version; const compatiblityVersion = compatibleSDK[clientSDK.sdk]; return semver.satisfies(clientVersion, compatiblityVersion); - } + }; } function supportsForwardDelete(clientSDK) { return compatible({ - js: '>=1.9.0' + js: '>=1.9.0', })(clientSDK); } @@ -27,8 +27,8 @@ function fromString(version) { if (match && match.length === 3) { return { sdk: match[1], - version: match[2] - } + version: match[2], + }; } return undefined; } @@ -36,5 +36,5 @@ function fromString(version) { module.exports = { compatible, supportsForwardDelete, - fromString -} + fromString, +}; diff --git a/src/Config.js b/src/Config.js index d9eec85da7..7f685dd5b0 100644 --- a/src/Config.js +++ b/src/Config.js @@ -11,7 +11,7 @@ function removeTrailingSlash(str) { if (!str) { return str; } - if (str.endsWith("/")) { + if (str.endsWith('/')) { str = str.substr(0, str.length - 1); } return str; @@ -25,19 +25,28 @@ export class Config { } const config = new Config(); config.applicationId = applicationId; - Object.keys(cacheInfo).forEach((key) => { + Object.keys(cacheInfo).forEach(key => { if (key == 'databaseController') { - const schemaCache = new SchemaCache(cacheInfo.cacheController, + const schemaCache = new SchemaCache( + cacheInfo.cacheController, cacheInfo.schemaCacheTTL, - cacheInfo.enableSingleSchemaCache); - config.database = new DatabaseController(cacheInfo.databaseController.adapter, schemaCache); + cacheInfo.enableSingleSchemaCache + ); + config.database = new DatabaseController( + cacheInfo.databaseController.adapter, + schemaCache + ); } else { config[key] = cacheInfo[key]; } }); config.mount = removeTrailingSlash(mount); - config.generateSessionExpiresAt = config.generateSessionExpiresAt.bind(config); - config.generateEmailVerifyTokenExpiresAt = config.generateEmailVerifyTokenExpiresAt.bind(config); + config.generateSessionExpiresAt = config.generateSessionExpiresAt.bind( + config + ); + config.generateEmailVerifyTokenExpiresAt = config.generateEmailVerifyTokenExpiresAt.bind( + config + ); return config; } @@ -64,14 +73,18 @@ export class Config { masterKey, readOnlyMasterKey, }) { - if (masterKey === readOnlyMasterKey) { throw new Error('masterKey and readOnlyMasterKey should be different'); } const emailAdapter = userController.adapter; if (verifyUserEmails) { - this.validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}); + this.validateEmailConfiguration({ + emailAdapter, + appName, + publicServerURL, + emailVerifyTokenValidityDuration, + }); } this.validateAccountLockoutPolicy(accountLockout); @@ -83,8 +96,11 @@ export class Config { } if (publicServerURL) { - if (!publicServerURL.startsWith("http://") && !publicServerURL.startsWith("https://")) { - throw "publicServerURL should be a valid HTTPS URL starting with https://" + if ( + !publicServerURL.startsWith('http://') && + !publicServerURL.startsWith('https://') + ) { + throw 'publicServerURL should be a valid HTTPS URL starting with https://'; } } @@ -97,11 +113,19 @@ export class Config { static validateAccountLockoutPolicy(accountLockout) { if (accountLockout) { - if (typeof accountLockout.duration !== 'number' || accountLockout.duration <= 0 || accountLockout.duration > 99999) { + if ( + typeof accountLockout.duration !== 'number' || + accountLockout.duration <= 0 || + accountLockout.duration > 99999 + ) { throw 'Account lockout duration should be greater than 0 and less than 100000'; } - if (!Number.isInteger(accountLockout.threshold) || accountLockout.threshold < 1 || accountLockout.threshold > 999) { + if ( + !Number.isInteger(accountLockout.threshold) || + accountLockout.threshold < 1 || + accountLockout.threshold > 999 + ) { throw 'Account lockout threshold should be an integer greater than 0 and less than 1000'; } } @@ -109,33 +133,52 @@ export class Config { static validatePasswordPolicy(passwordPolicy) { if (passwordPolicy) { - if (passwordPolicy.maxPasswordAge !== undefined && (typeof passwordPolicy.maxPasswordAge !== 'number' || passwordPolicy.maxPasswordAge < 0)) { + if ( + passwordPolicy.maxPasswordAge !== undefined && + (typeof passwordPolicy.maxPasswordAge !== 'number' || + passwordPolicy.maxPasswordAge < 0) + ) { throw 'passwordPolicy.maxPasswordAge must be a positive number'; } - if (passwordPolicy.resetTokenValidityDuration !== undefined && (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || passwordPolicy.resetTokenValidityDuration <= 0)) { + if ( + passwordPolicy.resetTokenValidityDuration !== undefined && + (typeof passwordPolicy.resetTokenValidityDuration !== 'number' || + passwordPolicy.resetTokenValidityDuration <= 0) + ) { throw 'passwordPolicy.resetTokenValidityDuration must be a positive number'; } - if(passwordPolicy.validatorPattern){ - if(typeof(passwordPolicy.validatorPattern) === 'string') { - passwordPolicy.validatorPattern = new RegExp(passwordPolicy.validatorPattern); - } - else if(!(passwordPolicy.validatorPattern instanceof RegExp)){ + if (passwordPolicy.validatorPattern) { + if (typeof passwordPolicy.validatorPattern === 'string') { + passwordPolicy.validatorPattern = new RegExp( + passwordPolicy.validatorPattern + ); + } else if (!(passwordPolicy.validatorPattern instanceof RegExp)) { throw 'passwordPolicy.validatorPattern must be a regex string or RegExp object.'; } } - - if(passwordPolicy.validatorCallback && typeof passwordPolicy.validatorCallback !== 'function') { + if ( + passwordPolicy.validatorCallback && + typeof passwordPolicy.validatorCallback !== 'function' + ) { throw 'passwordPolicy.validatorCallback must be a function.'; } - if(passwordPolicy.doNotAllowUsername && typeof passwordPolicy.doNotAllowUsername !== 'boolean') { + if ( + passwordPolicy.doNotAllowUsername && + typeof passwordPolicy.doNotAllowUsername !== 'boolean' + ) { throw 'passwordPolicy.doNotAllowUsername must be a boolean value.'; } - if (passwordPolicy.maxPasswordHistory && (!Number.isInteger(passwordPolicy.maxPasswordHistory) || passwordPolicy.maxPasswordHistory <= 0 || passwordPolicy.maxPasswordHistory > 20)) { + if ( + passwordPolicy.maxPasswordHistory && + (!Number.isInteger(passwordPolicy.maxPasswordHistory) || + passwordPolicy.maxPasswordHistory <= 0 || + passwordPolicy.maxPasswordHistory > 20) + ) { throw 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20'; } } @@ -144,13 +187,18 @@ export class Config { // if the passwordPolicy.validatorPattern is configured then setup a callback to process the pattern static setupPasswordValidator(passwordPolicy) { if (passwordPolicy && passwordPolicy.validatorPattern) { - passwordPolicy.patternValidator = (value) => { + passwordPolicy.patternValidator = value => { return passwordPolicy.validatorPattern.test(value); - } + }; } } - static validateEmailConfiguration({emailAdapter, appName, publicServerURL, emailVerifyTokenValidityDuration}) { + static validateEmailConfiguration({ + emailAdapter, + appName, + publicServerURL, + emailVerifyTokenValidityDuration, + }) { if (!emailAdapter) { throw 'An emailAdapter is required for e-mail verification and password resets.'; } @@ -164,14 +212,14 @@ export class Config { if (isNaN(emailVerifyTokenValidityDuration)) { throw 'Email verify token validity duration must be a valid number.'; } else if (emailVerifyTokenValidityDuration <= 0) { - throw 'Email verify token validity duration must be a value greater than 0.' + throw 'Email verify token validity duration must be a value greater than 0.'; } } } static validateMasterKeyIps(masterKeyIps) { for (const ip of masterKeyIps) { - if(!net.isIP(ip)){ + if (!net.isIP(ip)) { throw `Invalid ip in masterKeyIps: ${ip}`; } } @@ -193,16 +241,15 @@ export class Config { 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.' + } else if (sessionLength <= 0) { + throw 'Session length must be a value greater than 0.'; } } } static validateMaxLimit(maxLimit) { if (maxLimit <= 0) { - throw 'Max limit must be a value greater than 0.' + throw 'Max limit must be a value greater than 0.'; } } @@ -211,15 +258,22 @@ export class Config { return undefined; } var now = new Date(); - return new Date(now.getTime() + (this.emailVerifyTokenValidityDuration * 1000)); + return new Date( + now.getTime() + this.emailVerifyTokenValidityDuration * 1000 + ); } generatePasswordResetTokenExpiresAt() { - if (!this.passwordPolicy || !this.passwordPolicy.resetTokenValidityDuration) { + if ( + !this.passwordPolicy || + !this.passwordPolicy.resetTokenValidityDuration + ) { return undefined; } const now = new Date(); - return new Date(now.getTime() + (this.passwordPolicy.resetTokenValidityDuration * 1000)); + return new Date( + now.getTime() + this.passwordPolicy.resetTokenValidityDuration * 1000 + ); } generateSessionExpiresAt() { @@ -227,39 +281,62 @@ export class Config { return undefined; } var now = new Date(); - return new Date(now.getTime() + (this.sessionLength * 1000)); + return new Date(now.getTime() + this.sessionLength * 1000); } get invalidLinkURL() { - return this.customPages.invalidLink || `${this.publicServerURL}/apps/invalid_link.html`; + return ( + this.customPages.invalidLink || + `${this.publicServerURL}/apps/invalid_link.html` + ); } get invalidVerificationLinkURL() { - return this.customPages.invalidVerificationLink || `${this.publicServerURL}/apps/invalid_verification_link.html`; + return ( + this.customPages.invalidVerificationLink || + `${this.publicServerURL}/apps/invalid_verification_link.html` + ); } get linkSendSuccessURL() { - return this.customPages.linkSendSuccess || `${this.publicServerURL}/apps/link_send_success.html` + return ( + this.customPages.linkSendSuccess || + `${this.publicServerURL}/apps/link_send_success.html` + ); } get linkSendFailURL() { - return this.customPages.linkSendFail || `${this.publicServerURL}/apps/link_send_fail.html` + return ( + this.customPages.linkSendFail || + `${this.publicServerURL}/apps/link_send_fail.html` + ); } get verifyEmailSuccessURL() { - return this.customPages.verifyEmailSuccess || `${this.publicServerURL}/apps/verify_email_success.html`; + return ( + this.customPages.verifyEmailSuccess || + `${this.publicServerURL}/apps/verify_email_success.html` + ); } get choosePasswordURL() { - return this.customPages.choosePassword || `${this.publicServerURL}/apps/choose_password`; + return ( + this.customPages.choosePassword || + `${this.publicServerURL}/apps/choose_password` + ); } get requestResetPasswordURL() { - return `${this.publicServerURL}/apps/${this.applicationId}/request_password_reset`; + return `${this.publicServerURL}/apps/${ + this.applicationId + }/request_password_reset`; } get passwordResetSuccessURL() { - return this.customPages.passwordResetSuccess || `${this.publicServerURL}/apps/password_reset_success.html`; + return ( + this.customPages.passwordResetSuccess || + `${this.publicServerURL}/apps/password_reset_success.html` + ); } get parseFrameURL() { diff --git a/src/Controllers/AdaptableController.js b/src/Controllers/AdaptableController.js index 2638db65ca..da74c63bf1 100644 --- a/src/Controllers/AdaptableController.js +++ b/src/Controllers/AdaptableController.js @@ -13,7 +13,6 @@ var _adapter = Symbol(); import Config from '../Config'; export class AdaptableController { - constructor(adapter, appId, options) { this.options = options; this.appId = appId; @@ -34,7 +33,7 @@ export class AdaptableController { } expectedAdapterType() { - throw new Error("Subclasses should implement expectedAdapterType()"); + throw new Error('Subclasses should implement expectedAdapterType()'); } validateAdapter(adapter) { @@ -43,7 +42,7 @@ export class AdaptableController { static validateAdapter(adapter, self, ExpectedType) { if (!adapter) { - throw new Error(this.constructor.name + " requires an adapter"); + throw new Error(this.constructor.name + ' requires an adapter'); } const Type = ExpectedType || self.expectedAdapterType(); @@ -53,20 +52,27 @@ export class AdaptableController { } // Makes sure the prototype matches - const mismatches = Object.getOwnPropertyNames(Type.prototype).reduce((obj, key) => { - const adapterType = typeof adapter[key]; - const expectedType = typeof Type.prototype[key]; - if (adapterType !== expectedType) { - obj[key] = { - expected: expectedType, - actual: adapterType + const mismatches = Object.getOwnPropertyNames(Type.prototype).reduce( + (obj, key) => { + const adapterType = typeof adapter[key]; + const expectedType = typeof Type.prototype[key]; + if (adapterType !== expectedType) { + obj[key] = { + expected: expectedType, + actual: adapterType, + }; } - } - return obj; - }, {}); + return obj; + }, + {} + ); if (Object.keys(mismatches).length > 0) { - throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches); + throw new Error( + "Adapter prototype don't match expected prototype", + adapter, + mismatches + ); } } } diff --git a/src/Controllers/AnalyticsController.js b/src/Controllers/AnalyticsController.js index 74b43932d7..89aa48eda8 100644 --- a/src/Controllers/AnalyticsController.js +++ b/src/Controllers/AnalyticsController.js @@ -3,23 +3,29 @@ import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter'; export class AnalyticsController extends AdaptableController { appOpened(req) { - return Promise.resolve().then(() => { - return this.adapter.appOpened(req.body, req); - }).then((response) => { - return { response: response || {} }; - }).catch(() => { - return { response: {} }; - }); + return Promise.resolve() + .then(() => { + return this.adapter.appOpened(req.body, req); + }) + .then(response => { + return { response: response || {} }; + }) + .catch(() => { + return { response: {} }; + }); } trackEvent(req) { - return Promise.resolve().then(() => { - return this.adapter.trackEvent(req.params.eventName, req.body, req); - }).then((response) => { - return { response: response || {} }; - }).catch(() => { - return { response: {} }; - }); + return Promise.resolve() + .then(() => { + return this.adapter.trackEvent(req.params.eventName, req.body, req); + }) + .then(response => { + return { response: response || {} }; + }) + .catch(() => { + return { response: {} }; + }); } expectedAdapterType() { diff --git a/src/Controllers/CacheController.js b/src/Controllers/CacheController.js index 78e102e470..f387765f60 100644 --- a/src/Controllers/CacheController.js +++ b/src/Controllers/CacheController.js @@ -1,5 +1,5 @@ import AdaptableController from './AdaptableController'; -import CacheAdapter from '../Adapters/Cache/CacheAdapter'; +import CacheAdapter from '../Adapters/Cache/CacheAdapter'; const KEY_SEPARATOR_CHAR = ':'; @@ -39,9 +39,7 @@ export class SubCache { } } - export class CacheController extends AdaptableController { - constructor(adapter, appId, options = {}) { super(adapter, appId, options); diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 372be94f81..fb6d6b9525 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -3,30 +3,32 @@ // Parse database. // @flow-disable-next -import { Parse } from 'parse/node'; +import { Parse } from 'parse/node'; // @flow-disable-next -import _ from 'lodash'; +import _ from 'lodash'; // @flow-disable-next -import intersect from 'intersect'; +import intersect from 'intersect'; // @flow-disable-next -import deepcopy from 'deepcopy'; -import logger from '../logger'; -import * as SchemaController from './SchemaController'; -import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; -import type { QueryOptions, - FullQueryOptions } from '../Adapters/Storage/StorageAdapter'; +import deepcopy from 'deepcopy'; +import logger from '../logger'; +import * as SchemaController from './SchemaController'; +import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; +import type { + QueryOptions, + FullQueryOptions, +} from '../Adapters/Storage/StorageAdapter'; function addWriteACL(query, acl) { const newQuery = _.cloneDeep(query); //Can't be any existing '_wperm' query, we don't allow client queries on that, no need to $and - newQuery._wperm = { "$in" : [null, ...acl]}; + newQuery._wperm = { $in: [null, ...acl] }; return newQuery; } function addReadACL(query, acl) { const newQuery = _.cloneDeep(query); //Can't be any existing '_rperm' query, we don't allow client queries on that, no need to $and - newQuery._rperm = {"$in": [null, "*", ...acl]}; + newQuery._rperm = { $in: [null, '*', ...acl] }; return newQuery; } @@ -48,13 +50,24 @@ const transformObjectACL = ({ ACL, ...result }) => { } } return result; -} +}; -const specialQuerykeys = ['$and', '$or', '$nor', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count']; +const specialQuerykeys = [ + '$and', + '$or', + '$nor', + '_rperm', + '_wperm', + '_perishable_token', + '_email_verify_token', + '_email_verify_token_expires_at', + '_account_lockout_expires_at', + '_failed_login_count', +]; const isSpecialQueryKey = key => { return specialQuerykeys.indexOf(key) >= 0; -} +}; const validateQuery = (query: any): void => { if (query.ACL) { @@ -85,10 +98,10 @@ const validateQuery = (query: any): void => { * https://github.com/parse-community/parse-server/issues/3767 */ Object.keys(query).forEach(key => { - const noCollisions = !query.$or.some(subq => subq.hasOwnProperty(key)) - let hasNears = false + const noCollisions = !query.$or.some(subq => subq.hasOwnProperty(key)); + let hasNears = false; if (query[key] != null && typeof query[key] == 'object') { - hasNears = ('$near' in query[key] || '$nearSphere' in query[key]) + hasNears = '$near' in query[key] || '$nearSphere' in query[key]; } if (key != '$or' && noCollisions && !hasNears) { query.$or.forEach(subquery => { @@ -99,7 +112,10 @@ const validateQuery = (query: any): void => { }); query.$or.forEach(validateQuery); } else { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Bad $or format - use an array value.' + ); } } @@ -107,7 +123,10 @@ const validateQuery = (query: any): void => { if (query.$and instanceof Array) { query.$and.forEach(validateQuery); } else { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Bad $and format - use an array value.' + ); } } @@ -115,7 +134,10 @@ const validateQuery = (query: any): void => { if (query.$nor instanceof Array && query.$nor.length > 0) { query.$nor.forEach(validateQuery); } else { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $nor format - use an array of at least 1 value.'); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Bad $nor format - use an array of at least 1 value.' + ); } } @@ -123,15 +145,21 @@ const validateQuery = (query: any): void => { if (query && query[key] && query[key].$regex) { if (typeof query[key].$options === 'string') { if (!query[key].$options.match(/^[imxs]+$/)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Bad $options value for query: ${query[key].$options}`); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Bad $options value for query: ${query[key].$options}` + ); } } } if (!isSpecialQueryKey(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`); + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Invalid key name: ${key}` + ); } }); -} +}; // Filters out any data that shouldn't be on this REST-formatted object. const filterSensitiveData = (isMaster, aclGroup, className, object) => { @@ -157,7 +185,7 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => { delete object._password_changed_at; delete object._password_history; - if ((aclGroup.indexOf(object.objectId) > -1)) { + if (aclGroup.indexOf(object.objectId) > -1) { return object; } delete object.authData; @@ -174,11 +202,21 @@ import type { LoadSchemaOptions } from './types'; // acl: a list of strings. If the object to be updated has an ACL, // one of the provided strings must provide the caller with // write permissions. -const specialKeysForUpdate = ['_hashed_password', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count', '_perishable_token_expires_at', '_password_changed_at', '_password_history']; +const specialKeysForUpdate = [ + '_hashed_password', + '_perishable_token', + '_email_verify_token', + '_email_verify_token_expires_at', + '_account_lockout_expires_at', + '_failed_login_count', + '_perishable_token_expires_at', + '_password_changed_at', + '_password_history', +]; const isSpecialUpdateKey = key => { return specialKeysForUpdate.indexOf(key) >= 0; -} +}; function expandResultOnKeyPath(object, key, value) { if (key.indexOf('.') < 0) { @@ -188,7 +226,11 @@ function expandResultOnKeyPath(object, key, value) { const path = key.split('.'); const firstKey = path[0]; const nextPath = path.slice(1).join('.'); - object[firstKey] = expandResultOnKeyPath(object[firstKey] || {}, nextPath, value[firstKey]); + object[firstKey] = expandResultOnKeyPath( + object[firstKey] || {}, + nextPath, + value[firstKey] + ); delete object[key]; return object; } @@ -201,8 +243,12 @@ function sanitizeDatabaseResult(originalObject, result): Promise { Object.keys(originalObject).forEach(key => { const keyUpdate = originalObject[key]; // determine if that was an op - if (keyUpdate && typeof keyUpdate === 'object' && keyUpdate.__op - && ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1) { + if ( + keyUpdate && + typeof keyUpdate === 'object' && + keyUpdate.__op && + ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1 + ) { // only valid ops that produce an actionable result // the op may have happend on a keypath expandResultOnKeyPath(response, key, result); @@ -219,39 +265,54 @@ const flattenUpdateOperatorsForCreate = object => { for (const key in object) { if (object[key] && object[key].__op) { switch (object[key].__op) { - case 'Increment': - if (typeof object[key].amount !== 'number') { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); - } - object[key] = object[key].amount; - break; - case 'Add': - if (!(object[key].objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); - } - object[key] = object[key].objects; - break; - case 'AddUnique': - if (!(object[key].objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); - } - object[key] = object[key].objects; - break; - case 'Remove': - if (!(object[key].objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); - } - object[key] = [] - break; - case 'Delete': - delete object[key]; - break; - default: - throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, `The ${object[key].__op} operator is not supported yet.`); + case 'Increment': + if (typeof object[key].amount !== 'number') { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'objects to add must be an array' + ); + } + object[key] = object[key].amount; + break; + case 'Add': + if (!(object[key].objects instanceof Array)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'objects to add must be an array' + ); + } + object[key] = object[key].objects; + break; + case 'AddUnique': + if (!(object[key].objects instanceof Array)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'objects to add must be an array' + ); + } + object[key] = object[key].objects; + break; + case 'Remove': + if (!(object[key].objects instanceof Array)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'objects to add must be an array' + ); + } + object[key] = []; + break; + case 'Delete': + delete object[key]; + break; + default: + throw new Parse.Error( + Parse.Error.COMMAND_UNAVAILABLE, + `The ${object[key].__op} operator is not supported yet.` + ); } } } -} +}; const transformAuthData = (className, object, schema) => { if (object.authData && className === '_User') { @@ -260,18 +321,18 @@ const transformAuthData = (className, object, schema) => { const fieldName = `_auth_data_${provider}`; if (providerData == null) { object[fieldName] = { - __op: 'Delete' - } + __op: 'Delete', + }; } else { object[fieldName] = providerData; - schema.fields[fieldName] = { type: 'Object' } + schema.fields[fieldName] = { type: 'Object' }; } }); delete object.authData; } -} +}; // Transforms a Database format ACL to a REST API format ACL -const untransformObjectACL = ({_rperm, _wperm, ...output}) => { +const untransformObjectACL = ({ _rperm, _wperm, ...output }) => { if (_rperm || _wperm) { output.ACL = {}; @@ -292,7 +353,7 @@ const untransformObjectACL = ({_rperm, _wperm, ...output}) => { }); } return output; -} +}; /** * When querying, the fieldName may be compound, extract the root fieldName @@ -301,10 +362,12 @@ const untransformObjectACL = ({_rperm, _wperm, ...output}) => { * @returns {string} the root name of the field */ const getRootFieldName = (fieldName: string): string => { - return fieldName.split('.')[0] -} + return fieldName.split('.')[0]; +}; -const relationSchema = { fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } } }; +const relationSchema = { + fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } }, +}; class DatabaseController { adapter: StorageAdapter; @@ -332,19 +395,32 @@ class DatabaseController { validateClassName(className: string): Promise { if (!SchemaController.classNameIsValid(className)) { - return Promise.reject(new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className)); + return Promise.reject( + new Parse.Error( + Parse.Error.INVALID_CLASS_NAME, + 'invalid className: ' + className + ) + ); } return Promise.resolve(); } // Returns a promise for a schemaController. - loadSchema(options: LoadSchemaOptions = {clearCache: false}): Promise { + loadSchema( + options: LoadSchemaOptions = { clearCache: false } + ): Promise { if (this.schemaPromise != null) { return this.schemaPromise; } - this.schemaPromise = SchemaController.load(this.adapter, this.schemaCache, options); - this.schemaPromise.then(() => delete this.schemaPromise, - () => delete this.schemaPromise); + this.schemaPromise = SchemaController.load( + this.adapter, + this.schemaCache, + options + ); + this.schemaPromise.then( + () => delete this.schemaPromise, + () => delete this.schemaPromise + ); return this.loadSchema(options); } @@ -352,8 +428,8 @@ class DatabaseController { // classname through the key. // TODO: make this not in the DatabaseController interface redirectClassNameForKey(className: string, key: string): Promise { - return this.loadSchema().then((schema) => { - var t = schema.getExpectedType(className, key); + return this.loadSchema().then(schema => { + var t = schema.getExpectedType(className, key); if (t != null && typeof t !== 'string' && t.type === 'Relation') { return t.targetClass; } @@ -365,26 +441,35 @@ class DatabaseController { // Returns a promise that resolves to the new schema. // This does not update this.schema, because in a situation like a // batch request, that could confuse other users of the schema. - validateObject(className: string, object: any, query: any, { acl }: QueryOptions): Promise { + validateObject( + className: string, + object: any, + query: any, + { acl }: QueryOptions + ): Promise { let schema; const isMaster = acl === undefined; - var aclGroup: string[] = acl || []; - return this.loadSchema().then(s => { - schema = s; - if (isMaster) { - return Promise.resolve(); - } - return this.canAddField(schema, className, object, aclGroup); - }).then(() => { - return schema.validateObject(className, object, query); - }); + var aclGroup: string[] = acl || []; + return this.loadSchema() + .then(s => { + schema = s; + if (isMaster) { + return Promise.resolve(); + } + return this.canAddField(schema, className, object, aclGroup); + }) + .then(() => { + return schema.validateObject(className, object, query); + }); } - update(className: string, query: any, update: any, { - acl, - many, - upsert, - }: FullQueryOptions = {}, skipSanitization: boolean = false): Promise { + update( + className: string, + query: any, + update: any, + { acl, many, upsert }: FullQueryOptions = {}, + skipSanitization: boolean = false + ): Promise { const originalQuery = query; const originalUpdate = update; // Make a copy of the object, so we don't mutate the incoming data. @@ -392,70 +477,125 @@ class DatabaseController { var relationUpdates = []; var isMaster = acl === undefined; var aclGroup = acl || []; - return this.loadSchema() - .then(schemaController => { - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'update')) - .then(() => { - relationUpdates = this.collectRelationUpdates(className, originalQuery.objectId, update); - if (!isMaster) { - query = this.addPointerPermissions(schemaController, className, 'update', query, aclGroup); - } - if (!query) { - return Promise.resolve(); - } - if (acl) { - query = addWriteACL(query, acl); - } - validateQuery(query); - return schemaController.getOneSchema(className, true) - .catch(error => { - // If the schema doesn't exist, pretend it exists with no fields. This behavior - // will likely need revisiting. - if (error === undefined) { - return { fields: {} }; - } - throw error; - }) - .then(schema => { - Object.keys(update).forEach(fieldName => { - if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`); - } - const rootFieldName = getRootFieldName(fieldName); - if (!SchemaController.fieldNameIsValid(rootFieldName) && !isSpecialUpdateKey(rootFieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name for update: ${fieldName}`); - } - }); - for (const updateOperation in update) { - if (update[updateOperation] && typeof update[updateOperation] === 'object' && Object.keys(update[updateOperation]).some(innerKey => innerKey.includes('$') || innerKey.includes('.'))) { - throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); - } + return this.loadSchema().then(schemaController => { + return (isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, 'update') + ) + .then(() => { + relationUpdates = this.collectRelationUpdates( + className, + originalQuery.objectId, + update + ); + if (!isMaster) { + query = this.addPointerPermissions( + schemaController, + className, + 'update', + query, + aclGroup + ); + } + if (!query) { + return Promise.resolve(); + } + if (acl) { + query = addWriteACL(query, acl); + } + validateQuery(query); + return schemaController + .getOneSchema(className, true) + .catch(error => { + // If the schema doesn't exist, pretend it exists with no fields. This behavior + // will likely need revisiting. + if (error === undefined) { + return { fields: {} }; + } + throw error; + }) + .then(schema => { + Object.keys(update).forEach(fieldName => { + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Invalid field name for update: ${fieldName}` + ); } - update = transformObjectACL(update); - transformAuthData(className, update, schema); - if (many) { - return this.adapter.updateObjectsByQuery(className, schema, query, update); - } else if (upsert) { - return this.adapter.upsertOneObject(className, schema, query, update); - } else { - return this.adapter.findOneAndUpdate(className, schema, query, update) + const rootFieldName = getRootFieldName(fieldName); + if ( + !SchemaController.fieldNameIsValid(rootFieldName) && + !isSpecialUpdateKey(rootFieldName) + ) { + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Invalid field name for update: ${fieldName}` + ); } }); - }) - .then((result: any) => { - if (!result) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); - } - return this.handleRelationUpdates(className, originalQuery.objectId, update, relationUpdates).then(() => { - return result; + for (const updateOperation in update) { + if ( + update[updateOperation] && + typeof update[updateOperation] === 'object' && + Object.keys(update[updateOperation]).some( + innerKey => innerKey.includes('$') || innerKey.includes('.') + ) + ) { + throw new Parse.Error( + Parse.Error.INVALID_NESTED_KEY, + "Nested keys should not contain the '$' or '.' characters" + ); + } + } + update = transformObjectACL(update); + transformAuthData(className, update, schema); + if (many) { + return this.adapter.updateObjectsByQuery( + className, + schema, + query, + update + ); + } else if (upsert) { + return this.adapter.upsertOneObject( + className, + schema, + query, + update + ); + } else { + return this.adapter.findOneAndUpdate( + className, + schema, + query, + update + ); + } }); - }).then((result) => { - if (skipSanitization) { - return Promise.resolve(result); - } - return sanitizeDatabaseResult(originalUpdate, result); + }) + .then((result: any) => { + if (!result) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); + } + return this.handleRelationUpdates( + className, + originalQuery.objectId, + update, + relationUpdates + ).then(() => { + return result; }); - }); + }) + .then(result => { + if (skipSanitization) { + return Promise.resolve(result); + } + return sanitizeDatabaseResult(originalUpdate, result); + }); + }); } // Collect all relation-updating operations from a REST-format update. @@ -471,12 +611,12 @@ class DatabaseController { return; } if (op.__op == 'AddRelation') { - ops.push({key, op}); + ops.push({ key, op }); deleteMe.push(key); } if (op.__op == 'RemoveRelation') { - ops.push({key, op}); + ops.push({ key, op }); deleteMe.push(key); } @@ -498,26 +638,31 @@ class DatabaseController { // Processes relation-updating operations from a REST-format update. // Returns a promise that resolves when all updates have been performed - handleRelationUpdates(className: string, objectId: string, update: any, ops: any) { + handleRelationUpdates( + className: string, + objectId: string, + update: any, + ops: any + ) { var pending = []; objectId = update.objectId || objectId; - ops.forEach(({key, op}) => { + ops.forEach(({ key, op }) => { if (!op) { return; } if (op.__op == 'AddRelation') { for (const object of op.objects) { - pending.push(this.addRelation(key, className, - objectId, - object.objectId)); + pending.push( + this.addRelation(key, className, objectId, object.objectId) + ); } } if (op.__op == 'RemoveRelation') { for (const object of op.objects) { - pending.push(this.removeRelation(key, className, - objectId, - object.objectId)); + pending.push( + this.removeRelation(key, className, objectId, object.objectId) + ); } } }); @@ -527,23 +672,43 @@ class DatabaseController { // Adds a relation. // Returns a promise that resolves successfully iff the add was successful. - addRelation(key: string, fromClassName: string, fromId: string, toId: string) { + addRelation( + key: string, + fromClassName: string, + fromId: string, + toId: string + ) { const doc = { relatedId: toId, - owningId: fromId + owningId: fromId, }; - return this.adapter.upsertOneObject(`_Join:${key}:${fromClassName}`, relationSchema, doc, doc); + return this.adapter.upsertOneObject( + `_Join:${key}:${fromClassName}`, + relationSchema, + doc, + doc + ); } // Removes a relation. // Returns a promise that resolves successfully iff the remove was // successful. - removeRelation(key: string, fromClassName: string, fromId: string, toId: string) { + removeRelation( + key: string, + fromClassName: string, + fromId: string, + toId: string + ) { var doc = { relatedId: toId, - owningId: fromId + owningId: fromId, }; - return this.adapter.deleteObjectsByQuery(`_Join:${key}:${fromClassName}`, relationSchema, doc) + return this.adapter + .deleteObjectsByQuery( + `_Join:${key}:${fromClassName}`, + relationSchema, + doc + ) .catch(error => { // We don't care if they try to delete a non-existent relation. if (error.code == Parse.Error.OBJECT_NOT_FOUND) { @@ -560,50 +725,78 @@ class DatabaseController { // acl: a list of strings. If the object to be updated has an ACL, // one of the provided strings must provide the caller with // write permissions. - destroy(className: string, query: any, { acl }: QueryOptions = {}): Promise { + destroy( + className: string, + query: any, + { acl }: QueryOptions = {} + ): Promise { const isMaster = acl === undefined; const aclGroup = acl || []; - return this.loadSchema() - .then(schemaController => { - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'delete')) - .then(() => { - if (!isMaster) { - query = this.addPointerPermissions(schemaController, className, 'delete', query, aclGroup); - if (!query) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); - } + return this.loadSchema().then(schemaController => { + return (isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, 'delete') + ).then(() => { + if (!isMaster) { + query = this.addPointerPermissions( + schemaController, + className, + 'delete', + query, + aclGroup + ); + if (!query) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); + } + } + // delete by query + if (acl) { + query = addWriteACL(query, acl); + } + validateQuery(query); + return schemaController + .getOneSchema(className) + .catch(error => { + // If the schema doesn't exist, pretend it exists with no fields. This behavior + // will likely need revisiting. + if (error === undefined) { + return { fields: {} }; } - // delete by query - if (acl) { - query = addWriteACL(query, acl); + throw error; + }) + .then(parseFormatSchema => + this.adapter.deleteObjectsByQuery( + className, + parseFormatSchema, + query + ) + ) + .catch(error => { + // When deleting sessions while changing passwords, don't throw an error if they don't have any sessions. + if ( + className === '_Session' && + error.code === Parse.Error.OBJECT_NOT_FOUND + ) { + return Promise.resolve({}); } - validateQuery(query); - return schemaController.getOneSchema(className) - .catch(error => { - // If the schema doesn't exist, pretend it exists with no fields. This behavior - // will likely need revisiting. - if (error === undefined) { - return { fields: {} }; - } - throw error; - }) - .then(parseFormatSchema => this.adapter.deleteObjectsByQuery(className, parseFormatSchema, query)) - .catch(error => { - // When deleting sessions while changing passwords, don't throw an error if they don't have any sessions. - if (className === "_Session" && error.code === Parse.Error.OBJECT_NOT_FOUND) { - return Promise.resolve({}); - } - throw error; - }); + throw error; }); }); + }); } // Inserts an object into the database. // Returns a promise that resolves successfully iff the object saved. - create(className: string, object: any, { acl }: QueryOptions = {}): Promise { - // Make a copy of the object, so we don't mutate the incoming data. + create( + className: string, + object: any, + { acl }: QueryOptions = {} + ): Promise { + // Make a copy of the object, so we don't mutate the incoming data. const originalObject = object; object = transformObjectACL(object); @@ -612,37 +805,62 @@ class DatabaseController { var isMaster = acl === undefined; var aclGroup = acl || []; - const relationUpdates = this.collectRelationUpdates(className, null, object); + const relationUpdates = this.collectRelationUpdates( + className, + null, + object + ); return this.validateClassName(className) .then(() => this.loadSchema()) .then(schemaController => { - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'create')) + return (isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, 'create') + ) .then(() => schemaController.enforceClassExists(className)) .then(() => schemaController.reloadData()) .then(() => schemaController.getOneSchema(className, true)) .then(schema => { transformAuthData(className, object, schema); flattenUpdateOperatorsForCreate(object); - return this.adapter.createObject(className, SchemaController.convertSchemaToAdapterSchema(schema), object); + return this.adapter.createObject( + className, + SchemaController.convertSchemaToAdapterSchema(schema), + object + ); }) .then(result => { - return this.handleRelationUpdates(className, object.objectId, object, relationUpdates).then(() => { - return sanitizeDatabaseResult(originalObject, result.ops[0]) + return this.handleRelationUpdates( + className, + object.objectId, + object, + relationUpdates + ).then(() => { + return sanitizeDatabaseResult(originalObject, result.ops[0]); }); }); - }) + }); } - canAddField(schema: SchemaController.SchemaController, className: string, object: any, aclGroup: string[]): Promise { + canAddField( + schema: SchemaController.SchemaController, + className: string, + object: any, + aclGroup: string[] + ): Promise { const classSchema = schema.data[className]; if (!classSchema) { return Promise.resolve(); } const fields = Object.keys(object); const schemaFields = Object.keys(classSchema); - const newKeys = fields.filter((field) => { + const newKeys = fields.filter(field => { // Skip fields that are unset - if (object[field] && object[field].__op && object[field].__op === 'Delete') { + if ( + object[field] && + object[field].__op && + object[field].__op === 'Delete' + ) { return false; } return schemaFields.indexOf(field) < 0; @@ -664,30 +882,50 @@ class DatabaseController { this.schemaPromise = null; return Promise.all([ this.adapter.deleteAllClasses(fast), - this.schemaCache.clear() + this.schemaCache.clear(), ]); } - // Returns a promise for a list of related ids given an owning id. // className here is the owning className. - relatedIds(className: string, key: string, owningId: string, queryOptions: QueryOptions): Promise> { + relatedIds( + className: string, + key: string, + owningId: string, + queryOptions: QueryOptions + ): Promise> { const { skip, limit, sort } = queryOptions; const findOptions = {}; if (sort && sort.createdAt && this.adapter.canSortOnJoinTables) { - findOptions.sort = { '_id' : sort.createdAt }; + findOptions.sort = { _id: sort.createdAt }; findOptions.limit = limit; findOptions.skip = skip; queryOptions.skip = 0; } - return this.adapter.find(joinTableName(className, key), relationSchema, { owningId }, findOptions) + return this.adapter + .find( + joinTableName(className, key), + relationSchema, + { owningId }, + findOptions + ) .then(results => results.map(result => result.relatedId)); } // Returns a promise for a list of owning ids given some related ids. // className here is the owning className. - owningIds(className: string, key: string, relatedIds: string[]): Promise { - return this.adapter.find(joinTableName(className, key), relationSchema, { relatedId: { '$in': relatedIds } }, {}) + owningIds( + className: string, + key: string, + relatedIds: string[] + ): Promise { + return this.adapter + .find( + joinTableName(className, key), + relationSchema, + { relatedId: { $in: relatedIds } }, + {} + ) .then(results => results.map(result => result.owningId)); } @@ -695,28 +933,38 @@ class DatabaseController { // equal-to-pointer constraints on relation fields. // Returns a promise that resolves when query is mutated reduceInRelation(className: string, query: any, schema: any): Promise { - // Search for an in-relation or equal-to-relation - // Make it sequential for now, not sure of paralleization side effects + // Search for an in-relation or equal-to-relation + // Make it sequential for now, not sure of paralleization side effects if (query['$or']) { const ors = query['$or']; - return Promise.all(ors.map((aQuery, index) => { - return this.reduceInRelation(className, aQuery, schema).then((aQuery) => { - query['$or'][index] = aQuery; - }); - })).then(() => { + return Promise.all( + ors.map((aQuery, index) => { + return this.reduceInRelation(className, aQuery, schema).then( + aQuery => { + query['$or'][index] = aQuery; + } + ); + }) + ).then(() => { return Promise.resolve(query); }); } - const promises = Object.keys(query).map((key) => { + const promises = Object.keys(query).map(key => { const t = schema.getExpectedType(className, key); if (!t || t.type !== 'Relation') { return Promise.resolve(query); } - let queries: ?any[] = null; - if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) { - // Build the list of queries - queries = Object.keys(query[key]).map((constraintKey) => { + let queries: ?(any[]) = null; + if ( + query[key] && + (query[key]['$in'] || + query[key]['$ne'] || + query[key]['$nin'] || + query[key].__type == 'Pointer') + ) { + // Build the list of queries + queries = Object.keys(query[key]).map(constraintKey => { let relatedIds; let isNegation = false; if (constraintKey === 'objectId') { @@ -734,22 +982,22 @@ class DatabaseController { } return { isNegation, - relatedIds - } + relatedIds, + }; }); } else { - queries = [{isNegation: false, relatedIds: []}]; + queries = [{ isNegation: false, relatedIds: [] }]; } // remove the current queryKey as we don,t need it anymore delete query[key]; // execute each query independently to build the list of // $in / $nin - const promises = queries.map((q) => { + const promises = queries.map(q => { if (!q) { return Promise.resolve(); } - return this.owningIds(className, key, q.relatedIds).then((ids) => { + return this.owningIds(className, key, q.relatedIds).then(ids => { if (q.isNegation) { this.addNotInObjectIdsIds(ids, query); } else { @@ -761,23 +1009,27 @@ class DatabaseController { return Promise.all(promises).then(() => { return Promise.resolve(); - }) - - }) + }); + }); return Promise.all(promises).then(() => { return Promise.resolve(query); - }) + }); } // Modifies query so that it no longer has $relatedTo // Returns a promise that resolves when query is mutated - reduceRelationKeys(className: string, query: any, queryOptions: any): ?Promise { - + reduceRelationKeys( + className: string, + query: any, + queryOptions: any + ): ?Promise { if (query['$or']) { - return Promise.all(query['$or'].map((aQuery) => { - return this.reduceRelationKeys(className, aQuery, queryOptions); - })); + return Promise.all( + query['$or'].map(aQuery => { + return this.reduceRelationKeys(className, aQuery, queryOptions); + }) + ); } var relatedTo = query['$relatedTo']; @@ -786,22 +1038,32 @@ class DatabaseController { relatedTo.object.className, relatedTo.key, relatedTo.object.objectId, - queryOptions) - .then((ids) => { + queryOptions + ) + .then(ids => { delete query['$relatedTo']; this.addInObjectIdsIds(ids, query); return this.reduceRelationKeys(className, query, queryOptions); - }).then(() => {}); + }) + .then(() => {}); } } addInObjectIdsIds(ids: ?Array = null, query: any) { - const idsFromString: ?Array = typeof query.objectId === 'string' ? [query.objectId] : null; - const idsFromEq: ?Array = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null; - const idsFromIn: ?Array = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null; + const idsFromString: ?Array = + typeof query.objectId === 'string' ? [query.objectId] : null; + const idsFromEq: ?Array = + query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null; + const idsFromIn: ?Array = + query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null; // @flow-disable-next - const allIds: Array> = [idsFromString, idsFromEq, idsFromIn, ids].filter(list => list !== null); + const allIds: Array> = [ + idsFromString, + idsFromEq, + idsFromIn, + ids, + ].filter(list => list !== null); const totalLength = allIds.reduce((memo, list) => memo + list.length, 0); let idsIntersection = []; @@ -819,7 +1081,7 @@ class DatabaseController { } else if (typeof query.objectId === 'string') { query.objectId = { $in: undefined, - $eq: query.objectId + $eq: query.objectId, }; } query.objectId['$in'] = idsIntersection; @@ -828,8 +1090,9 @@ class DatabaseController { } addNotInObjectIdsIds(ids: string[] = [], query: any) { - const idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : []; - let allIds = [...idsFromNin,...ids].filter(list => list !== null); + const idsFromNin = + query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : []; + let allIds = [...idsFromNin, ...ids].filter(list => list !== null); // make a set and spread to remove duplicates allIds = [...new Set(allIds)]; @@ -842,7 +1105,7 @@ class DatabaseController { } else if (typeof query.objectId === 'string') { query.objectId = { $nin: undefined, - $eq: query.objectId + $eq: query.objectId, }; } @@ -864,115 +1127,170 @@ class DatabaseController { // TODO: make userIds not needed here. The db adapter shouldn't know // anything about users, ideally. Then, improve the format of the ACL // arg to work like the others. - find(className: string, query: any, { - skip, - limit, - acl, - sort = {}, - count, - keys, - op, - distinct, - pipeline, - readPreference, - isWrite, - }: any = {}): Promise { + find( + className: string, + query: any, + { + skip, + limit, + acl, + sort = {}, + count, + keys, + op, + distinct, + pipeline, + readPreference, + isWrite, + }: any = {} + ): Promise { const isMaster = acl === undefined; const aclGroup = acl || []; - op = op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find'); + op = + op || + (typeof query.objectId == 'string' && Object.keys(query).length === 1 + ? 'get' + : 'find'); // Count operation if counting - op = (count === true ? 'count' : op); + op = count === true ? 'count' : op; let classExists = true; - return this.loadSchema() - .then(schemaController => { - //Allow volatile classes if querying with Master (for _PushStatus) - //TODO: Move volatile classes concept into mongo adapter, postgres adapter shouldn't care - //that api.parse.com breaks when _PushStatus exists in mongo. - return schemaController.getOneSchema(className, isMaster) - .catch(error => { + return this.loadSchema().then(schemaController => { + //Allow volatile classes if querying with Master (for _PushStatus) + //TODO: Move volatile classes concept into mongo adapter, postgres adapter shouldn't care + //that api.parse.com breaks when _PushStatus exists in mongo. + return schemaController + .getOneSchema(className, isMaster) + .catch(error => { // Behavior for non-existent classes is kinda weird on Parse.com. Probably doesn't matter too much. // For now, pretend the class exists but has no objects, - if (error === undefined) { - classExists = false; - return { fields: {} }; - } - throw error; - }) - .then(schema => { + if (error === undefined) { + classExists = false; + return { fields: {} }; + } + throw error; + }) + .then(schema => { // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, // so duplicate that behavior here. If both are specified, the correct behavior to match Parse.com is to // use the one that appears first in the sort list. - if (sort._created_at) { - sort.createdAt = sort._created_at; - delete sort._created_at; + if (sort._created_at) { + sort.createdAt = sort._created_at; + delete sort._created_at; + } + if (sort._updated_at) { + sort.updatedAt = sort._updated_at; + delete sort._updated_at; + } + const queryOptions = { skip, limit, sort, keys, readPreference }; + Object.keys(sort).forEach(fieldName => { + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Cannot sort by ${fieldName}` + ); } - if (sort._updated_at) { - sort.updatedAt = sort._updated_at; - delete sort._updated_at; + const rootFieldName = getRootFieldName(fieldName); + if (!SchemaController.fieldNameIsValid(rootFieldName)) { + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + `Invalid field name: ${fieldName}.` + ); } - const queryOptions = { skip, limit, sort, keys, readPreference }; - Object.keys(sort).forEach(fieldName => { - if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Cannot sort by ${fieldName}`); + }); + return (isMaster + ? Promise.resolve() + : schemaController.validatePermission(className, aclGroup, op) + ) + .then(() => this.reduceRelationKeys(className, query, queryOptions)) + .then(() => + this.reduceInRelation(className, query, schemaController) + ) + .then(() => { + if (!isMaster) { + query = this.addPointerPermissions( + schemaController, + className, + op, + query, + aclGroup + ); } - const rootFieldName = getRootFieldName(fieldName); - if (!SchemaController.fieldNameIsValid(rootFieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + if (!query) { + if (op == 'get') { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); + } else { + return []; + } } - }); - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)) - .then(() => this.reduceRelationKeys(className, query, queryOptions)) - .then(() => this.reduceInRelation(className, query, schemaController)) - .then(() => { - if (!isMaster) { - query = this.addPointerPermissions(schemaController, className, op, query, aclGroup); + if (!isMaster) { + if (isWrite) { + query = addWriteACL(query, aclGroup); + } else { + query = addReadACL(query, aclGroup); } - if (!query) { - if (op == 'get') { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); - } else { - return []; - } + } + validateQuery(query); + if (count) { + if (!classExists) { + return 0; + } else { + return this.adapter.count( + className, + schema, + query, + readPreference + ); } - if (!isMaster) { - if (isWrite) { - query = addWriteACL(query, aclGroup); - } else { - query = addReadACL(query, aclGroup); - } + } else if (distinct) { + if (!classExists) { + return []; + } else { + return this.adapter.distinct( + className, + schema, + query, + distinct + ); } - validateQuery(query); - if (count) { - if (!classExists) { - return 0; - } else { - return this.adapter.count(className, schema, query, readPreference); - } - } else if (distinct) { - if (!classExists) { - return []; - } else { - return this.adapter.distinct(className, schema, query, distinct); - } - } else if (pipeline) { - if (!classExists) { - return []; - } else { - return this.adapter.aggregate(className, schema, pipeline, readPreference); - } + } else if (pipeline) { + if (!classExists) { + return []; } else { - return this.adapter.find(className, schema, query, queryOptions) - .then(objects => objects.map(object => { - object = untransformObjectACL(object); - return filterSensitiveData(isMaster, aclGroup, className, object) - })).catch((error) => { - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, error); - }); + return this.adapter.aggregate( + className, + schema, + pipeline, + readPreference + ); } - }); - }); - }); + } else { + return this.adapter + .find(className, schema, query, queryOptions) + .then(objects => + objects.map(object => { + object = untransformObjectACL(object); + return filterSensitiveData( + isMaster, + aclGroup, + className, + object + ); + }) + ) + .catch(error => { + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + error + ); + }); + } + }); + }); + }); } deleteSchema(className: string): Promise { @@ -990,64 +1308,82 @@ class DatabaseController { .then(() => this.adapter.count(className, { fields: {} })) .then(count => { if (count > 0) { - throw new Parse.Error(255, `Class ${className} is not empty, contains ${count} objects, cannot drop schema.`); + throw new Parse.Error( + 255, + `Class ${className} is not empty, contains ${count} objects, cannot drop schema.` + ); } return this.adapter.deleteClass(className); }) .then(wasParseCollection => { if (wasParseCollection) { - const relationFieldNames = Object.keys(schema.fields).filter(fieldName => schema.fields[fieldName].type === 'Relation'); - return Promise.all(relationFieldNames.map(name => this.adapter.deleteClass(joinTableName(className, name)))).then(() => { + const relationFieldNames = Object.keys(schema.fields).filter( + fieldName => schema.fields[fieldName].type === 'Relation' + ); + return Promise.all( + relationFieldNames.map(name => + this.adapter.deleteClass(joinTableName(className, name)) + ) + ).then(() => { return; }); } else { return Promise.resolve(); } }); - }) + }); } - addPointerPermissions(schema: any, className: string, operation: string, query: any, aclGroup: any[] = []) { - // Check if class has public permission for operation - // If the BaseCLP pass, let go through + addPointerPermissions( + schema: any, + className: string, + operation: string, + query: any, + aclGroup: any[] = [] + ) { + // Check if class has public permission for operation + // If the BaseCLP pass, let go through if (schema.testBaseCLP(className, aclGroup, operation)) { return query; } const perms = schema.perms[className]; - const field = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields'; - const userACL = aclGroup.filter((acl) => { + const field = + ['get', 'find'].indexOf(operation) > -1 + ? 'readUserFields' + : 'writeUserFields'; + const userACL = aclGroup.filter(acl => { return acl.indexOf('role:') != 0 && acl != '*'; }); // the ACL should have exactly 1 user if (perms && perms[field] && perms[field].length > 0) { - // No user set return undefined - // If the length is > 1, that means we didn't de-dupe users correctly + // No user set return undefined + // If the length is > 1, that means we didn't de-dupe users correctly if (userACL.length != 1) { return; } const userId = userACL[0]; - const userPointer = { - "__type": "Pointer", - "className": "_User", - "objectId": userId + const userPointer = { + __type: 'Pointer', + className: '_User', + objectId: userId, }; const permFields = perms[field]; - const ors = permFields.map((key) => { + const ors = permFields.map(key => { const q = { - [key]: userPointer + [key]: userPointer, }; // if we already have a constraint on the key, use the $and if (query.hasOwnProperty(key)) { - return {'$and': [q, query]}; + return { $and: [q, query] }; } // otherwise just add the constaint return Object.assign({}, query, { [`${key}`]: userPointer, - }) + }); }); if (ors.length > 1) { - return {'$or': ors}; + return { $or: ors }; } return ors[0]; } else { @@ -1058,30 +1394,51 @@ class DatabaseController { // 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. performInitialization() { - const requiredUserFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._User } }; - const requiredRoleFields = { fields: { ...SchemaController.defaultColumns._Default, ...SchemaController.defaultColumns._Role } }; + const requiredUserFields = { + fields: { + ...SchemaController.defaultColumns._Default, + ...SchemaController.defaultColumns._User, + }, + }; + const requiredRoleFields = { + fields: { + ...SchemaController.defaultColumns._Default, + ...SchemaController.defaultColumns._Role, + }, + }; - const userClassPromise = this.loadSchema() - .then(schema => schema.enforceClassExists('_User')) - const roleClassPromise = this.loadSchema() - .then(schema => schema.enforceClassExists('_Role')) + const userClassPromise = this.loadSchema().then(schema => + schema.enforceClassExists('_User') + ); + const roleClassPromise = this.loadSchema().then(schema => + schema.enforceClassExists('_Role') + ); const usernameUniqueness = userClassPromise - .then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['username'])) + .then(() => + this.adapter.ensureUniqueness('_User', requiredUserFields, ['username']) + ) .catch(error => { logger.warn('Unable to ensure uniqueness for usernames: ', error); throw error; }); const emailUniqueness = userClassPromise - .then(() => this.adapter.ensureUniqueness('_User', requiredUserFields, ['email'])) + .then(() => + this.adapter.ensureUniqueness('_User', requiredUserFields, ['email']) + ) .catch(error => { - logger.warn('Unable to ensure uniqueness for user email addresses: ', error); + logger.warn( + 'Unable to ensure uniqueness for user email addresses: ', + error + ); throw error; }); const roleUniqueness = roleClassPromise - .then(() => this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name'])) + .then(() => + this.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']) + ) .catch(error => { logger.warn('Unable to ensure uniqueness for role name: ', error); throw error; @@ -1090,11 +1447,19 @@ class DatabaseController { const indexPromise = this.adapter.updateSchemaWithIndexes(); // Create tables for volatile classes - const adapterInit = this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas }); - return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit, indexPromise]); + const adapterInit = this.adapter.performInitialization({ + VolatileClassesSchemas: SchemaController.VolatileClassesSchemas, + }); + return Promise.all([ + usernameUniqueness, + emailUniqueness, + roleUniqueness, + adapterInit, + indexPromise, + ]); } - static _validateQuery: ((any) => void) + static _validateQuery: any => void; } module.exports = DatabaseController; diff --git a/src/Controllers/FilesController.js b/src/Controllers/FilesController.js index 16cd2ac79c..d0e42368e8 100644 --- a/src/Controllers/FilesController.js +++ b/src/Controllers/FilesController.js @@ -5,16 +5,16 @@ 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}-.*"); +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) { return this.adapter.getFileData(filename); } createFile(config, filename, data, contentType) { - const extname = path.extname(filename); const hasExtension = extname.length > 0; @@ -33,7 +33,7 @@ export class FilesController extends AdaptableController { return this.adapter.createFile(filename, data, contentType).then(() => { return Promise.resolve({ url: location, - name: filename + name: filename, }); }); } @@ -49,7 +49,7 @@ export class FilesController extends AdaptableController { */ expandFilesInObject(config, object) { if (object instanceof Array) { - object.map((obj) => this.expandFilesInObject(config, obj)); + object.map(obj => this.expandFilesInObject(config, obj)); return; } if (typeof object !== 'object') { @@ -69,9 +69,17 @@ export class FilesController extends AdaptableController { fileObject['url'] = this.adapter.getFileLocation(config, filename); } else { if (filename.indexOf('tfss-') === 0) { - fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename); + 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); + fileObject['url'] = + 'http://files.parse.com/' + + config.fileKey + + '/' + + encodeURIComponent(filename); } else { fileObject['url'] = this.adapter.getFileLocation(config, filename); } diff --git a/src/Controllers/HooksController.js b/src/Controllers/HooksController.js index 046cc1a295..78a23ac53c 100644 --- a/src/Controllers/HooksController.js +++ b/src/Controllers/HooksController.js @@ -1,26 +1,26 @@ /** @flow weak */ -import * as triggers from "../triggers"; +import * as triggers from '../triggers'; // @flow-disable-next -import * as Parse from "parse/node"; +import * as Parse from 'parse/node'; // @flow-disable-next -import * as request from "request"; -import { logger } from '../logger'; -import http from 'http'; -import https from 'https'; +import * as request from 'request'; +import { logger } from '../logger'; +import http from 'http'; +import https from 'https'; -const DefaultHooksCollectionName = "_Hooks"; +const DefaultHooksCollectionName = '_Hooks'; const HTTPAgents = { http: new http.Agent({ keepAlive: true }), https: new https.Agent({ keepAlive: true }), -} +}; export class HooksController { - _applicationId:string; - _webhookKey:string; + _applicationId: string; + _webhookKey: string; database: any; - constructor(applicationId:string, databaseController, webhookKey) { + constructor(applicationId: string, databaseController, webhookKey) { this._applicationId = applicationId; this._webhookKey = webhookKey; this.database = databaseController; @@ -29,14 +29,16 @@ export class HooksController { load() { return this._getHooks().then(hooks => { hooks = hooks || []; - hooks.forEach((hook) => { + hooks.forEach(hook => { this.addHookToTriggers(hook); }); }); } getFunction(functionName) { - return this._getHooks({ functionName: functionName }).then(results => results[0]); + return this._getHooks({ functionName: functionName }).then( + results => results[0] + ); } getFunctions() { @@ -44,11 +46,17 @@ export class HooksController { } getTrigger(className, triggerName) { - return this._getHooks({ className: className, triggerName: triggerName }).then(results => results[0]); + return this._getHooks({ + className: className, + triggerName: triggerName, + }).then(results => results[0]); } getTriggers() { - return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true } }); + return this._getHooks({ + className: { $exists: true }, + triggerName: { $exists: true }, + }); } deleteFunction(functionName) { @@ -58,16 +66,21 @@ export class HooksController { deleteTrigger(className, triggerName) { triggers.removeTrigger(triggerName, className, this._applicationId); - return this._removeHooks({ className: className, triggerName: triggerName }); + return this._removeHooks({ + className: className, + triggerName: triggerName, + }); } _getHooks(query = {}) { - return this.database.find(DefaultHooksCollectionName, query).then((results) => { - return results.map((result) => { - delete result.objectId; - return result; + return this.database + .find(DefaultHooksCollectionName, query) + .then(results => { + return results.map(result => { + delete result.objectId; + return result; + }); }); - }); } _removeHooks(query) { @@ -79,24 +92,36 @@ export class HooksController { saveHook(hook) { var query; if (hook.functionName && hook.url) { - query = { functionName: hook.functionName } + query = { functionName: hook.functionName }; } else if (hook.triggerName && hook.className && hook.url) { - query = { className: hook.className, triggerName: hook.triggerName } + query = { className: hook.className, triggerName: hook.triggerName }; } else { - throw new Parse.Error(143, "invalid hook declaration"); + throw new Parse.Error(143, 'invalid hook declaration'); } - return this.database.update(DefaultHooksCollectionName, query, hook, {upsert: true}).then(() => { - return Promise.resolve(hook); - }) + return this.database + .update(DefaultHooksCollectionName, query, hook, { upsert: true }) + .then(() => { + return Promise.resolve(hook); + }); } addHookToTriggers(hook) { var wrappedFunction = wrapToHTTPRequest(hook, this._webhookKey); wrappedFunction.url = hook.url; if (hook.className) { - triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, this._applicationId) + triggers.addTrigger( + hook.triggerName, + hook.className, + wrappedFunction, + this._applicationId + ); } else { - triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId); + triggers.addFunction( + hook.functionName, + wrappedFunction, + null, + this._applicationId + ); } } @@ -111,14 +136,19 @@ export class HooksController { hook = {}; hook.functionName = aHook.functionName; hook.url = aHook.url; - } else if (aHook && aHook.className && aHook.url && aHook.triggerName && triggers.Types[aHook.triggerName]) { + } else if ( + aHook && + aHook.className && + aHook.url && + aHook.triggerName && + triggers.Types[aHook.triggerName] + ) { hook = {}; hook.className = aHook.className; hook.url = aHook.url; hook.triggerName = aHook.triggerName; - } else { - throw new Parse.Error(143, "invalid hook declaration"); + throw new Parse.Error(143, 'invalid hook declaration'); } return this.addHook(hook); @@ -126,47 +156,62 @@ export class HooksController { createHook(aHook) { if (aHook.functionName) { - return this.getFunction(aHook.functionName).then((result) => { + return this.getFunction(aHook.functionName).then(result => { if (result) { - throw new Parse.Error(143, `function name: ${aHook.functionName} already exits`); + throw new Parse.Error( + 143, + `function name: ${aHook.functionName} already exits` + ); } else { return this.createOrUpdateHook(aHook); } }); } else if (aHook.className && aHook.triggerName) { - return this.getTrigger(aHook.className, aHook.triggerName).then((result) => { - if (result) { - throw new Parse.Error(143, `class ${aHook.className} already has trigger ${aHook.triggerName}`); + return this.getTrigger(aHook.className, aHook.triggerName).then( + result => { + if (result) { + throw new Parse.Error( + 143, + `class ${aHook.className} already has trigger ${ + aHook.triggerName + }` + ); + } + return this.createOrUpdateHook(aHook); } - return this.createOrUpdateHook(aHook); - }); + ); } - throw new Parse.Error(143, "invalid hook declaration"); + throw new Parse.Error(143, 'invalid hook declaration'); } updateHook(aHook) { if (aHook.functionName) { - return this.getFunction(aHook.functionName).then((result) => { + return this.getFunction(aHook.functionName).then(result => { if (result) { return this.createOrUpdateHook(aHook); } - throw new Parse.Error(143, `no function named: ${aHook.functionName} is defined`); + throw new Parse.Error( + 143, + `no function named: ${aHook.functionName} is defined` + ); }); } else if (aHook.className && aHook.triggerName) { - return this.getTrigger(aHook.className, aHook.triggerName).then((result) => { - if (result) { - return this.createOrUpdateHook(aHook); + return this.getTrigger(aHook.className, aHook.triggerName).then( + result => { + if (result) { + return this.createOrUpdateHook(aHook); + } + throw new Parse.Error(143, `class ${aHook.className} does not exist`); } - throw new Parse.Error(143, `class ${aHook.className} does not exist`); - }); + ); } - throw new Parse.Error(143, "invalid hook declaration"); + throw new Parse.Error(143, 'invalid hook declaration'); } } function wrapToHTTPRequest(hook, key) { - return (req) => { + return req => { const jsonBody = {}; for (var i in req) { jsonBody[i] = req[i]; @@ -181,32 +226,36 @@ function wrapToHTTPRequest(hook, key) { } const jsonRequest: any = { headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', }, body: JSON.stringify(jsonBody), }; - const agent = hook.url.startsWith('https') ? HTTPAgents['https'] : HTTPAgents['http']; + const agent = hook.url.startsWith('https') + ? HTTPAgents['https'] + : HTTPAgents['http']; jsonRequest.agent = agent; if (key) { jsonRequest.headers['X-Parse-Webhook-Key'] = key; } else { - logger.warn('Making outgoing webhook request without webhookKey being set!'); + logger.warn( + 'Making outgoing webhook request without webhookKey being set!' + ); } return new Promise((resolve, reject) => { - request.post(hook.url, jsonRequest, function (err, httpResponse, body) { + 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) { err = { - error: "Malformed response", + error: 'Malformed response', code: -1, - partialResponse: body.substring(0, 100) + partialResponse: body.substring(0, 100), }; } } @@ -222,13 +271,13 @@ function wrapToHTTPRequest(hook, key) { delete result.createdAt; delete result.updatedAt; } - return resolve({object: result}); + return resolve({ object: result }); } else { return resolve(result); } }); }); - } + }; } export default HooksController; diff --git a/src/Controllers/LiveQueryController.js b/src/Controllers/LiveQueryController.js index 7f741c359c..89834e2588 100644 --- a/src/Controllers/LiveQueryController.js +++ b/src/Controllers/LiveQueryController.js @@ -11,7 +11,7 @@ export class LiveQueryController { } else if (config.classNames instanceof Array) { this.classNames = new Set(config.classNames); } else { - throw 'liveQuery.classes should be an array of string' + throw 'liveQuery.classes should be an array of string'; } this.liveQueryPublisher = new ParseCloudCodePublisher(config); } @@ -38,7 +38,7 @@ export class LiveQueryController { _makePublisherRequest(currentObject: any, originalObject: any): any { const req = { - object: currentObject + object: currentObject, }; if (currentObject) { req.original = originalObject; diff --git a/src/Controllers/LoggerController.js b/src/Controllers/LoggerController.js index cc8f5318e3..54effc3ddf 100644 --- a/src/Controllers/LoggerController.js +++ b/src/Controllers/LoggerController.js @@ -9,26 +9,18 @@ const truncationMarker = '... (truncated)'; export const LogLevel = { INFO: 'info', - ERROR: 'error' -} + ERROR: 'error', +}; export const LogOrder = { DESCENDING: 'desc', - ASCENDING: 'asc' -} + ASCENDING: 'asc', +}; -const logLevels = [ - 'error', - 'warn', - 'info', - 'debug', - 'verbose', - 'silly', -] +const logLevels = ['error', 'warn', 'info', 'debug', 'verbose', 'silly']; export class LoggerController extends AdaptableController { - - constructor(adapter, appId, options = {logLevel: 'info'}) { + constructor(adapter, appId, options = { logLevel: 'info' }) { super(adapter, appId, options); let level = 'info'; if (options.verbose) { @@ -39,7 +31,8 @@ export class LoggerController extends AdaptableController { } const index = logLevels.indexOf(level); // info by default logLevels.forEach((level, levelIndex) => { - if (levelIndex > index) { // silence the levels that are > maxIndex + if (levelIndex > index) { + // silence the levels that are > maxIndex this[level] = () => {}; } }); @@ -50,8 +43,8 @@ export class LoggerController extends AdaptableController { const query = urlObj.query; let sanitizedQuery = '?'; - for(const key in query) { - if(key !== 'password') { + for (const key in query) { + if (key !== 'password') { // normal value sanitizedQuery += key + '=' + query[key] + '&'; } else { @@ -83,7 +76,8 @@ export class LoggerController extends AdaptableController { // for strings if (typeof e.url === 'string') { e.url = this.maskSensitiveUrl(e.url); - } else if (Array.isArray(e.url)) { // for strings in array + } else if (Array.isArray(e.url)) { + // for strings in array e.url = e.url.map(item => { if (typeof item === 'string') { return this.maskSensitiveUrl(item); @@ -119,10 +113,15 @@ export class LoggerController extends AdaptableController { log(level, args) { // make the passed in arguments object an array with the spread operator args = this.maskSensitive([...args]); - args = [].concat(level, args.map((arg) => { - if (typeof arg === 'function') { return arg(); } - return arg; - })); + args = [].concat( + level, + args.map(arg => { + if (typeof arg === 'function') { + return arg(); + } + return arg; + }) + ); this.adapter.log.apply(this.adapter, args); } @@ -150,33 +149,28 @@ export class LoggerController extends AdaptableController { return this.log('silly', arguments); } - logRequest({ - method, - url, - headers, - body - }) { - this.verbose(() => { - const stringifiedBody = JSON.stringify(body, null, 2); - return `REQUEST for [${method}] ${url}: ${stringifiedBody}`; - }, { - method, - url, - headers, - body - }); + logRequest({ method, url, headers, body }) { + this.verbose( + () => { + const stringifiedBody = JSON.stringify(body, null, 2); + return `REQUEST for [${method}] ${url}: ${stringifiedBody}`; + }, + { + method, + url, + headers, + body, + } + ); } - logResponse({ - method, - url, - result - }) { + logResponse({ method, url, result }) { this.verbose( - () => { const stringifiedResponse = JSON.stringify(result, null, 2); + () => { + const stringifiedResponse = JSON.stringify(result, null, 2); return `RESPONSE from [${method}] ${url}: ${stringifiedResponse}`; }, - {result: result} + { result: result } ); } // check that date input is valid @@ -195,7 +189,8 @@ export class LoggerController extends AdaptableController { truncateLogMessage(string) { if (string && string.length > LOG_STRING_TRUNCATE_LENGTH) { - const truncated = string.substring(0, LOG_STRING_TRUNCATE_LENGTH) + truncationMarker; + const truncated = + string.substring(0, LOG_STRING_TRUNCATE_LENGTH) + truncationMarker; return truncated; } @@ -203,7 +198,8 @@ export class LoggerController extends AdaptableController { } static parseOptions(options = {}) { - const from = LoggerController.validDateTime(options.from) || + const from = + LoggerController.validDateTime(options.from) || new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); const until = LoggerController.validDateTime(options.until) || new Date(); const size = Number(options.size) || 10; @@ -228,12 +224,16 @@ export class LoggerController extends AdaptableController { // size (optional) Number of rows returned by search. Defaults to 10 getLogs(options = {}) { if (!this.adapter) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Logger adapter is not available'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + 'Logger adapter is not available' + ); } if (typeof this.adapter.query !== 'function') { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Querying logs is not supported with this adapter'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + 'Querying logs is not supported with this adapter' + ); } options = LoggerController.parseOptions(options); return this.adapter.query(options); diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 8b645a3680..fcc910134b 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -1,16 +1,24 @@ -import { Parse } from 'parse/node'; -import RestQuery from '../RestQuery'; -import RestWrite from '../RestWrite'; -import { master } from '../Auth'; -import { pushStatusHandler } from '../StatusHandler'; +import { Parse } from 'parse/node'; +import RestQuery from '../RestQuery'; +import RestWrite from '../RestWrite'; +import { master } from '../Auth'; +import { pushStatusHandler } from '../StatusHandler'; import { applyDeviceTokenExists } from '../Push/utils'; export class PushController { - - sendPush(body = {}, where = {}, config, auth, onPushStatusSaved = () => {}, now = new Date()) { + sendPush( + body = {}, + where = {}, + config, + auth, + onPushStatusSaved = () => {}, + now = new Date() + ) { if (!config.hasPushSupport) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Missing push configuration'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + 'Missing push configuration' + ); } // Replace the expiration_time and push_time with a valid Unix epoch milliseconds time @@ -19,13 +27,14 @@ export class PushController { if (body.expiration_time && body.expiration_interval) { throw new Parse.Error( Parse.Error.PUSH_MISCONFIGURED, - 'Both expiration_time and expiration_interval cannot be set'); + 'Both expiration_time and expiration_interval cannot be set' + ); } // Immediate push if (body.expiration_interval && !body.hasOwnProperty('push_time')) { const ttlMs = body.expiration_interval * 1000; - body.expiration_time = (new Date(now.valueOf() + ttlMs)).valueOf(); + body.expiration_time = new Date(now.valueOf() + ttlMs).valueOf(); } const pushTime = PushController.getPushTime(body); @@ -37,18 +46,22 @@ export class PushController { // pushes to be sent. We probably change this behaviour in the future. let badgeUpdate = () => { return Promise.resolve(); - } + }; if (body.data && body.data.badge) { const badge = body.data.badge; let restUpdate = {}; if (typeof badge == 'string' && badge.toLowerCase() === 'increment') { - restUpdate = { badge: { __op: 'Increment', amount: 1 } } - } else if (typeof badge == 'object' && typeof badge.__op == 'string' && - badge.__op.toLowerCase() == 'increment' && Number(badge.amount)) { - restUpdate = { badge: { __op: 'Increment', amount: badge.amount } } + restUpdate = { badge: { __op: 'Increment', amount: 1 } }; + } else if ( + typeof badge == 'object' && + typeof badge.__op == 'string' && + badge.__op.toLowerCase() == 'increment' && + Number(badge.amount) + ) { + restUpdate = { badge: { __op: 'Increment', amount: badge.amount } }; } else if (Number(badge)) { - restUpdate = { badge: badge } + restUpdate = { badge: badge }; } else { throw "Invalid value for badge, expected number or 'Increment' or {increment: number}"; } @@ -57,44 +70,75 @@ export class PushController { const updateWhere = applyDeviceTokenExists(where); badgeUpdate = () => { // Build a real RestQuery so we can use it in RestWrite - const restQuery = new RestQuery(config, master(config), '_Installation', updateWhere); + const restQuery = new RestQuery( + config, + master(config), + '_Installation', + updateWhere + ); return restQuery.buildRestWhere().then(() => { - const write = new RestWrite(config, master(config), '_Installation', restQuery.restWhere, restUpdate); + const write = new RestWrite( + config, + master(config), + '_Installation', + restQuery.restWhere, + restUpdate + ); write.runOptions.many = true; return write.execute(); }); - } + }; } const pushStatus = pushStatusHandler(config); - return Promise.resolve().then(() => { - return pushStatus.setInitial(body, where); - }).then(() => { - onPushStatusSaved(pushStatus.objectId); - return badgeUpdate(); - }).then(() => { - // Update audience lastUsed and timesUsed - if (body.audience_id) { - const audienceId = body.audience_id; - - var updateAudience = { - lastUsed: { __type: "Date", iso: new Date().toISOString() }, - timesUsed: { __op: "Increment", "amount": 1 } - }; - const write = new RestWrite(config, master(config), '_Audience', {objectId: audienceId}, updateAudience); - write.execute(); - } - // Don't wait for the audience update promise to resolve. - return Promise.resolve(); - }).then(() => { - if (body.hasOwnProperty('push_time') && config.hasPushScheduledSupport) { + return Promise.resolve() + .then(() => { + return pushStatus.setInitial(body, where); + }) + .then(() => { + onPushStatusSaved(pushStatus.objectId); + return badgeUpdate(); + }) + .then(() => { + // Update audience lastUsed and timesUsed + if (body.audience_id) { + const audienceId = body.audience_id; + + var updateAudience = { + lastUsed: { __type: 'Date', iso: new Date().toISOString() }, + timesUsed: { __op: 'Increment', amount: 1 }, + }; + const write = new RestWrite( + config, + master(config), + '_Audience', + { objectId: audienceId }, + updateAudience + ); + write.execute(); + } + // Don't wait for the audience update promise to resolve. return Promise.resolve(); - } - return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus); - }).catch((err) => { - return pushStatus.fail(err).then(() => { - throw err; + }) + .then(() => { + if ( + body.hasOwnProperty('push_time') && + config.hasPushScheduledSupport + ) { + return Promise.resolve(); + } + return config.pushControllerQueue.enqueue( + body, + where, + config, + auth, + pushStatus + ); + }) + .catch(err => { + return pushStatus.fail(err).then(() => { + throw err; + }); }); - }); } /** @@ -114,13 +158,17 @@ export class PushController { } else if (typeof expirationTimeParam === 'string') { expirationTime = new Date(expirationTimeParam); } else { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - body['expiration_time'] + ' is not valid time.'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + body['expiration_time'] + ' is not valid time.' + ); } // Check expirationTime is valid or not, if it is not valid, expirationTime is NaN if (!isFinite(expirationTime)) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - body['expiration_time'] + ' is not valid time.'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + body['expiration_time'] + ' is not valid time.' + ); } return expirationTime.valueOf(); } @@ -132,9 +180,14 @@ export class PushController { } var expirationIntervalParam = body['expiration_interval']; - if (typeof expirationIntervalParam !== 'number' || expirationIntervalParam <= 0) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - `expiration_interval must be a number greater than 0`); + if ( + typeof expirationIntervalParam !== 'number' || + expirationIntervalParam <= 0 + ) { + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + `expiration_interval must be a number greater than 0` + ); } return expirationIntervalParam; } @@ -159,13 +212,17 @@ export class PushController { isLocalTime = !PushController.pushTimeHasTimezoneComponent(pushTimeParam); date = new Date(pushTimeParam); } else { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - body['push_time'] + ' is not valid time.'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + body['push_time'] + ' is not valid time.' + ); } // Check pushTime is valid or not, if it is not valid, pushTime is NaN if (!isFinite(date)) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - body['push_time'] + ' is not valid time.'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + body['push_time'] + ' is not valid time.' + ); } return { @@ -181,8 +238,10 @@ export class PushController { */ static pushTimeHasTimezoneComponent(pushTimeParam: string): boolean { const offsetPattern = /(.+)([+-])\d\d:\d\d$/; - return pushTimeParam.indexOf('Z') === pushTimeParam.length - 1 // 2007-04-05T12:30Z - || offsetPattern.test(pushTimeParam); // 2007-04-05T12:30.000+02:00, 2007-04-05T12:30.000-02:00 + return ( + pushTimeParam.indexOf('Z') === pushTimeParam.length - 1 || // 2007-04-05T12:30Z + offsetPattern.test(pushTimeParam) + ); // 2007-04-05T12:30.000+02:00, 2007-04-05T12:30.000-02:00 } /** @@ -191,8 +250,15 @@ export class PushController { * @param isLocalTime {boolean} * @returns {string} */ - static formatPushTime({ date, isLocalTime }: { date: Date, isLocalTime: boolean }) { - if (isLocalTime) { // Strip 'Z' + static formatPushTime({ + date, + isLocalTime, + }: { + date: Date, + isLocalTime: boolean, + }) { + if (isLocalTime) { + // Strip 'Z' const isoString = date.toISOString(); return isoString.substring(0, isoString.indexOf('Z')); } diff --git a/src/Controllers/SchemaCache.js b/src/Controllers/SchemaCache.js index 8284d16f7e..007d51d308 100644 --- a/src/Controllers/SchemaCache.js +++ b/src/Controllers/SchemaCache.js @@ -1,6 +1,6 @@ -const MAIN_SCHEMA = "__MAIN_SCHEMA"; -const SCHEMA_CACHE_PREFIX = "__SCHEMA"; -const ALL_KEYS = "__ALL_KEYS"; +const MAIN_SCHEMA = '__MAIN_SCHEMA'; +const SCHEMA_CACHE_PREFIX = '__SCHEMA'; +const ALL_KEYS = '__ALL_KEYS'; import { randomString } from '../cryptoUtils'; import defaults from '../defaults'; @@ -8,7 +8,11 @@ import defaults from '../defaults'; export default class SchemaCache { cache: Object; - constructor(cacheController, ttl = defaults.schemaCacheTTL, singleCache = false) { + constructor( + cacheController, + ttl = defaults.schemaCacheTTL, + singleCache = false + ) { this.ttl = ttl; if (typeof ttl == 'string') { this.ttl = parseInt(ttl); @@ -21,10 +25,13 @@ export default class SchemaCache { } put(key, value) { - return this.cache.get(this.prefix + ALL_KEYS).then((allKeys) => { + return this.cache.get(this.prefix + ALL_KEYS).then(allKeys => { allKeys = allKeys || {}; allKeys[key] = true; - return Promise.all([this.cache.put(this.prefix + ALL_KEYS, allKeys, this.ttl), this.cache.put(key, value, this.ttl)]); + return Promise.all([ + this.cache.put(this.prefix + ALL_KEYS, allKeys, this.ttl), + this.cache.put(key, value, this.ttl), + ]); }); } @@ -53,13 +60,13 @@ export default class SchemaCache { if (!this.ttl) { return Promise.resolve(null); } - return this.cache.get(this.prefix + className).then((schema) => { + return this.cache.get(this.prefix + className).then(schema => { if (schema) { return Promise.resolve(schema); } - return this.cache.get(this.prefix + MAIN_SCHEMA).then((cachedSchemas) => { + return this.cache.get(this.prefix + MAIN_SCHEMA).then(cachedSchemas => { cachedSchemas = cachedSchemas || []; - schema = cachedSchemas.find((cachedSchema) => { + schema = cachedSchemas.find(cachedSchema => { return cachedSchema.className === className; }); if (schema) { @@ -72,11 +79,11 @@ export default class SchemaCache { clear() { // That clears all caches... - return this.cache.get(this.prefix + ALL_KEYS).then((allKeys) => { + return this.cache.get(this.prefix + ALL_KEYS).then(allKeys => { if (!allKeys) { return; } - const promises = Object.keys(allKeys).map((key) => { + const promises = Object.keys(allKeys).map(key => { return this.cache.del(key); }); return Promise.all(promises); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 7c133f269c..a7252700ac 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -16,8 +16,8 @@ // TODO: hide all schema logic inside the database adapter. // @flow-disable-next const Parse = require('parse/node').Parse; -import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; -import DatabaseController from './DatabaseController'; +import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; +import DatabaseController from './DatabaseController'; import type { Schema, SchemaFields, @@ -26,137 +26,159 @@ import type { LoadSchemaOptions, } from './types'; -const defaultColumns: {[string]: SchemaFields} = Object.freeze({ +const defaultColumns: { [string]: SchemaFields } = 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'}, + 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'}, - "authData": {type:'Object'} + username: { type: 'String' }, + password: { type: 'String' }, + email: { type: 'String' }, + emailVerified: { type: 'Boolean' }, + authData: { type: 'Object' }, }, // 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'}, + 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'} + 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'} + 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'}, + 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:'String'}, // the stringified JSON payload, - "title": {type:'String'}, - "expiry": {type:'Number'}, - "expiration_interval": {type:'Number'}, - "status": {type:'String'}, - "numSent": {type:'Number'}, - "numFailed": {type:'Number'}, - "pushHash": {type:'String'}, - "errorMessage": {type:'Object'}, - "sentPerType": {type:'Object'}, - "failedPerType": {type:'Object'}, - "sentPerUTCOffset": {type:'Object'}, - "failedPerUTCOffset": {type:'Object'}, - "count": {type:'Number'} // tracks # of batches queued and pending + pushTime: { type: 'String' }, + source: { type: 'String' }, // rest or webui + query: { type: 'String' }, // the stringified JSON query + payload: { type: 'String' }, // the stringified JSON payload, + title: { type: 'String' }, + expiry: { type: 'Number' }, + expiration_interval: { type: 'Number' }, + status: { type: 'String' }, + numSent: { type: 'Number' }, + numFailed: { type: 'Number' }, + pushHash: { type: 'String' }, + errorMessage: { type: 'Object' }, + sentPerType: { type: 'Object' }, + failedPerType: { type: 'Object' }, + sentPerUTCOffset: { type: 'Object' }, + failedPerUTCOffset: { type: 'Object' }, + count: { type: 'Number' }, // tracks # of batches queued and pending }, _JobStatus: { - "jobName": {type: 'String'}, - "source": {type: 'String'}, - "status": {type: 'String'}, - "message": {type: 'String'}, - "params": {type: 'Object'}, // params received when calling the job - "finishedAt": {type: 'Date'} + jobName: { type: 'String' }, + source: { type: 'String' }, + status: { type: 'String' }, + message: { type: 'String' }, + params: { type: 'Object' }, // params received when calling the job + finishedAt: { type: 'Date' }, }, _JobSchedule: { - "jobName": {type:'String'}, - "description": {type:'String'}, - "params": {type:'String'}, - "startAfter": {type:'String'}, - "daysOfWeek": {type:'Array'}, - "timeOfDay": {type:'String'}, - "lastRun": {type:'Number'}, - "repeatMinutes":{type:'Number'} + jobName: { type: 'String' }, + description: { type: 'String' }, + params: { type: 'String' }, + startAfter: { type: 'String' }, + daysOfWeek: { type: 'Array' }, + timeOfDay: { type: 'String' }, + lastRun: { type: 'Number' }, + repeatMinutes: { type: 'Number' }, }, _Hooks: { - "functionName": {type:'String'}, - "className": {type:'String'}, - "triggerName": {type:'String'}, - "url": {type:'String'} + functionName: { type: 'String' }, + className: { type: 'String' }, + triggerName: { type: 'String' }, + url: { type: 'String' }, }, _GlobalConfig: { - "objectId": {type: 'String'}, - "params": {type: 'Object'} + objectId: { type: 'String' }, + params: { type: 'Object' }, }, _Audience: { - "objectId": {type:'String'}, - "name": {type:'String'}, - "query": {type:'String'}, //storing query as JSON string to prevent "Nested keys should not contain the '$' or '.' characters" error - "lastUsed": {type:'Date'}, - "timesUsed": {type:'Number'} - } + objectId: { type: 'String' }, + name: { type: 'String' }, + query: { type: 'String' }, //storing query as JSON string to prevent "Nested keys should not contain the '$' or '.' characters" error + lastUsed: { type: 'Date' }, + timesUsed: { type: 'Number' }, + }, }); const requiredColumns = Object.freeze({ - _Product: ["productIdentifier", "icon", "order", "title", "subtitle"], - _Role: ["name", "ACL"] + _Product: ['productIdentifier', 'icon', 'order', 'title', 'subtitle'], + _Role: ['name', 'ACL'], }); -const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus', '_JobStatus', '_JobSchedule', '_Audience']); - -const volatileClasses = Object.freeze(['_JobStatus', '_PushStatus', '_Hooks', '_GlobalConfig', '_JobSchedule', '_Audience']); +const systemClasses = Object.freeze([ + '_User', + '_Installation', + '_Role', + '_Session', + '_Product', + '_PushStatus', + '_JobStatus', + '_JobSchedule', + '_Audience', +]); + +const volatileClasses = Object.freeze([ + '_JobStatus', + '_PushStatus', + '_Hooks', + '_GlobalConfig', + '_JobSchedule', + '_Audience', +]); // 10 alpha numberic chars + uppercase const userIdRegex = /^[a-zA-Z0-9]{10}$/; // Anything that start with role const roleRegex = /^role:.*/; // * permission -const publicRegex = /^\*$/ +const publicRegex = /^\*$/; -const requireAuthenticationRegex = /^requiresAuthentication$/ +const requireAuthenticationRegex = /^requiresAuthentication$/; -const permissionKeyRegex = Object.freeze([userIdRegex, roleRegex, publicRegex, requireAuthenticationRegex]); +const permissionKeyRegex = Object.freeze([ + userIdRegex, + roleRegex, + publicRegex, + requireAuthenticationRegex, +]); function verifyPermissionKey(key) { const result = permissionKeyRegex.reduce((isGood, regEx) => { @@ -164,18 +186,34 @@ function verifyPermissionKey(key) { return isGood; }, false); if (!result) { - throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid key for class level permissions`); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `'${key}' is not a valid key for class level permissions` + ); } } -const CLPValidKeys = Object.freeze(['find', 'count', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']); +const CLPValidKeys = Object.freeze([ + 'find', + 'count', + 'get', + 'create', + 'update', + 'delete', + 'addField', + 'readUserFields', + 'writeUserFields', +]); function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields) { if (!perms) { return; } - Object.keys(perms).forEach((operation) => { + 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`); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `${operation} is not a valid operation for class level permissions` + ); } if (!perms[operation]) { return; @@ -184,11 +222,23 @@ function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields) { if (operation === 'readUserFields' || operation === 'writeUserFields') { if (!Array.isArray(perms[operation])) { // @flow-disable-next - throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perms[operation]}' is not a valid value for class level permissions ${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}`); + 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}` + ); } }); } @@ -196,13 +246,16 @@ function validateCLP(perms: ClassLevelPermissions, fields: SchemaFields) { } // @flow-disable-next - Object.keys(perms[operation]).forEach((key) => { + Object.keys(perms[operation]).forEach(key => { verifyPermissionKey(key); // @flow-disable-next const perm = perms[operation][key]; if (perm !== true) { // @flow-disable-next - throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}` + ); } }); }); @@ -227,7 +280,10 @@ function fieldNameIsValid(fieldName: string): boolean { } // Checks that it's not trying to clobber one of the default fields of the class. -function fieldNameIsValidForClass(fieldName: string, className: string): boolean { +function fieldNameIsValidForClass( + fieldName: string, + className: string +): boolean { if (!fieldNameIsValid(fieldName)) { return false; } @@ -241,10 +297,17 @@ function fieldNameIsValidForClass(fieldName: string, className: string): boolean } function invalidClassNameMessage(className: string): string { - return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character '; + 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 invalidJsonError = new Parse.Error( + Parse.Error.INVALID_JSON, + 'invalid JSON' +); const validNonRelationOrPointerTypes = [ 'Number', 'String', @@ -255,7 +318,7 @@ const validNonRelationOrPointerTypes = [ 'GeoPoint', 'File', 'Bytes', - 'Polygon' + 'Polygon', ]; // Returns an error suitable for throwing if the type is invalid const fieldTypeIsInvalid = ({ type, targetClass }) => { @@ -265,7 +328,10 @@ const fieldTypeIsInvalid = ({ type, targetClass }) => { } else if (typeof targetClass !== 'string') { return invalidJsonError; } else if (!classNameIsValid(targetClass)) { - return new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(targetClass)); + return new Parse.Error( + Parse.Error.INVALID_CLASS_NAME, + invalidClassNameMessage(targetClass) + ); } else { return undefined; } @@ -274,10 +340,13 @@ const fieldTypeIsInvalid = ({ type, targetClass }) => { return invalidJsonError; } if (validNonRelationOrPointerTypes.indexOf(type) < 0) { - return new Parse.Error(Parse.Error.INCORRECT_TYPE, `invalid field type: ${type}`); + return new Parse.Error( + Parse.Error.INCORRECT_TYPE, + `invalid field type: ${type}` + ); } return undefined; -} +}; const convertSchemaToAdapterSchema = (schema: any) => { schema = injectDefaultSchema(schema); @@ -291,9 +360,9 @@ const convertSchemaToAdapterSchema = (schema: any) => { } return schema; -} +}; -const convertAdapterSchemaToParseSchema = ({...schema}) => { +const convertAdapterSchemaToParseSchema = ({ ...schema }) => { delete schema.fields._rperm; delete schema.fields._wperm; @@ -310,9 +379,14 @@ const convertAdapterSchemaToParseSchema = ({...schema}) => { } return schema; -} +}; -const injectDefaultSchema = ({className, fields, classLevelPermissions, indexes}: Schema) => { +const injectDefaultSchema = ({ + className, + fields, + classLevelPermissions, + indexes, +}: Schema) => { const defaultSchema: Schema = { className, fields: { @@ -328,37 +402,58 @@ const injectDefaultSchema = ({className, fields, classLevelPermissions, indexes} return defaultSchema; }; -const _HooksSchema = {className: "_Hooks", fields: defaultColumns._Hooks}; -const _GlobalConfigSchema = { className: "_GlobalConfig", fields: defaultColumns._GlobalConfig } -const _PushStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ - className: "_PushStatus", - fields: {}, - classLevelPermissions: {} -})); -const _JobStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ - className: "_JobStatus", - fields: {}, - classLevelPermissions: {} -})); -const _JobScheduleSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ - className: "_JobSchedule", - fields: {}, - classLevelPermissions: {} -})); -const _AudienceSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ - className: "_Audience", - fields: defaultColumns._Audience, - classLevelPermissions: {} -})); -const VolatileClassesSchemas = [_HooksSchema, _JobStatusSchema, _JobScheduleSchema, _PushStatusSchema, _GlobalConfigSchema, _AudienceSchema]; - -const dbTypeMatchesObjectType = (dbType: SchemaField | string, objectType: SchemaField) => { +const _HooksSchema = { className: '_Hooks', fields: defaultColumns._Hooks }; +const _GlobalConfigSchema = { + className: '_GlobalConfig', + fields: defaultColumns._GlobalConfig, +}; +const _PushStatusSchema = convertSchemaToAdapterSchema( + injectDefaultSchema({ + className: '_PushStatus', + fields: {}, + classLevelPermissions: {}, + }) +); +const _JobStatusSchema = convertSchemaToAdapterSchema( + injectDefaultSchema({ + className: '_JobStatus', + fields: {}, + classLevelPermissions: {}, + }) +); +const _JobScheduleSchema = convertSchemaToAdapterSchema( + injectDefaultSchema({ + className: '_JobSchedule', + fields: {}, + classLevelPermissions: {}, + }) +); +const _AudienceSchema = convertSchemaToAdapterSchema( + injectDefaultSchema({ + className: '_Audience', + fields: defaultColumns._Audience, + classLevelPermissions: {}, + }) +); +const VolatileClassesSchemas = [ + _HooksSchema, + _JobStatusSchema, + _JobScheduleSchema, + _PushStatusSchema, + _GlobalConfigSchema, + _AudienceSchema, +]; + +const dbTypeMatchesObjectType = ( + dbType: SchemaField | string, + objectType: SchemaField +) => { 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; -} +}; const typeToString = (type: SchemaField | string): string => { if (typeof type === 'string') { @@ -368,7 +463,7 @@ const typeToString = (type: SchemaField | string): string => { return `${type.type}<${type.targetClass}>`; } return `${type.type}`; -} +}; // 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. @@ -391,7 +486,7 @@ export default class SchemaController { this.indexes = {}; } - reloadData(options: LoadSchemaOptions = {clearCache: false}): Promise { + reloadData(options: LoadSchemaOptions = { clearCache: false }): Promise { let promise = Promise.resolve(); if (options.clearCache) { promise = promise.then(() => { @@ -401,61 +496,79 @@ export default class SchemaController { if (this.reloadDataPromise && !options.clearCache) { return this.reloadDataPromise; } - this.reloadDataPromise = promise.then(() => { - return this.getAllClasses(options).then((allSchemas) => { - const data = {}; - const perms = {}; - const indexes = {}; - allSchemas.forEach(schema => { - data[schema.className] = injectDefaultSchema(schema).fields; - perms[schema.className] = schema.classLevelPermissions; - indexes[schema.className] = schema.indexes; - }); + this.reloadDataPromise = promise + .then(() => { + return this.getAllClasses(options).then( + allSchemas => { + const data = {}; + const perms = {}; + const indexes = {}; + allSchemas.forEach(schema => { + data[schema.className] = injectDefaultSchema(schema).fields; + perms[schema.className] = schema.classLevelPermissions; + indexes[schema.className] = schema.indexes; + }); - // Inject the in-memory classes - volatileClasses.forEach(className => { - const schema = injectDefaultSchema({ className, fields: {}, classLevelPermissions: {} }); - data[className] = schema.fields; - perms[className] = schema.classLevelPermissions; - indexes[className] = schema.indexes; - }); - this.data = data; - this.perms = perms; - this.indexes = indexes; - delete this.reloadDataPromise; - }, (err) => { - this.data = {}; - this.perms = {}; - this.indexes = {}; - delete this.reloadDataPromise; - throw err; + // Inject the in-memory classes + volatileClasses.forEach(className => { + const schema = injectDefaultSchema({ + className, + fields: {}, + classLevelPermissions: {}, + }); + data[className] = schema.fields; + perms[className] = schema.classLevelPermissions; + indexes[className] = schema.indexes; + }); + this.data = data; + this.perms = perms; + this.indexes = indexes; + delete this.reloadDataPromise; + }, + err => { + this.data = {}; + this.perms = {}; + this.indexes = {}; + delete this.reloadDataPromise; + throw err; + } + ); }) - }).then(() => {}); + .then(() => {}); return this.reloadDataPromise; } - getAllClasses(options: LoadSchemaOptions = {clearCache: false}): Promise> { + getAllClasses( + options: LoadSchemaOptions = { clearCache: false } + ): Promise> { let promise = Promise.resolve(); if (options.clearCache) { promise = this._cache.clear(); } - return promise.then(() => { - return this._cache.getAllClasses() - }).then((allClasses) => { - if (allClasses && allClasses.length && !options.clearCache) { - return Promise.resolve(allClasses); - } - return this._dbAdapter.getAllClasses() - .then(allSchemas => allSchemas.map(injectDefaultSchema)) - .then(allSchemas => { - return this._cache.setAllClasses(allSchemas).then(() => { - return allSchemas; + return promise + .then(() => { + return this._cache.getAllClasses(); + }) + .then(allClasses => { + if (allClasses && allClasses.length && !options.clearCache) { + return Promise.resolve(allClasses); + } + return this._dbAdapter + .getAllClasses() + .then(allSchemas => allSchemas.map(injectDefaultSchema)) + .then(allSchemas => { + return this._cache.setAllClasses(allSchemas).then(() => { + return allSchemas; + }); }); - }) - }); + }); } - getOneSchema(className: string, allowVolatileClasses: boolean = false, options: LoadSchemaOptions = {clearCache: false}): Promise { + getOneSchema( + className: string, + allowVolatileClasses: boolean = false, + options: LoadSchemaOptions = { clearCache: false } + ): Promise { let promise = Promise.resolve(); if (options.clearCache) { promise = this._cache.clear(); @@ -466,19 +579,20 @@ export default class SchemaController { className, fields: this.data[className], classLevelPermissions: this.perms[className], - indexes: this.indexes[className] + indexes: this.indexes[className], }); } - return this._cache.getOneSchema(className).then((cached) => { + return this._cache.getOneSchema(className).then(cached => { if (cached && !options.clearCache) { return Promise.resolve(cached); } - return this._dbAdapter.getClass(className) + return this._dbAdapter + .getClass(className) .then(injectDefaultSchema) - .then((result) => { + .then(result => { return this._cache.setOneSchema(className, result).then(() => { return result; - }) + }); }); }); }); @@ -491,29 +605,56 @@ export default class SchemaController { // 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: string, fields: SchemaFields = {}, classLevelPermissions: any, indexes: any = {}): Promise { - var validationError = this.validateNewClass(className, fields, classLevelPermissions); + addClassIfNotExists( + className: string, + fields: SchemaFields = {}, + classLevelPermissions: any, + indexes: any = {} + ): Promise { + var validationError = this.validateNewClass( + className, + fields, + classLevelPermissions + ); if (validationError) { return Promise.reject(validationError); } - return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, indexes, className })) + return this._dbAdapter + .createClass( + className, + convertSchemaToAdapterSchema({ + fields, + classLevelPermissions, + indexes, + className, + }) + ) .then(convertAdapterSchemaToParseSchema) - .then((res) => { + .then(res => { return this._cache.clear().then(() => { return Promise.resolve(res); }); }) .catch(error => { if (error && error.code === Parse.Error.DUPLICATE_VALUE) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); + throw new Parse.Error( + Parse.Error.INVALID_CLASS_NAME, + `Class ${className} already exists.` + ); } else { throw error; } }); } - updateClass(className: string, submittedFields: SchemaFields, classLevelPermissions: any, indexes: any, database: DatabaseController) { + updateClass( + className: string, + submittedFields: SchemaFields, + classLevelPermissions: any, + indexes: any, + database: DatabaseController + ) { return this.getOneSchema(className) .then(schema => { const existingFields = schema.fields; @@ -523,16 +664,28 @@ export default class SchemaController { 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.`); + throw new Parse.Error( + 255, + `Field ${name} does not exist, cannot delete.` + ); } }); delete existingFields._rperm; delete existingFields._wperm; - const newSchema = buildMergedSchemaObject(existingFields, submittedFields); - const defaultFields = defaultColumns[className] || defaultColumns._Default; + const newSchema = buildMergedSchemaObject( + existingFields, + submittedFields + ); + const defaultFields = + defaultColumns[className] || defaultColumns._Default; const fullNewSchema = Object.assign({}, newSchema, defaultFields); - const validationError = this.validateSchemaData(className, newSchema, classLevelPermissions, Object.keys(existingFields)); + const validationError = this.validateSchemaData( + className, + newSchema, + classLevelPermissions, + Object.keys(existingFields) + ); if (validationError) { throw new Parse.Error(validationError.code, validationError.error); } @@ -553,38 +706,55 @@ export default class SchemaController { if (deletedFields.length > 0) { deletePromise = this.deleteFields(deletedFields, className, database); } - return deletePromise // Delete Everything - .then(() => this.reloadData({ clearCache: true })) // Reload our Schema, so we have all the new values - .then(() => { - const promises = insertedFields.map(fieldName => { - const type = submittedFields[fieldName]; - return this.enforceFieldExists(className, fieldName, type); - }); - return Promise.all(promises); - }) - .then(() => this.setPermissions(className, classLevelPermissions, newSchema)) - .then(() => this._dbAdapter.setIndexesWithSchemaFormat(className, indexes, schema.indexes, fullNewSchema)) - .then(() => this.reloadData({ clearCache: true })) - //TODO: Move this logic into the database adapter - .then(() => { - const reloadedSchema: Schema = { - className: className, - fields: this.data[className], - classLevelPermissions: this.perms[className], - }; - if (this.indexes[className] && Object.keys(this.indexes[className]).length !== 0) { - reloadedSchema.indexes = this.indexes[className]; - } - return reloadedSchema; - }); + return ( + deletePromise // Delete Everything + .then(() => this.reloadData({ clearCache: true })) // Reload our Schema, so we have all the new values + .then(() => { + const promises = insertedFields.map(fieldName => { + const type = submittedFields[fieldName]; + return this.enforceFieldExists(className, fieldName, type); + }); + return Promise.all(promises); + }) + .then(() => + this.setPermissions(className, classLevelPermissions, newSchema) + ) + .then(() => + this._dbAdapter.setIndexesWithSchemaFormat( + className, + indexes, + schema.indexes, + fullNewSchema + ) + ) + .then(() => this.reloadData({ clearCache: true })) + //TODO: Move this logic into the database adapter + .then(() => { + const reloadedSchema: Schema = { + className: className, + fields: this.data[className], + classLevelPermissions: this.perms[className], + }; + if ( + this.indexes[className] && + Object.keys(this.indexes[className]).length !== 0 + ) { + reloadedSchema.indexes = this.indexes[className]; + } + return reloadedSchema; + }) + ); }) .catch(error => { if (error === undefined) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`); + 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 @@ -594,33 +764,48 @@ export default class SchemaController { 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({ clearCache: true })) - .catch(() => { - // 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({ clearCache: true }); - }) - .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(() => { - // The schema still doesn't validate. Give up - throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate'); - }); + return ( + this.addClassIfNotExists(className) + // The schema update succeeded. Reload the schema + .then(() => this.reloadData({ clearCache: true })) + .catch(() => { + // 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({ clearCache: true }); + }) + .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(() => { + // The schema still doesn't validate. Give up + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'schema class name does not revalidate' + ); + }) + ); } - validateNewClass(className: string, fields: SchemaFields = {}, classLevelPermissions: any): any { + validateNewClass( + className: string, + fields: SchemaFields = {}, + classLevelPermissions: any + ): any { if (this.data[className]) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); + throw new Parse.Error( + Parse.Error.INVALID_CLASS_NAME, + `Class ${className} already exists.` + ); } if (!classNameIsValid(className)) { return { @@ -628,10 +813,20 @@ export default class SchemaController { error: invalidClassNameMessage(className), }; } - return this.validateSchemaData(className, fields, classLevelPermissions, []); + return this.validateSchemaData( + className, + fields, + classLevelPermissions, + [] + ); } - validateSchemaData(className: string, fields: SchemaFields, classLevelPermissions: ClassLevelPermissions, existingFieldNames: Array) { + validateSchemaData( + className: string, + fields: SchemaFields, + classLevelPermissions: ClassLevelPermissions, + existingFieldNames: Array + ) { for (const fieldName in fields) { if (existingFieldNames.indexOf(fieldName) < 0) { if (!fieldNameIsValid(fieldName)) { @@ -655,11 +850,18 @@ export default class SchemaController { fields[fieldName] = defaultColumns[className][fieldName]; } - const geoPoints = Object.keys(fields).filter(key => fields[key] && fields[key].type === 'GeoPoint'); + const 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.', + error: + 'currently, only one GeoPoint field may exist in an object. Adding ' + + geoPoints[1] + + ' when ' + + geoPoints[0] + + ' already exists.', }; } validateCLP(classLevelPermissions, fields); @@ -678,14 +880,21 @@ export default class SchemaController { // 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: string, fieldName: string, type: string | SchemaField) { - if (fieldName.indexOf(".") > 0) { + enforceFieldExists( + className: string, + fieldName: string, + type: string | SchemaField + ) { + if (fieldName.indexOf('.') > 0) { // subdocument key (x.y) => ok if x is of type 'object' - fieldName = fieldName.split(".")[ 0 ]; + fieldName = fieldName.split('.')[0]; type = 'Object'; } if (!fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${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; @@ -703,42 +912,57 @@ export default class SchemaController { if (!dbTypeMatchesObjectType(expectedType, type)) { throw new Parse.Error( Parse.Error.INCORRECT_TYPE, - `schema mismatch for ${className}.${fieldName}; expected ${typeToString(expectedType)} but got ${typeToString(type)}` + `schema mismatch for ${className}.${fieldName}; expected ${typeToString( + expectedType + )} but got ${typeToString(type)}` ); } return this; } - return this._dbAdapter.addFieldIfNotExists(className, fieldName, type).then(() => { - // The update succeeded. Reload the schema - return this.reloadData({ clearCache: true }); - }, (error) => { - if (error.code == Parse.Error.INCORRECT_TYPE) { - // Make sure that we throw errors when it is appropriate to do so. - throw error; - } - // 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({ clearCache: true }); - }).then(() => { - // Ensure that the schema now validates - const expectedType = this.getExpectedType(className, fieldName); - if (typeof type === 'string') { - type = { type }; - } - if (!expectedType || !dbTypeMatchesObjectType(expectedType, type)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, `Could not add field ${fieldName}`); - } - // Remove the cached schema - this._cache.clear(); - return this; - }); + return this._dbAdapter + .addFieldIfNotExists(className, fieldName, type) + .then( + () => { + // The update succeeded. Reload the schema + return this.reloadData({ clearCache: true }); + }, + error => { + if (error.code == Parse.Error.INCORRECT_TYPE) { + // Make sure that we throw errors when it is appropriate to do so. + throw error; + } + // 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({ clearCache: true }); + } + ) + .then(() => { + // Ensure that the schema now validates + const expectedType = this.getExpectedType(className, fieldName); + if (typeof type === 'string') { + type = { type }; + } + if (!expectedType || !dbTypeMatchesObjectType(expectedType, type)) { + throw new Parse.Error( + Parse.Error.INVALID_JSON, + `Could not add field ${fieldName}` + ); + } + // Remove the cached schema + this._cache.clear(); + return this; + }); }); } // maintain compatibility - deleteField(fieldName: string, className: string, database: DatabaseController) { + deleteField( + fieldName: string, + className: string, + database: DatabaseController + ) { return this.deleteFields([fieldName], className, database); } @@ -749,14 +973,24 @@ export default class SchemaController { // 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. - deleteFields(fieldNames: Array, className: string, database: DatabaseController) { + deleteFields( + fieldNames: Array, + className: string, + database: DatabaseController + ) { if (!classNameIsValid(className)) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className)); + throw new Parse.Error( + Parse.Error.INVALID_CLASS_NAME, + invalidClassNameMessage(className) + ); } fieldNames.forEach(fieldName => { if (!fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${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)) { @@ -764,10 +998,13 @@ export default class SchemaController { } }); - return this.getOneSchema(className, false, {clearCache: true}) + return this.getOneSchema(className, false, { clearCache: true }) .catch(error => { if (error === undefined) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`); + throw new Parse.Error( + Parse.Error.INVALID_CLASS_NAME, + `Class ${className} does not exist.` + ); } else { throw error; } @@ -775,23 +1012,32 @@ export default class SchemaController { .then(schema => { fieldNames.forEach(fieldName => { if (!schema.fields[fieldName]) { - throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`); + throw new Parse.Error( + 255, + `Field ${fieldName} does not exist, cannot delete.` + ); } }); const schemaFields = { ...schema.fields }; - return database.adapter.deleteFields(className, schema, fieldNames) + return database.adapter + .deleteFields(className, schema, fieldNames) .then(() => { - return Promise.all(fieldNames.map(fieldName => { - const field = schemaFields[fieldName]; - if (field && field.type === 'Relation') { - //For relations, drop the _Join table - return database.adapter.deleteClass(`_Join:${fieldName}:${className}`); - } - return Promise.resolve(); - })); + return Promise.all( + fieldNames.map(fieldName => { + const field = schemaFields[fieldName]; + if (field && field.type === 'Relation') { + //For relations, drop the _Join table + return database.adapter.deleteClass( + `_Join:${fieldName}:${className}` + ); + } + return Promise.resolve(); + }) + ); }); - }).then(() => { + }) + .then(() => { this._cache.clear(); }); } @@ -814,8 +1060,12 @@ export default class SchemaController { // 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')); + return Promise.reject( + new Parse.Error( + Parse.Error.INCORRECT_TYPE, + 'there can only be one geopoint field in a class' + ) + ); }); } if (!expected) { @@ -826,7 +1076,9 @@ export default class SchemaController { continue; } - promise = promise.then(schema => schema.enforceFieldExists(className, fieldName, expected)); + promise = promise.then(schema => + schema.enforceFieldExists(className, fieldName, expected) + ); } promise = thenValidateRequiredColumns(promise, className, object, query); return promise; @@ -839,22 +1091,23 @@ export default class SchemaController { return Promise.resolve(this); } - const missingColumns = columns.filter(function(column){ + const missingColumns = columns.filter(function(column) { if (query && query.objectId) { - if (object[column] && typeof object[column] === "object") { + 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] + return !object[column]; }); if (missingColumns.length > 0) { throw new Parse.Error( Parse.Error.INCORRECT_TYPE, - missingColumns[0] + ' is required.'); + missingColumns[0] + ' is required.' + ); } return Promise.resolve(this); } @@ -871,7 +1124,11 @@ export default class SchemaController { return true; } // Check permissions against the aclGroup provided (array of userId/roles) - if (aclGroup.some(acl => { return perms[acl] === true })) { + if ( + aclGroup.some(acl => { + return perms[acl] === true; + }) + ) { return true; } return false; @@ -879,7 +1136,6 @@ export default class SchemaController { // Validates an operation passes class-level-permissions set in the schema validatePermission(className: string, aclGroup: string[], operation: string) { - if (this.testBaseCLP(className, aclGroup, operation)) { return Promise.resolve(); } @@ -895,11 +1151,15 @@ export default class SchemaController { if (perms['requiresAuthentication']) { // If aclGroup has * (public) if (!aclGroup || aclGroup.length == 0) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Permission denied, user needs to be authenticated.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Permission denied, user needs to be authenticated.' + ); } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Permission denied, user needs to be authenticated.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Permission denied, user needs to be authenticated.' + ); } // requiresAuthentication passed, just move forward // probably would be wise at some point to rename to 'authenticatedUser' @@ -908,27 +1168,40 @@ export default class SchemaController { // No matching CLP, let's check the Pointer permissions // And handle those later - const permissionField = ['get', 'find', 'count'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields'; + const permissionField = + ['get', 'find', 'count'].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 action ${operation} on class ${className}.`); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + `Permission denied for action ${operation} on class ${className}.` + ); } // Process the readUserFields later - if (Array.isArray(classPerms[permissionField]) && classPerms[permissionField].length > 0) { + if ( + Array.isArray(classPerms[permissionField]) && + classPerms[permissionField].length > 0 + ) { return Promise.resolve(); } - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, - `Permission denied for action ${operation} on class ${className}.`); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + `Permission denied for action ${operation} on class ${className}.` + ); } // Returns the expected type for a className+key combination // or undefined if the schema is not set - getExpectedType(className: string, fieldName: string): ?(SchemaField | string) { + getExpectedType( + className: string, + fieldName: string + ): ?(SchemaField | string) { if (this.data && this.data[className]) { - const expectedType = this.data[className][fieldName] + const expectedType = this.data[className][fieldName]; return expectedType === 'map' ? 'Object' : expectedType; } return undefined; @@ -936,31 +1209,51 @@ export default class SchemaController { // Checks if a given class is in the schema. hasClass(className: string) { - return this.reloadData().then(() => !!(this.data[className])); + return this.reloadData().then(() => !!this.data[className]); } } // Returns a promise for a new Schema. -const load = (dbAdapter: StorageAdapter, schemaCache: any, options: any): Promise => { +const load = ( + dbAdapter: StorageAdapter, + schemaCache: any, + options: any +): Promise => { const schema = new SchemaController(dbAdapter, schemaCache); return schema.reloadData(options).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: SchemaFields, putRequest: any): SchemaFields { +function buildMergedSchemaObject( + existingFields: SchemaFields, + putRequest: any +): SchemaFields { const newSchema = {}; // @flow-disable-next - const sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]); + const sysSchemaField = + Object.keys(defaultColumns).indexOf(existingFields._id) === -1 + ? [] + : Object.keys(defaultColumns[existingFields._id]); for (const oldField in existingFields) { - if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') { - if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) { + if ( + oldField !== '_id' && + oldField !== 'ACL' && + oldField !== 'updatedAt' && + oldField !== 'createdAt' && + oldField !== 'objectId' + ) { + if ( + sysSchemaField.length > 0 && + sysSchemaField.indexOf(oldField) !== -1 + ) { continue; } - const fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete' + const fieldIsDeleted = + putRequest[oldField] && putRequest[oldField].__op === 'Delete'; if (!fieldIsDeleted) { newSchema[oldField] = existingFields[oldField]; } @@ -968,7 +1261,10 @@ function buildMergedSchemaObject(existingFields: SchemaFields, putRequest: any): } for (const newField in putRequest) { if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') { - if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) { + if ( + sysSchemaField.length > 0 && + sysSchemaField.indexOf(newField) !== -1 + ) { continue; } newSchema[newField] = putRequest[newField]; @@ -980,7 +1276,7 @@ function buildMergedSchemaObject(existingFields: SchemaFields, putRequest: any): // 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 schemaPromise.then(schema => { return schema.validateRequiredColumns(className, object, query); }); } @@ -992,24 +1288,24 @@ function thenValidateRequiredColumns(schemaPromise, className, object, query) { // TODO: ensure that this is compatible with the format used in Open DB function getType(obj: any): ?(SchemaField | string) { const 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; + 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; } } @@ -1020,75 +1316,78 @@ function getObjectType(obj): ?(SchemaField | string) { if (obj instanceof Array) { return 'Array'; } - if (obj.__type){ - switch(obj.__type) { - case 'Pointer' : - if(obj.className) { - return { - type: 'Pointer', - targetClass: obj.className + if (obj.__type) { + switch (obj.__type) { + case 'Pointer': + if (obj.className) { + return { + type: 'Pointer', + targetClass: obj.className, + }; } - } - break; - case 'Relation' : - if(obj.className) { - return { - type: 'Relation', - targetClass: obj.className + break; + case 'Relation': + if (obj.className) { + return { + type: 'Relation', + targetClass: obj.className, + }; } - } - break; - case 'File' : - if(obj.name) { - return 'File'; - } - break; - case 'Date' : - if(obj.iso) { - return 'Date'; - } - break; - case 'GeoPoint' : - if(obj.latitude != null && obj.longitude != null) { - return 'GeoPoint'; - } - break; - case 'Bytes' : - if(obj.base64) { - return 'Bytes'; - } - break; - case 'Polygon' : - if(obj.coordinates) { - return 'Polygon'; - } - break; + break; + case 'File': + if (obj.name) { + return 'File'; + } + break; + case 'Date': + if (obj.iso) { + return 'Date'; + } + break; + case 'GeoPoint': + if (obj.latitude != null && obj.longitude != null) { + return 'GeoPoint'; + } + break; + case 'Bytes': + if (obj.base64) { + return 'Bytes'; + } + break; + case 'Polygon': + if (obj.coordinates) { + return 'Polygon'; + } + break; } - throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid " + obj.__type); + throw new Parse.Error( + Parse.Error.INCORRECT_TYPE, + 'This is not a valid ' + obj.__type + ); } if (obj['$ne']) { return getObjectType(obj['$ne']); } 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; + 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'; diff --git a/src/Controllers/UserController.js b/src/Controllers/UserController.js index 76a5066242..3518505037 100644 --- a/src/Controllers/UserController.js +++ b/src/Controllers/UserController.js @@ -1,15 +1,14 @@ -import { randomString } from '../cryptoUtils'; -import { inflate } from '../triggers'; +import { randomString } from '../cryptoUtils'; +import { inflate } from '../triggers'; import AdaptableController from './AdaptableController'; -import MailAdapter from '../Adapters/Email/MailAdapter'; -import rest from '../rest'; -import Parse from 'parse/node'; +import MailAdapter from '../Adapters/Email/MailAdapter'; +import rest from '../rest'; +import Parse from 'parse/node'; var RestQuery = require('../RestQuery'); var Auth = require('../Auth'); export class UserController extends AdaptableController { - constructor(adapter, appId, options = {}) { super(adapter, appId, options); } @@ -36,7 +35,9 @@ export class UserController extends AdaptableController { user.emailVerified = false; if (this.config.emailVerifyTokenValidityDuration) { - user._email_verify_token_expires_at = Parse._encode(this.config.generateEmailVerifyTokenExpiresAt()); + user._email_verify_token_expires_at = Parse._encode( + this.config.generateEmailVerifyTokenExpiresAt() + ); } } } @@ -48,8 +49,11 @@ export class UserController extends AdaptableController { throw undefined; } - const query = {username: username, _email_verify_token: token}; - const updateFields = { emailVerified: true, _email_verify_token: {__op: 'Delete'}}; + const query = { username: username, _email_verify_token: token }; + const updateFields = { + emailVerified: true, + _email_verify_token: { __op: 'Delete' }, + }; // if the email verify token needs to be validated then // add additional query params and additional fields that need to be updated @@ -57,10 +61,15 @@ export class UserController extends AdaptableController { query.emailVerified = false; query._email_verify_token_expires_at = { $gt: Parse._encode(new Date()) }; - updateFields._email_verify_token_expires_at = {__op: 'Delete'}; + updateFields._email_verify_token_expires_at = { __op: 'Delete' }; } const masterAuth = Auth.master(this.config); - var checkIfAlreadyVerified = new RestQuery(this.config, Auth.master(this.config), '_User', {username: username, emailVerified: true}); + var checkIfAlreadyVerified = new RestQuery( + this.config, + Auth.master(this.config), + '_User', + { username: username, emailVerified: true } + ); return checkIfAlreadyVerified.execute().then(result => { if (result.results.length) { return Promise.resolve(result.results.length[0]); @@ -70,25 +79,34 @@ export class UserController extends AdaptableController { } checkResetTokenValidity(username, token) { - return this.config.database.find('_User', { - username: username, - _perishable_token: token - }, {limit: 1}).then(results => { - if (results.length != 1) { - throw undefined; - } + return this.config.database + .find( + '_User', + { + username: username, + _perishable_token: token, + }, + { limit: 1 } + ) + .then(results => { + if (results.length != 1) { + throw undefined; + } - if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { - let expiresDate = results[0]._perishable_token_expires_at; - if (expiresDate && expiresDate.__type == 'Date') { - expiresDate = new Date(expiresDate.iso); + if ( + this.config.passwordPolicy && + this.config.passwordPolicy.resetTokenValidityDuration + ) { + let expiresDate = results[0]._perishable_token_expires_at; + if (expiresDate && expiresDate.__type == 'Date') { + expiresDate = new Date(expiresDate.iso); + } + if (expiresDate < new Date()) + throw 'The password reset link has expired'; } - if (expiresDate < new Date()) - throw 'The password reset link has expired'; - } - return results[0]; - }); + return results[0]; + }); } getUserIfNeeded(user) { @@ -103,13 +121,18 @@ export class UserController extends AdaptableController { where.email = user.email; } - var query = new RestQuery(this.config, Auth.master(this.config), '_User', where); - return query.execute().then(function(result){ + var query = new RestQuery( + this.config, + Auth.master(this.config), + '_User', + where + ); + return query.execute().then(function(result) { if (result.results.length != 1) { throw undefined; } return result.results[0]; - }) + }); } sendVerificationEmail(user) { @@ -118,10 +141,15 @@ export class UserController extends AdaptableController { } const token = encodeURIComponent(user._email_verify_token); // We may need to fetch the user in case of update email - this.getUserIfNeeded(user).then((user) => { + this.getUserIfNeeded(user).then(user => { const username = encodeURIComponent(user.username); - const link = buildEmailLink(this.config.verifyEmailURL, username, token, this.config); + const link = buildEmailLink( + this.config.verifyEmailURL, + username, + token, + this.config + ); const options = { appName: this.config.appName, link: link, @@ -143,11 +171,15 @@ export class UserController extends AdaptableController { */ regenerateEmailVerifyToken(user) { this.setEmailVerifyToken(user); - return this.config.database.update('_User', { username: user.username }, user); + return this.config.database.update( + '_User', + { username: user.username }, + user + ); } resendVerificationEmail(username) { - return this.getUserIfNeeded({username: username}).then((aUser) => { + return this.getUserIfNeeded({ username: username }).then(aUser => { if (!aUser || aUser.emailVerified) { throw undefined; } @@ -160,93 +192,141 @@ export class UserController extends AdaptableController { setPasswordResetToken(email) { const token = { _perishable_token: randomString(25) }; - if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { - token._perishable_token_expires_at = Parse._encode(this.config.generatePasswordResetTokenExpiresAt()); + if ( + this.config.passwordPolicy && + this.config.passwordPolicy.resetTokenValidityDuration + ) { + token._perishable_token_expires_at = Parse._encode( + this.config.generatePasswordResetTokenExpiresAt() + ); } - return this.config.database.update('_User', { $or: [{email}, {username: email, email: {$exists: false}}] }, token, {}, true) + return this.config.database.update( + '_User', + { $or: [{ email }, { username: email, email: { $exists: false } }] }, + token, + {}, + true + ); } sendPasswordResetEmail(email) { if (!this.adapter) { - throw "Trying to send a reset password but no adapter is set"; + throw 'Trying to send a reset password but no adapter is set'; // TODO: No adapter? } - return this.setPasswordResetToken(email) - .then(user => { - const token = encodeURIComponent(user._perishable_token); - const username = encodeURIComponent(user.username); - - const link = buildEmailLink(this.config.requestResetPasswordURL, username, token, this.config); - const options = { - appName: this.config.appName, - link: link, - user: inflate('_User', user), - }; - - if (this.adapter.sendPasswordResetEmail) { - this.adapter.sendPasswordResetEmail(options); - } else { - this.adapter.sendMail(this.defaultResetPasswordEmail(options)); - } + return this.setPasswordResetToken(email).then(user => { + const token = encodeURIComponent(user._perishable_token); + const username = encodeURIComponent(user.username); - return Promise.resolve(user); - }); + const link = buildEmailLink( + this.config.requestResetPasswordURL, + username, + token, + this.config + ); + const options = { + appName: this.config.appName, + link: link, + user: inflate('_User', user), + }; + + if (this.adapter.sendPasswordResetEmail) { + this.adapter.sendPasswordResetEmail(options); + } else { + this.adapter.sendMail(this.defaultResetPasswordEmail(options)); + } + + return Promise.resolve(user); + }); } updatePassword(username, token, password) { - return this.checkResetTokenValidity(username, token) - .then(user => updateUserPassword(user.objectId, password, this.config)) - // clear reset password token - .then(() => this.config.database.update('_User', {username}, { - _perishable_token: {__op: 'Delete'}, - _perishable_token_expires_at: {__op: 'Delete'} - })).catch((error) => { - if (error.message) { // in case of Parse.Error, fail with the error message only - return Promise.reject(error.message); - } else { - return Promise.reject(error); - } - }); + return ( + this.checkResetTokenValidity(username, token) + .then(user => updateUserPassword(user.objectId, password, this.config)) + // clear reset password token + .then(() => + this.config.database.update( + '_User', + { username }, + { + _perishable_token: { __op: 'Delete' }, + _perishable_token_expires_at: { __op: 'Delete' }, + } + ) + ) + .catch(error => { + if (error.message) { + // in case of Parse.Error, fail with the error message only + return Promise.reject(error.message); + } else { + return Promise.reject(error); + } + }) + ); } - defaultVerificationEmail({link, user, appName, }) { - const text = "Hi,\n\n" + - "You are being asked to confirm the e-mail address " + user.get("email") + " with " + appName + "\n\n" + - "" + - "Click here to confirm it:\n" + link; - const to = user.get("email"); + defaultVerificationEmail({ link, user, appName }) { + const text = + 'Hi,\n\n' + + 'You are being asked to confirm the e-mail address ' + + user.get('email') + + ' with ' + + appName + + '\n\n' + + '' + + 'Click here to confirm it:\n' + + link; + const to = user.get('email'); const subject = 'Please verify your e-mail for ' + appName; return { text, to, subject }; } - defaultResetPasswordEmail({link, user, appName, }) { - const text = "Hi,\n\n" + - "You requested to reset your password for " + appName + - (user.get('username') ? (" (your username is '" + user.get('username') + "')") : "") + ".\n\n" + - "" + - "Click here to reset it:\n" + link; - const to = user.get("email") || user.get('username'); - const subject = 'Password Reset for ' + appName; + defaultResetPasswordEmail({ link, user, appName }) { + const text = + 'Hi,\n\n' + + 'You requested to reset your password for ' + + appName + + (user.get('username') + ? " (your username is '" + user.get('username') + "')" + : '') + + '.\n\n' + + '' + + 'Click here to reset it:\n' + + link; + const to = user.get('email') || user.get('username'); + const subject = 'Password Reset for ' + appName; return { text, to, subject }; } } // Mark this private function updateUserPassword(userId, password, config) { - return rest.update(config, Auth.master(config), '_User', { objectId: userId }, { - password: password - }); + return rest.update( + config, + Auth.master(config), + '_User', + { objectId: userId }, + { + password: password, + } + ); } function buildEmailLink(destination, username, token, config) { - const usernameAndToken = `token=${token}&username=${username}` + const usernameAndToken = `token=${token}&username=${username}`; if (config.parseFrameURL) { - const destinationWithoutHost = destination.replace(config.publicServerURL, ''); - - return `${config.parseFrameURL}?link=${encodeURIComponent(destinationWithoutHost)}&${usernameAndToken}`; + const destinationWithoutHost = destination.replace( + config.publicServerURL, + '' + ); + + return `${config.parseFrameURL}?link=${encodeURIComponent( + destinationWithoutHost + )}&${usernameAndToken}`; } else { return `${destination}?${usernameAndToken}`; } diff --git a/src/Controllers/index.js b/src/Controllers/index.js index a1958cc983..68630a07df 100644 --- a/src/Controllers/index.js +++ b/src/Controllers/index.js @@ -1,30 +1,30 @@ -import authDataManager from '../Adapters/Auth'; -import { ParseServerOptions } from '../Options'; -import { loadAdapter } from '../Adapters/AdapterLoader'; -import defaults from '../defaults'; -import url from 'url'; +import authDataManager from '../Adapters/Auth'; +import { ParseServerOptions } from '../Options'; +import { loadAdapter } from '../Adapters/AdapterLoader'; +import defaults from '../defaults'; +import url from 'url'; // Controllers -import { LoggerController } from './LoggerController'; -import { FilesController } from './FilesController'; -import { HooksController } from './HooksController'; -import { UserController } from './UserController'; -import { CacheController } from './CacheController'; -import { LiveQueryController } from './LiveQueryController'; -import { AnalyticsController } from './AnalyticsController'; -import { PushController } from './PushController'; -import { PushQueue } from '../Push/PushQueue'; -import { PushWorker } from '../Push/PushWorker'; -import DatabaseController from './DatabaseController'; -import SchemaCache from './SchemaCache'; +import { LoggerController } from './LoggerController'; +import { FilesController } from './FilesController'; +import { HooksController } from './HooksController'; +import { UserController } from './UserController'; +import { CacheController } from './CacheController'; +import { LiveQueryController } from './LiveQueryController'; +import { AnalyticsController } from './AnalyticsController'; +import { PushController } from './PushController'; +import { PushQueue } from '../Push/PushQueue'; +import { PushWorker } from '../Push/PushWorker'; +import DatabaseController from './DatabaseController'; +import SchemaCache from './SchemaCache'; // Adapters -import { GridStoreAdapter } from '../Adapters/Files/GridStoreAdapter'; +import { GridStoreAdapter } from '../Adapters/Files/GridStoreAdapter'; import { WinstonLoggerAdapter } from '../Adapters/Logger/WinstonLoggerAdapter'; import { InMemoryCacheAdapter } from '../Adapters/Cache/InMemoryCacheAdapter'; -import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter'; -import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter'; -import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter'; -import ParsePushAdapter from '@parse/push-adapter'; +import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter'; +import MongoStorageAdapter from '../Adapters/Storage/Mongo/MongoStorageAdapter'; +import PostgresStorageAdapter from '../Adapters/Storage/Postgres/PostgresStorageAdapter'; +import ParsePushAdapter from '@parse/push-adapter'; export function getControllers(options: ParseServerOptions) { const loggerController = getLoggerController(options); @@ -35,7 +35,7 @@ export function getControllers(options: ParseServerOptions) { hasPushScheduledSupport, hasPushSupport, pushControllerQueue, - pushWorker + pushWorker, } = getPushController(options); const cacheController = getCacheController(options); const analyticsController = getAnalyticsController(options); @@ -61,7 +61,9 @@ export function getControllers(options: ParseServerOptions) { }; } -export function getLoggerController(options: ParseServerOptions): LoggerController { +export function getLoggerController( + options: ParseServerOptions +): LoggerController { const { appId, jsonLogs, @@ -72,11 +74,17 @@ export function getLoggerController(options: ParseServerOptions): LoggerControll loggerAdapter, } = options; const loggerOptions = { jsonLogs, logsFolder, verbose, logLevel, silent }; - const loggerControllerAdapter = loadAdapter(loggerAdapter, WinstonLoggerAdapter, loggerOptions); + const loggerControllerAdapter = loadAdapter( + loggerAdapter, + WinstonLoggerAdapter, + loggerOptions + ); return new LoggerController(loggerControllerAdapter, appId, loggerOptions); } -export function getFilesController(options: ParseServerOptions): FilesController { +export function getFilesController( + options: ParseServerOptions +): FilesController { const { appId, databaseURI, @@ -90,43 +98,52 @@ export function getFilesController(options: ParseServerOptions): FilesController const filesControllerAdapter = loadAdapter(filesAdapter, () => { return new GridStoreAdapter(databaseURI); }); - return new FilesController(filesControllerAdapter, appId, { preserveFileName }); + return new FilesController(filesControllerAdapter, appId, { + preserveFileName, + }); } export function getUserController(options: ParseServerOptions): UserController { - const { - appId, - emailAdapter, - verifyUserEmails, - } = options; + const { appId, emailAdapter, verifyUserEmails } = options; const emailControllerAdapter = loadAdapter(emailAdapter); - return new UserController(emailControllerAdapter, appId, { verifyUserEmails }); + return new UserController(emailControllerAdapter, appId, { + verifyUserEmails, + }); } -export function getCacheController(options: ParseServerOptions): CacheController { - const { - appId, +export function getCacheController( + options: ParseServerOptions +): CacheController { + const { appId, cacheAdapter, cacheTTL, cacheMaxSize } = options; + const cacheControllerAdapter = loadAdapter( cacheAdapter, - cacheTTL, - cacheMaxSize, - } = options; - const cacheControllerAdapter = loadAdapter(cacheAdapter, InMemoryCacheAdapter, {appId: appId, ttl: cacheTTL, maxSize: cacheMaxSize }); + InMemoryCacheAdapter, + { appId: appId, ttl: cacheTTL, maxSize: cacheMaxSize } + ); return new CacheController(cacheControllerAdapter, appId); } -export function getAnalyticsController(options: ParseServerOptions): AnalyticsController { - const { +export function getAnalyticsController( + options: ParseServerOptions +): AnalyticsController { + const { analyticsAdapter } = options; + const analyticsControllerAdapter = loadAdapter( analyticsAdapter, - } = options; - const analyticsControllerAdapter = loadAdapter(analyticsAdapter, AnalyticsAdapter); + AnalyticsAdapter + ); return new AnalyticsController(analyticsControllerAdapter); } -export function getLiveQueryController(options: ParseServerOptions): LiveQueryController { +export function getLiveQueryController( + options: ParseServerOptions +): LiveQueryController { return new LiveQueryController(options.liveQuery); } -export function getDatabaseController(options: ParseServerOptions, cacheController: CacheController): DatabaseController { +export function getDatabaseController( + options: ParseServerOptions, + cacheController: CacheController +): DatabaseController { const { databaseURI, databaseOptions, @@ -134,39 +151,48 @@ export function getDatabaseController(options: ParseServerOptions, cacheControll schemaCacheTTL, enableSingleSchemaCache, } = options; - let { + let { databaseAdapter } = options; + if ( + (databaseOptions || + (databaseURI && databaseURI !== defaults.databaseURI) || + collectionPrefix !== defaults.collectionPrefix) && databaseAdapter - } = options; - if ((databaseOptions || (databaseURI && databaseURI !== defaults.databaseURI) || collectionPrefix !== defaults.collectionPrefix) && databaseAdapter) { + ) { throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/collectionPrefix.'; } else if (!databaseAdapter) { - databaseAdapter = getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) + databaseAdapter = getDatabaseAdapter( + databaseURI, + collectionPrefix, + databaseOptions + ); } else { - databaseAdapter = loadAdapter(databaseAdapter) + databaseAdapter = loadAdapter(databaseAdapter); } - return new DatabaseController(databaseAdapter, new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache)); + return new DatabaseController( + databaseAdapter, + new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache) + ); } -export function getHooksController(options: ParseServerOptions, databaseController: DatabaseController): HooksController { - const { - appId, - webhookKey, - } = options; +export function getHooksController( + options: ParseServerOptions, + databaseController: DatabaseController +): HooksController { + const { appId, webhookKey } = options; return new HooksController(appId, databaseController, webhookKey); } interface PushControlling { - pushController: PushController, - hasPushScheduledSupport: boolean, - pushControllerQueue: PushQueue, - pushWorker: PushWorker + pushController: PushController; + hasPushScheduledSupport: boolean; + pushControllerQueue: PushQueue; + pushWorker: PushWorker; } -export function getPushController(options: ParseServerOptions): PushControlling { - const { - scheduledPush, - push, - } = options; +export function getPushController( + options: ParseServerOptions +): PushControlling { + const { scheduledPush, push } = options; const pushOptions = Object.assign({}, push); const pushQueueOptions = pushOptions.queueOptions || {}; @@ -175,16 +201,18 @@ export function getPushController(options: ParseServerOptions): PushControlling } // Pass the push options too as it works with the default - const pushAdapter = loadAdapter(pushOptions && pushOptions.adapter, ParsePushAdapter, pushOptions); + const pushAdapter = loadAdapter( + pushOptions && pushOptions.adapter, + ParsePushAdapter, + pushOptions + ); // We pass the options and the base class for the adatper, // Note that passing an instance would work too const pushController = new PushController(); const hasPushSupport = !!(pushAdapter && push); - const hasPushScheduledSupport = hasPushSupport && (scheduledPush === true); + const hasPushScheduledSupport = hasPushSupport && scheduledPush === true; - const { - disablePushWorker - } = pushQueueOptions; + const { disablePushWorker } = pushQueueOptions; const pushControllerQueue = new PushQueue(pushQueueOptions); let pushWorker; @@ -196,36 +224,39 @@ export function getPushController(options: ParseServerOptions): PushControlling hasPushSupport, hasPushScheduledSupport, pushControllerQueue, - pushWorker - } + pushWorker, + }; } export function getAuthDataManager(options: ParseServerOptions) { - const { - auth, - enableAnonymousUsers - } = options; - return authDataManager(auth, enableAnonymousUsers) + const { auth, enableAnonymousUsers } = options; + return authDataManager(auth, enableAnonymousUsers); } -export function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) { +export function getDatabaseAdapter( + databaseURI, + collectionPrefix, + databaseOptions +) { let protocol; try { const parsedURI = url.parse(databaseURI); protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null; - } catch(e) { /* */ } + } catch (e) { + /* */ + } switch (protocol) { - case 'postgres:': - return new PostgresStorageAdapter({ - uri: databaseURI, - collectionPrefix, - databaseOptions - }); - default: - return new MongoStorageAdapter({ - uri: databaseURI, - collectionPrefix, - mongoOptions: databaseOptions, - }); + case 'postgres:': + return new PostgresStorageAdapter({ + uri: databaseURI, + collectionPrefix, + databaseOptions, + }); + default: + return new MongoStorageAdapter({ + uri: databaseURI, + collectionPrefix, + mongoOptions: databaseOptions, + }); } } diff --git a/src/Controllers/types.js b/src/Controllers/types.js index 1e0c484ae0..a4419c5134 100644 --- a/src/Controllers/types.js +++ b/src/Controllers/types.js @@ -1,29 +1,29 @@ export type LoadSchemaOptions = { - clearCache: boolean + clearCache: boolean, }; export type SchemaField = { - type: string; - targetClass?: ?string; -} + type: string, + targetClass?: ?string, +}; -export type SchemaFields = { [string]: SchemaField } +export type SchemaFields = { [string]: SchemaField }; export type Schema = { className: string, fields: SchemaFields, classLevelPermissions: ClassLevelPermissions, - indexes?: ?any + indexes?: ?any, }; export type ClassLevelPermissions = { - find?: {[string]: boolean}; - count?: {[string]: boolean}; - get?: {[string]: boolean}; - create?: {[string]: boolean}; - update?: {[string]: boolean}; - delete?: {[string]: boolean}; - addField?: {[string]: boolean}; - readUserFields?: string[]; - writeUserFields?: string[]; + find?: { [string]: boolean }, + count?: { [string]: boolean }, + get?: { [string]: boolean }, + create?: { [string]: boolean }, + update?: { [string]: boolean }, + delete?: { [string]: boolean }, + addField?: { [string]: boolean }, + readUserFields?: string[], + writeUserFields?: string[], }; diff --git a/src/LiveQuery/Client.js b/src/LiveQuery/Client.js index 4c88635d26..867a98e7a6 100644 --- a/src/LiveQuery/Client.js +++ b/src/LiveQuery/Client.js @@ -3,7 +3,13 @@ import logger from '../logger'; import type { FlattenedObjectData } from './Subscription'; export type Message = { [attr: string]: any }; -const dafaultFields = ['className', 'objectId', 'updatedAt', 'createdAt', 'ACL']; +const dafaultFields = [ + 'className', + 'objectId', + 'updatedAt', + 'createdAt', + 'ACL', +]; class Client { id: number; @@ -42,13 +48,21 @@ class Client { parseWebSocket.send(message); } - static pushError(parseWebSocket: any, code: number, error: string, reconnect: boolean = true): void { - Client.pushResponse(parseWebSocket, JSON.stringify({ - 'op': 'error', - 'error': error, - 'code': code, - 'reconnect': reconnect - })); + static pushError( + parseWebSocket: any, + code: number, + error: string, + reconnect: boolean = true + ): void { + Client.pushResponse( + parseWebSocket, + JSON.stringify({ + op: 'error', + error: error, + code: code, + reconnect: reconnect, + }) + ); } addSubscriptionInfo(requestId: number, subscriptionInfo: any): void { @@ -66,8 +80,8 @@ class Client { _pushEvent(type: string): Function { return function(subscriptionId: number, parseObjectJSON: any): void { const response: Message = { - 'op' : type, - 'clientId' : this.id + op: type, + clientId: this.id, }; if (typeof subscriptionId !== 'undefined') { response['requestId'] = subscriptionId; @@ -80,7 +94,7 @@ class Client { response['object'] = this._toJSONWithFields(parseObjectJSON, fields); } Client.pushResponse(this.parseWebSocket, JSON.stringify(response)); - } + }; } _toJSONWithFields(parseObjectJSON: any, fields: any): FlattenedObjectData { @@ -100,6 +114,4 @@ class Client { } } -export { - Client -} +export { Client }; diff --git a/src/LiveQuery/ParseCloudCodePublisher.js b/src/LiveQuery/ParseCloudCodePublisher.js index 5b1645bbe1..85e95121fb 100644 --- a/src/LiveQuery/ParseCloudCodePublisher.js +++ b/src/LiveQuery/ParseCloudCodePublisher.js @@ -1,5 +1,5 @@ import { ParsePubSub } from './ParsePubSub'; -import Parse from 'parse/node'; +import Parse from 'parse/node'; import logger from '../logger'; class ParseCloudCodePublisher { @@ -21,11 +21,15 @@ class ParseCloudCodePublisher { // Request is the request object from cloud code functions. request.object is a ParseObject. _onCloudCodeMessage(type: string, request: any): void { - logger.verbose('Raw request from cloud code current : %j | original : %j', request.object, request.original); + logger.verbose( + 'Raw request from cloud code current : %j | original : %j', + request.object, + request.original + ); // We need the full JSON which includes className const message = { - currentParseObject: request.object._toFullJSON() - } + currentParseObject: request.object._toFullJSON(), + }; if (request.original) { message.originalParseObject = request.original._toFullJSON(); } @@ -33,6 +37,4 @@ class ParseCloudCodePublisher { } } -export { - ParseCloudCodePublisher -} +export { ParseCloudCodePublisher }; diff --git a/src/LiveQuery/ParseLiveQueryServer.js b/src/LiveQuery/ParseLiveQueryServer.js index fb89ca81b7..ea8d709e01 100644 --- a/src/LiveQuery/ParseLiveQueryServer.js +++ b/src/LiveQuery/ParseLiveQueryServer.js @@ -17,7 +17,7 @@ class ParseLiveQueryServer { // className -> (queryHash -> subscription) subscriptions: Object; parseWebSocketServer: Object; - keyPairs : any; + keyPairs: any; // The subscriber we use to get object update from publisher subscriber: Object; @@ -49,7 +49,7 @@ class ParseLiveQueryServer { // Initialize websocket server this.parseWebSocketServer = new ParseWebSocketServer( server, - (parseWebsocket) => this._onConnect(parseWebsocket), + parseWebsocket => this._onConnect(parseWebsocket), config.websocketTimeout ); @@ -64,7 +64,7 @@ class ParseLiveQueryServer { let message; try { message = JSON.parse(messageStr); - } catch(e) { + } catch (e) { logger.error('unable to parse message', messageStr, e); return; } @@ -74,7 +74,11 @@ class ParseLiveQueryServer { } else if (channel === Parse.applicationId + 'afterDelete') { this._onAfterDelete(message); } else { - logger.error('Get message %s from unknown channel %j', message, channel); + logger.error( + 'Get message %s from unknown channel %j', + message, + channel + ); } }); @@ -108,7 +112,11 @@ class ParseLiveQueryServer { const deletedParseObject = message.currentParseObject.toJSON(); const className = deletedParseObject.className; - logger.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id); + logger.verbose( + 'ClassName: %j | ObjectId: %s', + className, + deletedParseObject.id + ); logger.verbose('Current client number : %d', this.clients.size); const classSubscriptions = this.subscriptions.get(className); @@ -117,11 +125,16 @@ class ParseLiveQueryServer { return; } for (const subscription of classSubscriptions.values()) { - const isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription); + const isSubscriptionMatched = this._matchesSubscription( + deletedParseObject, + subscription + ); if (!isSubscriptionMatched) { continue; } - for (const [clientId, requestIds] of _.entries(subscription.clientRequestIds)) { + for (const [clientId, requestIds] of _.entries( + subscription.clientRequestIds + )) { const client = this.clients.get(clientId); if (typeof client === 'undefined') { continue; @@ -129,14 +142,17 @@ class ParseLiveQueryServer { for (const requestId of requestIds) { const acl = message.currentParseObject.getACL(); // Check ACL - this._matchesACL(acl, client, requestId).then((isMatched) => { - if (!isMatched) { - return null; + this._matchesACL(acl, client, requestId).then( + isMatched => { + if (!isMatched) { + return null; + } + client.pushDelete(requestId, deletedParseObject); + }, + error => { + logger.error('Matching ACL error : ', error); } - client.pushDelete(requestId, deletedParseObject); - }, (error) => { - logger.error('Matching ACL error : ', error); - }); + ); } } } @@ -153,7 +169,11 @@ class ParseLiveQueryServer { } const currentParseObject = message.currentParseObject.toJSON(); const className = currentParseObject.className; - logger.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id); + logger.verbose( + 'ClassName: %s | ObjectId: %s', + className, + currentParseObject.id + ); logger.verbose('Current client number : %d', this.clients.size); const classSubscriptions = this.subscriptions.get(className); @@ -162,9 +182,17 @@ class ParseLiveQueryServer { return; } for (const subscription of classSubscriptions.values()) { - const isOriginalSubscriptionMatched = this._matchesSubscription(originalParseObject, subscription); - const isCurrentSubscriptionMatched = this._matchesSubscription(currentParseObject, subscription); - for (const [clientId, requestIds] of _.entries(subscription.clientRequestIds)) { + const isOriginalSubscriptionMatched = this._matchesSubscription( + originalParseObject, + subscription + ); + const isCurrentSubscriptionMatched = this._matchesSubscription( + currentParseObject, + subscription + ); + for (const [clientId, requestIds] of _.entries( + subscription.clientRequestIds + )) { const client = this.clients.get(clientId); if (typeof client === 'undefined') { continue; @@ -180,7 +208,11 @@ class ParseLiveQueryServer { if (message.originalParseObject) { originalACL = message.originalParseObject.getACL(); } - originalACLCheckingPromise = this._matchesACL(originalACL, client, requestId); + originalACLCheckingPromise = this._matchesACL( + originalACL, + client, + requestId + ); } // Set current ParseObject ACL checking promise, if the object does not match // subscription, we do not need to check ACL @@ -189,56 +221,62 @@ class ParseLiveQueryServer { currentACLCheckingPromise = Promise.resolve(false); } else { const currentACL = message.currentParseObject.getACL(); - currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId); - } - - Promise.all( - [ - originalACLCheckingPromise, - currentACLCheckingPromise - ] - ).then(([isOriginalMatched, isCurrentMatched]) => { - logger.verbose('Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', - originalParseObject, - currentParseObject, - isOriginalSubscriptionMatched, - isCurrentSubscriptionMatched, - isOriginalMatched, - isCurrentMatched, - subscription.hash + currentACLCheckingPromise = this._matchesACL( + currentACL, + client, + requestId ); + } - // Decide event type - let type; - if (isOriginalMatched && isCurrentMatched) { - type = 'Update'; - } else if (isOriginalMatched && !isCurrentMatched) { - type = 'Leave'; - } else if (!isOriginalMatched && isCurrentMatched) { - if (originalParseObject) { - type = 'Enter'; + Promise.all([ + originalACLCheckingPromise, + currentACLCheckingPromise, + ]).then( + ([isOriginalMatched, isCurrentMatched]) => { + logger.verbose( + 'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', + originalParseObject, + currentParseObject, + isOriginalSubscriptionMatched, + isCurrentSubscriptionMatched, + isOriginalMatched, + isCurrentMatched, + subscription.hash + ); + + // Decide event type + let type; + if (isOriginalMatched && isCurrentMatched) { + type = 'Update'; + } else if (isOriginalMatched && !isCurrentMatched) { + type = 'Leave'; + } else if (!isOriginalMatched && isCurrentMatched) { + if (originalParseObject) { + type = 'Enter'; + } else { + type = 'Create'; + } } else { - type = 'Create'; + return null; } - } else { - return null; + const functionName = 'push' + type; + client[functionName](requestId, currentParseObject); + }, + error => { + logger.error('Matching ACL error : ', error); } - const functionName = 'push' + type; - client[functionName](requestId, currentParseObject); - }, (error) => { - logger.error('Matching ACL error : ', error); - }); + ); } } } } _onConnect(parseWebsocket: any): void { - parseWebsocket.on('message', (request) => { + parseWebsocket.on('message', request => { if (typeof request === 'string') { try { request = JSON.parse(request); - } catch(e) { + } catch (e) { logger.error('unable to parse request', request, e); return; } @@ -246,28 +284,31 @@ class ParseLiveQueryServer { logger.verbose('Request: %j', request); // Check whether this request is a valid request, return error directly if not - if (!tv4.validate(request, RequestSchema['general']) || !tv4.validate(request, RequestSchema[request.op])) { + if ( + !tv4.validate(request, RequestSchema['general']) || + !tv4.validate(request, RequestSchema[request.op]) + ) { Client.pushError(parseWebsocket, 1, tv4.error.message); logger.error('Connect message error %s', tv4.error.message); return; } - switch(request.op) { - case 'connect': - this._handleConnect(parseWebsocket, request); - break; - case 'subscribe': - this._handleSubscribe(parseWebsocket, request); - break; - case 'update': - this._handleUpdateSubscription(parseWebsocket, request); - break; - case 'unsubscribe': - this._handleUnsubscribe(parseWebsocket, request); - break; - default: - Client.pushError(parseWebsocket, 3, 'Get unknown operation'); - logger.error('Get unknown operation', request.op); + switch (request.op) { + case 'connect': + this._handleConnect(parseWebsocket, request); + break; + case 'subscribe': + this._handleSubscribe(parseWebsocket, request); + break; + case 'update': + this._handleUpdateSubscription(parseWebsocket, request); + break; + case 'unsubscribe': + this._handleUnsubscribe(parseWebsocket, request); + break; + default: + Client.pushError(parseWebsocket, 3, 'Get unknown operation'); + logger.error('Get unknown operation', request.op); } }); @@ -279,7 +320,7 @@ class ParseLiveQueryServer { event: 'ws_disconnect_error', clients: this.clients.size, subscriptions: this.subscriptions.size, - error: `Unable to find client ${clientId}` + error: `Unable to find client ${clientId}`, }); logger.error(`Can not find client ${clientId} on disconnect`); return; @@ -290,12 +331,16 @@ class ParseLiveQueryServer { this.clients.delete(clientId); // Delete client from subscriptions - for (const [requestId, subscriptionInfo] of _.entries(client.subscriptionInfos)) { + for (const [requestId, subscriptionInfo] of _.entries( + client.subscriptionInfos + )) { const subscription = subscriptionInfo.subscription; subscription.deleteClientSubscription(clientId, requestId); // If there is no client which is subscribing this subscription, remove it from subscriptions - const classSubscriptions = this.subscriptions.get(subscription.className); + const classSubscriptions = this.subscriptions.get( + subscription.className + ); if (!subscription.hasSubscribingClient()) { classSubscriptions.delete(subscription.hash); } @@ -310,14 +355,14 @@ class ParseLiveQueryServer { runLiveQueryEventHandlers({ event: 'ws_disconnect', clients: this.clients.size, - subscriptions: this.subscriptions.size + subscriptions: this.subscriptions.size, }); }); runLiveQueryEventHandlers({ event: 'ws_connect', clients: this.clients.size, - subscriptions: this.subscriptions.size + subscriptions: this.subscriptions.size, }); } @@ -341,80 +386,86 @@ class ParseLiveQueryServer { } const subscriptionSessionToken = subscriptionInfo.sessionToken; - return this.sessionTokenCache.getUserId(subscriptionSessionToken).then((userId) => { - return acl.getReadAccess(userId); - }).then((isSubscriptionSessionTokenMatched) => { - if (isSubscriptionSessionTokenMatched) { - return Promise.resolve(true); - } - - // Check if the user has any roles that match the ACL - return new Promise((resolve, reject) => { - - // Resolve false right away if the acl doesn't have any roles - const acl_has_roles = Object.keys(acl.permissionsById).some(key => key.startsWith("role:")); - if (!acl_has_roles) { - return resolve(false); + return this.sessionTokenCache + .getUserId(subscriptionSessionToken) + .then(userId => { + return acl.getReadAccess(userId); + }) + .then(isSubscriptionSessionTokenMatched => { + if (isSubscriptionSessionTokenMatched) { + return Promise.resolve(true); } - this.sessionTokenCache.getUserId(subscriptionSessionToken) - .then((userId) => { - - // Pass along a null if there is no user id - if (!userId) { - return Promise.resolve(null); - } - - // Prepare a user object to query for roles - // To eliminate a query for the user, create one locally with the id - var user = new Parse.User(); - user.id = userId; - return user; - - }) - .then((user) => { - - // Pass along an empty array (of roles) if no user - if (!user) { - return Promise.resolve([]); - } + // Check if the user has any roles that match the ACL + return new Promise((resolve, reject) => { + // Resolve false right away if the acl doesn't have any roles + const acl_has_roles = Object.keys(acl.permissionsById).some(key => + key.startsWith('role:') + ); + if (!acl_has_roles) { + return resolve(false); + } - // Then get the user's roles - var rolesQuery = new Parse.Query(Parse.Role); - rolesQuery.equalTo("users", user); - return rolesQuery.find({useMasterKey:true}); - }). - then((roles) => { - - // Finally, see if any of the user's roles allow them read access - for (const role of roles) { - if (acl.getRoleReadAccess(role)) { - return resolve(true); + this.sessionTokenCache + .getUserId(subscriptionSessionToken) + .then(userId => { + // Pass along a null if there is no user id + if (!userId) { + return Promise.resolve(null); } - } - resolve(false); - }) - .catch((error) => { - reject(error); - }); - }); - }).then((isRoleMatched) => { + // Prepare a user object to query for roles + // To eliminate a query for the user, create one locally with the id + var user = new Parse.User(); + user.id = userId; + return user; + }) + .then(user => { + // Pass along an empty array (of roles) if no user + if (!user) { + return Promise.resolve([]); + } - if(isRoleMatched) { - return Promise.resolve(true); - } + // Then get the user's roles + var rolesQuery = new Parse.Query(Parse.Role); + rolesQuery.equalTo('users', user); + return rolesQuery.find({ useMasterKey: true }); + }) + .then(roles => { + // Finally, see if any of the user's roles allow them read access + for (const role of roles) { + if (acl.getRoleReadAccess(role)) { + return resolve(true); + } + } + resolve(false); + }) + .catch(error => { + reject(error); + }); + }); + }) + .then(isRoleMatched => { + if (isRoleMatched) { + return Promise.resolve(true); + } - // Check client sessionToken matches ACL - const clientSessionToken = client.sessionToken; - return this.sessionTokenCache.getUserId(clientSessionToken).then((userId) => { - return acl.getReadAccess(userId); - }); - }).then((isMatched) => { - return Promise.resolve(isMatched); - }, () => { - return Promise.resolve(false); - }); + // Check client sessionToken matches ACL + const clientSessionToken = client.sessionToken; + return this.sessionTokenCache + .getUserId(clientSessionToken) + .then(userId => { + return acl.getReadAccess(userId); + }); + }) + .then( + isMatched => { + return Promise.resolve(isMatched); + }, + () => { + return Promise.resolve(false); + } + ); } _handleConnect(parseWebsocket: any, request: any): any { @@ -433,19 +484,22 @@ class ParseLiveQueryServer { runLiveQueryEventHandlers({ event: 'connect', clients: this.clients.size, - subscriptions: this.subscriptions.size + subscriptions: this.subscriptions.size, }); } _hasMasterKey(request: any, validKeyPairs: any): boolean { - if(!validKeyPairs || validKeyPairs.size == 0 || - !validKeyPairs.has("masterKey")) { + if ( + !validKeyPairs || + validKeyPairs.size == 0 || + !validKeyPairs.has('masterKey') + ) { return false; } - if(!request || !request.hasOwnProperty("masterKey")) { + if (!request || !request.hasOwnProperty('masterKey')) { return false; } - return request.masterKey === validKeyPairs.get("masterKey"); + return request.masterKey === validKeyPairs.get('masterKey'); } _validateKeys(request: any, validKeyPairs: any): boolean { @@ -466,8 +520,14 @@ class ParseLiveQueryServer { _handleSubscribe(parseWebsocket: any, request: any): any { // If we can not find this client, return error to client if (!parseWebsocket.hasOwnProperty('clientId')) { - Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before subscribing'); - logger.error('Can not find this client, make sure you connect to server before subscribing'); + Client.pushError( + parseWebsocket, + 2, + 'Can not find this client, make sure you connect to server before subscribing' + ); + logger.error( + 'Can not find this client, make sure you connect to server before subscribing' + ); return; } const client = this.clients.get(parseWebsocket.clientId); @@ -484,13 +544,17 @@ class ParseLiveQueryServer { if (classSubscriptions.has(subscriptionHash)) { subscription = classSubscriptions.get(subscriptionHash); } else { - subscription = new Subscription(className, request.query.where, subscriptionHash); + subscription = new Subscription( + className, + request.query.where, + subscriptionHash + ); classSubscriptions.set(subscriptionHash, subscription); } // Add subscriptionInfo to client const subscriptionInfo = { - subscription: subscription + subscription: subscription, }; // Add selected fields and sessionToken for this subscription if necessary if (request.query.fields) { @@ -502,16 +566,23 @@ class ParseLiveQueryServer { client.addSubscriptionInfo(request.requestId, subscriptionInfo); // Add clientId to subscription - subscription.addClientSubscription(parseWebsocket.clientId, request.requestId); + subscription.addClientSubscription( + parseWebsocket.clientId, + request.requestId + ); client.pushSubscribe(request.requestId); - logger.verbose(`Create client ${parseWebsocket.clientId} new subscription: ${request.requestId}`); + logger.verbose( + `Create client ${parseWebsocket.clientId} new subscription: ${ + request.requestId + }` + ); logger.verbose('Current client number: %d', this.clients.size); runLiveQueryEventHandlers({ event: 'subscribe', clients: this.clients.size, - subscriptions: this.subscriptions.size + subscriptions: this.subscriptions.size, }); } @@ -520,27 +591,54 @@ class ParseLiveQueryServer { this._handleSubscribe(parseWebsocket, request); } - _handleUnsubscribe(parseWebsocket: any, request: any, notifyClient: bool = true): any { + _handleUnsubscribe( + parseWebsocket: any, + request: any, + notifyClient: boolean = true + ): any { // If we can not find this client, return error to client if (!parseWebsocket.hasOwnProperty('clientId')) { - Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before unsubscribing'); - logger.error('Can not find this client, make sure you connect to server before unsubscribing'); + Client.pushError( + parseWebsocket, + 2, + 'Can not find this client, make sure you connect to server before unsubscribing' + ); + logger.error( + 'Can not find this client, make sure you connect to server before unsubscribing' + ); return; } const requestId = request.requestId; const client = this.clients.get(parseWebsocket.clientId); if (typeof client === 'undefined') { - Client.pushError(parseWebsocket, 2, 'Cannot find client with clientId ' + parseWebsocket.clientId + - '. Make sure you connect to live query server before unsubscribing.'); + Client.pushError( + parseWebsocket, + 2, + 'Cannot find client with clientId ' + + parseWebsocket.clientId + + '. Make sure you connect to live query server before unsubscribing.' + ); logger.error('Can not find this client ' + parseWebsocket.clientId); return; } const subscriptionInfo = client.getSubscriptionInfo(requestId); if (typeof subscriptionInfo === 'undefined') { - Client.pushError(parseWebsocket, 2, 'Cannot find subscription with clientId ' + parseWebsocket.clientId + - ' subscriptionId ' + requestId + '. Make sure you subscribe to live query server before unsubscribing.'); - logger.error('Can not find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId); + Client.pushError( + parseWebsocket, + 2, + 'Cannot find subscription with clientId ' + + parseWebsocket.clientId + + ' subscriptionId ' + + requestId + + '. Make sure you subscribe to live query server before unsubscribing.' + ); + logger.error( + 'Can not find subscription with clientId ' + + parseWebsocket.clientId + + ' subscriptionId ' + + requestId + ); return; } @@ -562,7 +660,7 @@ class ParseLiveQueryServer { runLiveQueryEventHandlers({ event: 'unsubscribe', clients: this.clients.size, - subscriptions: this.subscriptions.size + subscriptions: this.subscriptions.size, }); if (!notifyClient) { @@ -571,10 +669,12 @@ class ParseLiveQueryServer { client.pushUnsubscribe(request.requestId); - logger.verbose(`Delete client: ${parseWebsocket.clientId} | subscription: ${request.requestId}`); + logger.verbose( + `Delete client: ${parseWebsocket.clientId} | subscription: ${ + request.requestId + }` + ); } } -export { - ParseLiveQueryServer -} +export { ParseLiveQueryServer }; diff --git a/src/LiveQuery/ParsePubSub.js b/src/LiveQuery/ParsePubSub.js index ff321889a1..59639ea378 100644 --- a/src/LiveQuery/ParsePubSub.js +++ b/src/LiveQuery/ParsePubSub.js @@ -1,11 +1,7 @@ import { loadAdapter } from '../Adapters/AdapterLoader'; -import { - EventEmitterPubSub -} from '../Adapters/PubSub/EventEmitterPubSub'; +import { EventEmitterPubSub } from '../Adapters/PubSub/EventEmitterPubSub'; -import { - RedisPubSub -} from '../Adapters/PubSub/RedisPubSub'; +import { RedisPubSub } from '../Adapters/PubSub/RedisPubSub'; const ParsePubSub = {}; @@ -18,26 +14,32 @@ ParsePubSub.createPublisher = function(config: any): any { if (useRedis(config)) { return RedisPubSub.createPublisher(config); } else { - const adapter = loadAdapter(config.pubSubAdapter, EventEmitterPubSub, config) + const adapter = loadAdapter( + config.pubSubAdapter, + EventEmitterPubSub, + config + ); if (typeof adapter.createPublisher !== 'function') { throw 'pubSubAdapter should have createPublisher()'; } return adapter.createPublisher(config); } -} +}; ParsePubSub.createSubscriber = function(config: any): void { if (useRedis(config)) { return RedisPubSub.createSubscriber(config); } else { - const adapter = loadAdapter(config.pubSubAdapter, EventEmitterPubSub, config) + const adapter = loadAdapter( + config.pubSubAdapter, + EventEmitterPubSub, + config + ); if (typeof adapter.createSubscriber !== 'function') { throw 'pubSubAdapter should have createSubscriber()'; } return adapter.createSubscriber(config); } -} +}; -export { - ParsePubSub -} +export { ParsePubSub }; diff --git a/src/LiveQuery/ParseWebSocketServer.js b/src/LiveQuery/ParseWebSocketServer.js index c76c36f752..1ec05c5875 100644 --- a/src/LiveQuery/ParseWebSocketServer.js +++ b/src/LiveQuery/ParseWebSocketServer.js @@ -4,21 +4,25 @@ const typeMap = new Map([['disconnect', 'close']]); const getWS = function() { try { return require('uws'); - } catch(e) { + } catch (e) { return require('ws'); } -} +}; export class ParseWebSocketServer { server: Object; - constructor(server: any, onConnect: Function, websocketTimeout: number = 10 * 1000) { + constructor( + server: any, + onConnect: Function, + websocketTimeout: number = 10 * 1000 + ) { const WebSocketServer = getWS().Server; const wss = new WebSocketServer({ server: server }); wss.on('listening', () => { logger.info('Parse LiveQuery Server starts running'); }); - wss.on('connection', (ws) => { + wss.on('connection', ws => { onConnect(new ParseWebSocket(ws)); // Send ping to client periodically const pingIntervalId = setInterval(() => { diff --git a/src/LiveQuery/QueryTools.js b/src/LiveQuery/QueryTools.js index 5a42b9d824..1a1282940a 100644 --- a/src/LiveQuery/QueryTools.js +++ b/src/LiveQuery/QueryTools.js @@ -55,8 +55,8 @@ function queryHash(query) { if (query instanceof Parse.Query) { query = { className: query.className, - where: query._where - } + where: query._where, + }; } var where = flattenOrQueries(query.where || {}); var columns = []; @@ -99,8 +99,10 @@ function contains(haystack: Array, needle: any): boolean { if (typeof ptr === 'string' && ptr === needle.objectId) { return true; } - if (ptr.className === needle.className && - ptr.objectId === needle.objectId) { + if ( + ptr.className === needle.className && + ptr.objectId === needle.objectId + ) { return true; } } @@ -117,7 +119,7 @@ function contains(haystack: Array, needle: any): boolean { function matchesQuery(object: any, query: any): boolean { if (query instanceof Parse.Query) { var className = - (object.id instanceof Id) ? object.id.className : object.className; + object.id instanceof Id ? object.id.className : object.className; if (className !== query.className) { return false; } @@ -144,7 +146,6 @@ function equalObjectsGeneric(obj, compareTo, eqlFn) { return eqlFn(obj, compareTo); } - /** * Determines whether an object matches a single key's constraints */ @@ -152,12 +153,16 @@ function matchesKeyConstraints(object, key, constraints) { if (constraints === null) { return false; } - if(key.indexOf(".") >= 0){ + if (key.indexOf('.') >= 0) { // Key references a subobject - var keyComponents = key.split("."); + var keyComponents = key.split('.'); var subObjectKey = keyComponents[0]; - var keyRemainder = keyComponents.slice(1).join("."); - return matchesKeyConstraints(object[subObjectKey] || {}, keyRemainder, constraints); + var keyRemainder = keyComponents.slice(1).join('.'); + return matchesKeyConstraints( + object[subObjectKey] || {}, + keyRemainder, + constraints + ); } var i; if (key === '$or') { @@ -191,7 +196,11 @@ function matchesKeyConstraints(object, key, constraints) { }); } - return equalObjectsGeneric(object[key], Parse._decode(key, constraints), equalObjects); + return equalObjectsGeneric( + object[key], + Parse._decode(key, constraints), + equalObjects + ); } // More complex cases for (var condition in constraints) { @@ -200,124 +209,131 @@ function matchesKeyConstraints(object, key, constraints) { compareTo = Parse._decode(key, compareTo); } switch (condition) { - case '$lt': - if (object[key] >= compareTo) { - return false; - } - break; - case '$lte': - if (object[key] > compareTo) { - return false; - } - break; - case '$gt': - if (object[key] <= compareTo) { - return false; - } - break; - case '$gte': - if (object[key] < compareTo) { - return false; - } - break; - case '$ne': - if (equalObjects(object[key], compareTo)) { - return false; - } - break; - case '$in': - if (!contains(compareTo, object[key])) { - return false; - } - break; - case '$nin': - if (contains(compareTo, object[key])) { - return false; - } - break; - case '$all': - for (i = 0; i < compareTo.length; i++) { - if (object[key].indexOf(compareTo[i]) < 0) { + case '$lt': + if (object[key] >= compareTo) { + return false; + } + break; + case '$lte': + if (object[key] > compareTo) { + return false; + } + break; + case '$gt': + if (object[key] <= compareTo) { + return false; + } + break; + case '$gte': + if (object[key] < compareTo) { + return false; + } + break; + case '$ne': + if (equalObjects(object[key], compareTo)) { + return false; + } + break; + case '$in': + if (!contains(compareTo, object[key])) { + return false; + } + break; + case '$nin': + if (contains(compareTo, object[key])) { + return false; + } + break; + case '$all': + for (i = 0; i < compareTo.length; i++) { + if (object[key].indexOf(compareTo[i]) < 0) { + return false; + } + } + break; + case '$exists': { + const propertyExists = typeof object[key] !== 'undefined'; + const 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; - case '$exists': { - const propertyExists = typeof object[key] !== 'undefined'; - const 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; - } - case '$regex': - if (typeof compareTo === 'object') { - return compareTo.test(object[key]); - } - // JS doesn't support perl-style escaping - var expString = ''; - var escapeEnd = -2; - var escapeStart = compareTo.indexOf('\\Q'); - while (escapeStart > -1) { - // Add the unescaped portion - expString += compareTo.substring(escapeEnd + 2, escapeStart); - escapeEnd = compareTo.indexOf('\\E', escapeStart); - if (escapeEnd > -1) { - expString += compareTo.substring(escapeStart + 2, escapeEnd) - .replace(/\\\\\\\\E/g, '\\E').replace(/\W/g, '\\$&'); + case '$regex': + if (typeof compareTo === 'object') { + return compareTo.test(object[key]); } + // JS doesn't support perl-style escaping + var expString = ''; + var escapeEnd = -2; + var escapeStart = compareTo.indexOf('\\Q'); + while (escapeStart > -1) { + // Add the unescaped portion + expString += compareTo.substring(escapeEnd + 2, escapeStart); + escapeEnd = compareTo.indexOf('\\E', escapeStart); + if (escapeEnd > -1) { + expString += compareTo + .substring(escapeStart + 2, escapeEnd) + .replace(/\\\\\\\\E/g, '\\E') + .replace(/\W/g, '\\$&'); + } - escapeStart = compareTo.indexOf('\\Q', escapeEnd); - } - expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2)); - var exp = new RegExp(expString, constraints.$options || ''); - if (!exp.test(object[key])) { - return false; - } - break; - case '$nearSphere': - if (!compareTo || !object[key]) { - return false; - } - var distance = compareTo.radiansTo(object[key]); - var max = constraints.$maxDistance || Infinity; - return distance <= max; - case '$within': - if (!compareTo || !object[key]) { - return false; - } - var southWest = compareTo.$box[0]; - var northEast = compareTo.$box[1]; - if (southWest.latitude > northEast.latitude || - southWest.longitude > northEast.longitude) { - // Invalid box, crosses the date line - return false; - } - return ( - object[key].latitude > southWest.latitude && + escapeStart = compareTo.indexOf('\\Q', escapeEnd); + } + expString += compareTo.substring(Math.max(escapeStart, escapeEnd + 2)); + var exp = new RegExp(expString, constraints.$options || ''); + if (!exp.test(object[key])) { + return false; + } + break; + case '$nearSphere': + if (!compareTo || !object[key]) { + return false; + } + var distance = compareTo.radiansTo(object[key]); + var max = constraints.$maxDistance || Infinity; + return distance <= max; + case '$within': + if (!compareTo || !object[key]) { + return false; + } + var southWest = compareTo.$box[0]; + var northEast = compareTo.$box[1]; + if ( + southWest.latitude > northEast.latitude || + southWest.longitude > northEast.longitude + ) { + // Invalid box, crosses the date line + return false; + } + return ( + object[key].latitude > southWest.latitude && object[key].latitude < northEast.latitude && object[key].longitude > southWest.longitude && object[key].longitude < northEast.longitude - ); - case '$options': - // Not a query type, but a way to add options to $regex. Ignore and - // avoid the default - break; - case '$maxDistance': - // Not a query type, but a way to add a cap to $nearSphere. Ignore and - // avoid the default - break; - case '$select': - return false; - case '$dontSelect': - return false; - default: - return false; + ); + case '$options': + // Not a query type, but a way to add options to $regex. Ignore and + // avoid the default + break; + case '$maxDistance': + // Not a query type, but a way to add a cap to $nearSphere. Ignore and + // avoid the default + break; + case '$select': + return false; + case '$dontSelect': + return false; + default: + return false; } } return true; @@ -325,7 +341,7 @@ function matchesKeyConstraints(object, key, constraints) { var QueryTools = { queryHash: queryHash, - matchesQuery: matchesQuery + matchesQuery: matchesQuery, }; module.exports = QueryTools; diff --git a/src/LiveQuery/RequestSchema.js b/src/LiveQuery/RequestSchema.js index 37fec6f2d8..7603fbf9af 100644 --- a/src/LiveQuery/RequestSchema.js +++ b/src/LiveQuery/RequestSchema.js @@ -1,141 +1,141 @@ const general = { - 'title': 'General request schema', - 'type': 'object', - 'properties': { - 'op': { - 'type': 'string', - 'enum': ['connect', 'subscribe', 'unsubscribe', 'update'] + title: 'General request schema', + type: 'object', + properties: { + op: { + type: 'string', + enum: ['connect', 'subscribe', 'unsubscribe', 'update'], }, }, - 'required': ['op'] + required: ['op'], }; -const connect = { - 'title': 'Connect operation schema', - 'type': 'object', - 'properties': { - 'op': 'connect', - 'applicationId': { - 'type': 'string' +const connect = { + title: 'Connect operation schema', + type: 'object', + properties: { + op: 'connect', + applicationId: { + type: 'string', }, - 'javascriptKey': { - type: 'string' + javascriptKey: { + type: 'string', }, - 'masterKey': { - type: 'string' + masterKey: { + type: 'string', }, - 'clientKey': { - type: 'string' + clientKey: { + type: 'string', }, - 'windowsKey': { - type: 'string' + windowsKey: { + type: 'string', }, - 'restAPIKey': { - 'type': 'string' + restAPIKey: { + type: 'string', + }, + sessionToken: { + type: 'string', }, - 'sessionToken': { - 'type': 'string' - } }, - 'required': ['op', 'applicationId'], - "additionalProperties": false + required: ['op', 'applicationId'], + additionalProperties: false, }; const subscribe = { - 'title': 'Subscribe operation schema', - 'type': 'object', - 'properties': { - 'op': 'subscribe', - 'requestId': { - 'type': 'number' + title: 'Subscribe operation schema', + type: 'object', + properties: { + op: 'subscribe', + requestId: { + type: 'number', }, - 'query': { - 'title': 'Query field schema', - 'type': 'object', - 'properties': { - 'className': { - 'type': 'string' + query: { + title: 'Query field schema', + type: 'object', + properties: { + className: { + type: 'string', }, - 'where': { - 'type': 'object' + where: { + type: 'object', }, - 'fields': { - "type": "array", - "items": { - "type": "string" + fields: { + type: 'array', + items: { + type: 'string', }, - "minItems": 1, - "uniqueItems": true - } + minItems: 1, + uniqueItems: true, + }, }, - 'required': ['where', 'className'], - 'additionalProperties': false + required: ['where', 'className'], + additionalProperties: false, + }, + sessionToken: { + type: 'string', }, - 'sessionToken': { - 'type': 'string' - } }, - 'required': ['op', 'requestId', 'query'], - 'additionalProperties': false + required: ['op', 'requestId', 'query'], + additionalProperties: false, }; const update = { - 'title': 'Update operation schema', - 'type': 'object', - 'properties': { - 'op': 'update', - 'requestId': { - 'type': 'number' + title: 'Update operation schema', + type: 'object', + properties: { + op: 'update', + requestId: { + type: 'number', }, - 'query': { - 'title': 'Query field schema', - 'type': 'object', - 'properties': { - 'className': { - 'type': 'string' + query: { + title: 'Query field schema', + type: 'object', + properties: { + className: { + type: 'string', }, - 'where': { - 'type': 'object' + where: { + type: 'object', }, - 'fields': { - "type": "array", - "items": { - "type": "string" + fields: { + type: 'array', + items: { + type: 'string', }, - "minItems": 1, - "uniqueItems": true - } + minItems: 1, + uniqueItems: true, + }, }, - 'required': ['where', 'className'], - 'additionalProperties': false + required: ['where', 'className'], + additionalProperties: false, + }, + sessionToken: { + type: 'string', }, - 'sessionToken': { - 'type': 'string' - } }, - 'required': ['op', 'requestId', 'query'], - 'additionalProperties': false + required: ['op', 'requestId', 'query'], + additionalProperties: false, }; const unsubscribe = { - 'title': 'Unsubscribe operation schema', - 'type': 'object', - 'properties': { - 'op': 'unsubscribe', - 'requestId': { - 'type': 'number' - } + title: 'Unsubscribe operation schema', + type: 'object', + properties: { + op: 'unsubscribe', + requestId: { + type: 'number', + }, }, - 'required': ['op', 'requestId'], - "additionalProperties": false -} + required: ['op', 'requestId'], + additionalProperties: false, +}; const RequestSchema = { - 'general': general, - 'connect': connect, - 'subscribe': subscribe, - 'update': update, - 'unsubscribe': unsubscribe -} + general: general, + connect: connect, + subscribe: subscribe, + update: update, + unsubscribe: unsubscribe, +}; export default RequestSchema; diff --git a/src/LiveQuery/SessionTokenCache.js b/src/LiveQuery/SessionTokenCache.js index 249f557956..282235ba88 100644 --- a/src/LiveQuery/SessionTokenCache.js +++ b/src/LiveQuery/SessionTokenCache.js @@ -2,24 +2,27 @@ import Parse from 'parse/node'; import LRU from 'lru-cache'; import logger from '../logger'; -function userForSessionToken(sessionToken){ - var q = new Parse.Query("_Session"); - q.equalTo("sessionToken", sessionToken); - return q.first({useMasterKey:true}).then(function(session){ - if(!session){ - return Promise.reject("No session found for session token"); +function userForSessionToken(sessionToken) { + var q = new Parse.Query('_Session'); + q.equalTo('sessionToken', sessionToken); + return q.first({ useMasterKey: true }).then(function(session) { + if (!session) { + return Promise.reject('No session found for session token'); } - return session.get("user"); + return session.get('user'); }); } class SessionTokenCache { cache: Object; - constructor(timeout: number = 30 * 24 * 60 * 60 * 1000, maxSize: number = 10000) { + constructor( + timeout: number = 30 * 24 * 60 * 60 * 1000, + maxSize: number = 10000 + ) { this.cache = new LRU({ max: maxSize, - maxAge: timeout + maxAge: timeout, }); } @@ -29,21 +32,34 @@ class SessionTokenCache { } const userId = this.cache.get(sessionToken); if (userId) { - logger.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken); + logger.verbose( + 'Fetch userId %s of sessionToken %s from Cache', + userId, + sessionToken + ); return Promise.resolve(userId); } - return userForSessionToken(sessionToken).then((user) => { - logger.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken); - const userId = user.id; - this.cache.set(sessionToken, userId); - return Promise.resolve(userId); - }, (error) => { - logger.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error); - return Promise.reject(error); - }); + return userForSessionToken(sessionToken).then( + user => { + logger.verbose( + 'Fetch userId %s of sessionToken %s from Parse', + user.id, + sessionToken + ); + const userId = user.id; + this.cache.set(sessionToken, userId); + return Promise.resolve(userId); + }, + error => { + logger.error( + 'Can not fetch userId for sessionToken %j, error %j', + sessionToken, + error + ); + return Promise.reject(error); + } + ); } } -export { - SessionTokenCache -} +export { SessionTokenCache }; diff --git a/src/LiveQuery/Subscription.js b/src/LiveQuery/Subscription.js index 53d3938748..7a88abc1b5 100644 --- a/src/LiveQuery/Subscription.js +++ b/src/LiveQuery/Subscription.js @@ -34,7 +34,11 @@ class Subscription { const index = requestIds.indexOf(requestId); if (index < 0) { - logger.error('Can not find client %d subscription %d to delete', clientId, requestId); + logger.error( + 'Can not find client %d subscription %d to delete', + clientId, + requestId + ); return; } requestIds.splice(index, 1); @@ -49,6 +53,4 @@ class Subscription { } } -export { - Subscription -} +export { Subscription }; diff --git a/src/LiveQuery/equalObjects.js b/src/LiveQuery/equalObjects.js index 931d392fd8..5bc3f5e957 100644 --- a/src/LiveQuery/equalObjects.js +++ b/src/LiveQuery/equalObjects.js @@ -9,14 +9,14 @@ function equalObjects(a, b) { return false; } if (typeof a !== 'object') { - return (a === b); + return a === b; } if (a === b) { return true; } if (toString.call(a) === '[object Date]') { if (toString.call(b) === '[object Date]') { - return (+a === +b); + return +a === +b; } return false; } diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js index a599344240..e484eba583 100644 --- a/src/Options/Definitions.js +++ b/src/Options/Definitions.js @@ -2,399 +2,421 @@ **** GENERATED CODE **** This code has been generated by resources/buildConfigDefinitions.js Do not edit manually, but update Options/index.js -*/"use strict"; +*/ 'use strict'; -var parsers = require("./parsers"); +var parsers = require('./parsers'); module.exports.ParseServerOptions = { - "appId": { - "env": "PARSE_SERVER_APPLICATION_ID", - "help": "Your Parse Application ID", - "required": true - }, - "masterKey": { - "env": "PARSE_SERVER_MASTER_KEY", - "help": "Your Parse Master Key", - "required": true - }, - "serverURL": { - "env": "PARSE_SERVER_URL", - "help": "URL to your parse server with http:// or https://.", - "required": true - }, - "masterKeyIps": { - "env": "PARSE_SERVER_MASTER_KEY_IPS", - "help": "Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)", - "action": parsers.arrayParser, - "default": [] - }, - "appName": { - "env": "PARSE_SERVER_APP_NAME", - "help": "Sets the app name" - }, - "analyticsAdapter": { - "env": "PARSE_SERVER_ANALYTICS_ADAPTER", - "help": "Adapter module for the analytics", - "action": parsers.moduleOrObjectParser - }, - "filesAdapter": { - "env": "PARSE_SERVER_FILES_ADAPTER", - "help": "Adapter module for the files sub-system", - "action": parsers.moduleOrObjectParser - }, - "push": { - "env": "PARSE_SERVER_PUSH", - "help": "Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications", - "action": parsers.objectParser - }, - "scheduledPush": { - "env": "PARSE_SERVER_SCHEDULED_PUSH", - "help": "Configuration for push scheduling, defaults to false.", - "action": parsers.booleanParser, - "default": false - }, - "loggerAdapter": { - "env": "PARSE_SERVER_LOGGER_ADAPTER", - "help": "Adapter module for the logging sub-system", - "action": parsers.moduleOrObjectParser - }, - "jsonLogs": { - "env": "JSON_LOGS", - "help": "Log as structured JSON objects", - "action": parsers.booleanParser - }, - "logsFolder": { - "env": "PARSE_SERVER_LOGS_FOLDER", - "help": "Folder for the logs (defaults to './logs'); set to null to disable file based logging", - "default": "./logs" - }, - "verbose": { - "env": "VERBOSE", - "help": "Set the logging to verbose", - "action": parsers.booleanParser - }, - "logLevel": { - "env": "PARSE_SERVER_LOG_LEVEL", - "help": "Sets the level for logs" - }, - "silent": { - "env": "SILENT", - "help": "Disables console output", - "action": parsers.booleanParser - }, - "databaseURI": { - "env": "PARSE_SERVER_DATABASE_URI", - "help": "The full URI to your database. Supported databases are mongodb or postgres.", - "required": true, - "default": "mongodb://localhost:27017/parse" - }, - "databaseOptions": { - "env": "PARSE_SERVER_DATABASE_OPTIONS", - "help": "Options to pass to the mongodb client", - "action": parsers.objectParser - }, - "databaseAdapter": { - "env": "PARSE_SERVER_DATABASE_ADAPTER", - "help": "Adapter module for the database", - "action": parsers.moduleOrObjectParser - }, - "cloud": { - "env": "PARSE_SERVER_CLOUD", - "help": "Full path to your cloud code main.js" - }, - "collectionPrefix": { - "env": "PARSE_SERVER_COLLECTION_PREFIX", - "help": "A collection prefix for the classes", - "default": "" - }, - "clientKey": { - "env": "PARSE_SERVER_CLIENT_KEY", - "help": "Key for iOS, MacOS, tvOS clients" - }, - "javascriptKey": { - "env": "PARSE_SERVER_JAVASCRIPT_KEY", - "help": "Key for the Javascript SDK" - }, - "dotNetKey": { - "env": "PARSE_SERVER_DOT_NET_KEY", - "help": "Key for Unity and .Net SDK" - }, - "restAPIKey": { - "env": "PARSE_SERVER_REST_API_KEY", - "help": "Key for REST calls" - }, - "readOnlyMasterKey": { - "env": "PARSE_SERVER_READ_ONLY_MASTER_KEY", - "help": "Read-only key, which has the same capabilities as MasterKey without writes" - }, - "webhookKey": { - "env": "PARSE_SERVER_WEBHOOK_KEY", - "help": "Key sent with outgoing webhook calls" - }, - "fileKey": { - "env": "PARSE_SERVER_FILE_KEY", - "help": "Key for your files" - }, - "preserveFileName": { - "env": "PARSE_SERVER_PRESERVE_FILE_NAME", - "help": "Enable (or disable) the addition of a unique hash to the file names", - "action": parsers.booleanParser, - "default": false - }, - "userSensitiveFields": { - "env": "PARSE_SERVER_USER_SENSITIVE_FIELDS", - "help": "Personally identifiable information fields in the user table the should be removed for non-authorized users.", - "action": parsers.arrayParser, - "default": ["email"] - }, - "enableAnonymousUsers": { - "env": "PARSE_SERVER_ENABLE_ANON_USERS", - "help": "Enable (or disable) anon users, defaults to true", - "action": parsers.booleanParser, - "default": true - }, - "allowClientClassCreation": { - "env": "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION", - "help": "Enable (or disable) client class creation, defaults to true", - "action": parsers.booleanParser, - "default": true - }, - "auth": { - "env": "PARSE_SERVER_AUTH_PROVIDERS", - "help": "Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication", - "action": parsers.objectParser - }, - "maxUploadSize": { - "env": "PARSE_SERVER_MAX_UPLOAD_SIZE", - "help": "Max file size for uploads, defaults to 20mb", - "default": "20mb" - }, - "verifyUserEmails": { - "env": "PARSE_SERVER_VERIFY_USER_EMAILS", - "help": "Enable (or disable) user email validation, defaults to false", - "action": parsers.booleanParser, - "default": false - }, - "preventLoginWithUnverifiedEmail": { - "env": "PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL", - "help": "Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false", - "action": parsers.booleanParser, - "default": false - }, - "emailVerifyTokenValidityDuration": { - "env": "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION", - "help": "Email verification token validity duration, in seconds", - "action": parsers.numberParser("emailVerifyTokenValidityDuration") - }, - "accountLockout": { - "env": "PARSE_SERVER_ACCOUNT_LOCKOUT", - "help": "account lockout policy for failed login attempts", - "action": parsers.objectParser - }, - "passwordPolicy": { - "env": "PARSE_SERVER_PASSWORD_POLICY", - "help": "Password policy for enforcing password related rules", - "action": parsers.objectParser - }, - "cacheAdapter": { - "env": "PARSE_SERVER_CACHE_ADAPTER", - "help": "Adapter module for the cache", - "action": parsers.moduleOrObjectParser - }, - "emailAdapter": { - "env": "PARSE_SERVER_EMAIL_ADAPTER", - "help": "Adapter module for email sending", - "action": parsers.moduleOrObjectParser - }, - "publicServerURL": { - "env": "PARSE_PUBLIC_SERVER_URL", - "help": "Public URL to your parse server with http:// or https://." - }, - "customPages": { - "env": "PARSE_SERVER_CUSTOM_PAGES", - "help": "custom pages for password validation and reset", - "action": parsers.objectParser, - "default": {} - }, - "liveQuery": { - "env": "PARSE_SERVER_LIVE_QUERY", - "help": "parse-server's LiveQuery configuration object", - "action": parsers.objectParser - }, - "sessionLength": { - "env": "PARSE_SERVER_SESSION_LENGTH", - "help": "Session duration, in seconds, defaults to 1 year", - "action": parsers.numberParser("sessionLength"), - "default": 31536000 - }, - "maxLimit": { - "env": "PARSE_SERVER_MAX_LIMIT", - "help": "Max value for limit option on queries, defaults to unlimited", - "action": parsers.numberParser("maxLimit") - }, - "expireInactiveSessions": { - "env": "PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS", - "help": "Sets wether we should expire the inactive sessions, defaults to true", - "action": parsers.booleanParser, - "default": true - }, - "revokeSessionOnPasswordReset": { - "env": "PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET", - "help": "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.", - "action": parsers.booleanParser, - "default": true - }, - "schemaCacheTTL": { - "env": "PARSE_SERVER_SCHEMA_CACHE_TTL", - "help": "The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.", - "action": parsers.numberParser("schemaCacheTTL"), - "default": 5000 - }, - "cacheTTL": { - "env": "PARSE_SERVER_CACHE_TTL", - "help": "Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)", - "action": parsers.numberParser("cacheTTL"), - "default": 5000 - }, - "cacheMaxSize": { - "env": "PARSE_SERVER_CACHE_MAX_SIZE", - "help": "Sets the maximum size for the in memory cache, defaults to 10000", - "action": parsers.numberParser("cacheMaxSize"), - "default": 10000 - }, - "enableSingleSchemaCache": { - "env": "PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE", - "help": "Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.", - "action": parsers.booleanParser, - "default": false - }, - "enableExpressErrorHandler": { - "env": "PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER", - "help": "Enables the default express error handler for all errors", - "action": parsers.booleanParser, - "default": false - }, - "objectIdSize": { - "env": "PARSE_SERVER_OBJECT_ID_SIZE", - "help": "Sets the number of characters in generated object id's, default 10", - "action": parsers.numberParser("objectIdSize"), - "default": 10 - }, - "port": { - "env": "PORT", - "help": "The port to run the ParseServer, defaults to 1337.", - "action": parsers.numberParser("port"), - "default": 1337 - }, - "host": { - "env": "PARSE_SERVER_HOST", - "help": "The host to serve ParseServer on, defaults to 0.0.0.0", - "default": "0.0.0.0" - }, - "mountPath": { - "env": "PARSE_SERVER_MOUNT_PATH", - "help": "Mount path for the server, defaults to /parse", - "default": "/parse" - }, - "cluster": { - "env": "PARSE_SERVER_CLUSTER", - "help": "Run with cluster, optionally set the number of processes default to os.cpus().length", - "action": parsers.numberOrBooleanParser - }, - "middleware": { - "env": "PARSE_SERVER_MIDDLEWARE", - "help": "middleware for express server, can be string or function" - }, - "startLiveQueryServer": { - "env": "PARSE_SERVER_START_LIVE_QUERY_SERVER", - "help": "Starts the liveQuery server", - "action": parsers.booleanParser - }, - "liveQueryServerOptions": { - "env": "PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS", - "help": "Live query server configuration options (will start the liveQuery server)", - "action": parsers.objectParser - } + appId: { + env: 'PARSE_SERVER_APPLICATION_ID', + help: 'Your Parse Application ID', + required: true, + }, + masterKey: { + env: 'PARSE_SERVER_MASTER_KEY', + help: 'Your Parse Master Key', + required: true, + }, + serverURL: { + env: 'PARSE_SERVER_URL', + help: 'URL to your parse server with http:// or https://.', + required: true, + }, + masterKeyIps: { + env: 'PARSE_SERVER_MASTER_KEY_IPS', + help: + 'Restrict masterKey to be used by only these ips, defaults to [] (allow all ips)', + action: parsers.arrayParser, + default: [], + }, + appName: { + env: 'PARSE_SERVER_APP_NAME', + help: 'Sets the app name', + }, + analyticsAdapter: { + env: 'PARSE_SERVER_ANALYTICS_ADAPTER', + help: 'Adapter module for the analytics', + action: parsers.moduleOrObjectParser, + }, + filesAdapter: { + env: 'PARSE_SERVER_FILES_ADAPTER', + help: 'Adapter module for the files sub-system', + action: parsers.moduleOrObjectParser, + }, + push: { + env: 'PARSE_SERVER_PUSH', + help: + 'Configuration for push, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#push-notifications', + action: parsers.objectParser, + }, + scheduledPush: { + env: 'PARSE_SERVER_SCHEDULED_PUSH', + help: 'Configuration for push scheduling, defaults to false.', + action: parsers.booleanParser, + default: false, + }, + loggerAdapter: { + env: 'PARSE_SERVER_LOGGER_ADAPTER', + help: 'Adapter module for the logging sub-system', + action: parsers.moduleOrObjectParser, + }, + jsonLogs: { + env: 'JSON_LOGS', + help: 'Log as structured JSON objects', + action: parsers.booleanParser, + }, + logsFolder: { + env: 'PARSE_SERVER_LOGS_FOLDER', + help: + "Folder for the logs (defaults to './logs'); set to null to disable file based logging", + default: './logs', + }, + verbose: { + env: 'VERBOSE', + help: 'Set the logging to verbose', + action: parsers.booleanParser, + }, + logLevel: { + env: 'PARSE_SERVER_LOG_LEVEL', + help: 'Sets the level for logs', + }, + silent: { + env: 'SILENT', + help: 'Disables console output', + action: parsers.booleanParser, + }, + databaseURI: { + env: 'PARSE_SERVER_DATABASE_URI', + help: + 'The full URI to your database. Supported databases are mongodb or postgres.', + required: true, + default: 'mongodb://localhost:27017/parse', + }, + databaseOptions: { + env: 'PARSE_SERVER_DATABASE_OPTIONS', + help: 'Options to pass to the mongodb client', + action: parsers.objectParser, + }, + databaseAdapter: { + env: 'PARSE_SERVER_DATABASE_ADAPTER', + help: 'Adapter module for the database', + action: parsers.moduleOrObjectParser, + }, + cloud: { + env: 'PARSE_SERVER_CLOUD', + help: 'Full path to your cloud code main.js', + }, + collectionPrefix: { + env: 'PARSE_SERVER_COLLECTION_PREFIX', + help: 'A collection prefix for the classes', + default: '', + }, + clientKey: { + env: 'PARSE_SERVER_CLIENT_KEY', + help: 'Key for iOS, MacOS, tvOS clients', + }, + javascriptKey: { + env: 'PARSE_SERVER_JAVASCRIPT_KEY', + help: 'Key for the Javascript SDK', + }, + dotNetKey: { + env: 'PARSE_SERVER_DOT_NET_KEY', + help: 'Key for Unity and .Net SDK', + }, + restAPIKey: { + env: 'PARSE_SERVER_REST_API_KEY', + help: 'Key for REST calls', + }, + readOnlyMasterKey: { + env: 'PARSE_SERVER_READ_ONLY_MASTER_KEY', + help: + 'Read-only key, which has the same capabilities as MasterKey without writes', + }, + webhookKey: { + env: 'PARSE_SERVER_WEBHOOK_KEY', + help: 'Key sent with outgoing webhook calls', + }, + fileKey: { + env: 'PARSE_SERVER_FILE_KEY', + help: 'Key for your files', + }, + preserveFileName: { + env: 'PARSE_SERVER_PRESERVE_FILE_NAME', + help: 'Enable (or disable) the addition of a unique hash to the file names', + action: parsers.booleanParser, + default: false, + }, + userSensitiveFields: { + env: 'PARSE_SERVER_USER_SENSITIVE_FIELDS', + help: + 'Personally identifiable information fields in the user table the should be removed for non-authorized users.', + action: parsers.arrayParser, + default: ['email'], + }, + enableAnonymousUsers: { + env: 'PARSE_SERVER_ENABLE_ANON_USERS', + help: 'Enable (or disable) anon users, defaults to true', + action: parsers.booleanParser, + default: true, + }, + allowClientClassCreation: { + env: 'PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION', + help: 'Enable (or disable) client class creation, defaults to true', + action: parsers.booleanParser, + default: true, + }, + auth: { + env: 'PARSE_SERVER_AUTH_PROVIDERS', + help: + 'Configuration for your authentication providers, as stringified JSON. See http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication', + action: parsers.objectParser, + }, + maxUploadSize: { + env: 'PARSE_SERVER_MAX_UPLOAD_SIZE', + help: 'Max file size for uploads, defaults to 20mb', + default: '20mb', + }, + verifyUserEmails: { + env: 'PARSE_SERVER_VERIFY_USER_EMAILS', + help: 'Enable (or disable) user email validation, defaults to false', + action: parsers.booleanParser, + default: false, + }, + preventLoginWithUnverifiedEmail: { + env: 'PARSE_SERVER_PREVENT_LOGIN_WITH_UNVERIFIED_EMAIL', + help: + 'Prevent user from login if email is not verified and PARSE_SERVER_VERIFY_USER_EMAILS is true, defaults to false', + action: parsers.booleanParser, + default: false, + }, + emailVerifyTokenValidityDuration: { + env: 'PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION', + help: 'Email verification token validity duration, in seconds', + action: parsers.numberParser('emailVerifyTokenValidityDuration'), + }, + accountLockout: { + env: 'PARSE_SERVER_ACCOUNT_LOCKOUT', + help: 'account lockout policy for failed login attempts', + action: parsers.objectParser, + }, + passwordPolicy: { + env: 'PARSE_SERVER_PASSWORD_POLICY', + help: 'Password policy for enforcing password related rules', + action: parsers.objectParser, + }, + cacheAdapter: { + env: 'PARSE_SERVER_CACHE_ADAPTER', + help: 'Adapter module for the cache', + action: parsers.moduleOrObjectParser, + }, + emailAdapter: { + env: 'PARSE_SERVER_EMAIL_ADAPTER', + help: 'Adapter module for email sending', + action: parsers.moduleOrObjectParser, + }, + publicServerURL: { + env: 'PARSE_PUBLIC_SERVER_URL', + help: 'Public URL to your parse server with http:// or https://.', + }, + customPages: { + env: 'PARSE_SERVER_CUSTOM_PAGES', + help: 'custom pages for password validation and reset', + action: parsers.objectParser, + default: {}, + }, + liveQuery: { + env: 'PARSE_SERVER_LIVE_QUERY', + help: "parse-server's LiveQuery configuration object", + action: parsers.objectParser, + }, + sessionLength: { + env: 'PARSE_SERVER_SESSION_LENGTH', + help: 'Session duration, in seconds, defaults to 1 year', + action: parsers.numberParser('sessionLength'), + default: 31536000, + }, + maxLimit: { + env: 'PARSE_SERVER_MAX_LIMIT', + help: 'Max value for limit option on queries, defaults to unlimited', + action: parsers.numberParser('maxLimit'), + }, + expireInactiveSessions: { + env: 'PARSE_SERVER_EXPIRE_INACTIVE_SESSIONS', + help: + 'Sets wether we should expire the inactive sessions, defaults to true', + action: parsers.booleanParser, + default: true, + }, + revokeSessionOnPasswordReset: { + env: 'PARSE_SERVER_REVOKE_SESSION_ON_PASSWORD_RESET', + help: + "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.", + action: parsers.booleanParser, + default: true, + }, + schemaCacheTTL: { + env: 'PARSE_SERVER_SCHEMA_CACHE_TTL', + help: + 'The TTL for caching the schema for optimizing read/write operations. You should put a long TTL when your DB is in production. default to 5000; set 0 to disable.', + action: parsers.numberParser('schemaCacheTTL'), + default: 5000, + }, + cacheTTL: { + env: 'PARSE_SERVER_CACHE_TTL', + help: + 'Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds)', + action: parsers.numberParser('cacheTTL'), + default: 5000, + }, + cacheMaxSize: { + env: 'PARSE_SERVER_CACHE_MAX_SIZE', + help: 'Sets the maximum size for the in memory cache, defaults to 10000', + action: parsers.numberParser('cacheMaxSize'), + default: 10000, + }, + enableSingleSchemaCache: { + env: 'PARSE_SERVER_ENABLE_SINGLE_SCHEMA_CACHE', + help: + 'Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request.', + action: parsers.booleanParser, + default: false, + }, + enableExpressErrorHandler: { + env: 'PARSE_SERVER_ENABLE_EXPRESS_ERROR_HANDLER', + help: 'Enables the default express error handler for all errors', + action: parsers.booleanParser, + default: false, + }, + objectIdSize: { + env: 'PARSE_SERVER_OBJECT_ID_SIZE', + help: "Sets the number of characters in generated object id's, default 10", + action: parsers.numberParser('objectIdSize'), + default: 10, + }, + port: { + env: 'PORT', + help: 'The port to run the ParseServer, defaults to 1337.', + action: parsers.numberParser('port'), + default: 1337, + }, + host: { + env: 'PARSE_SERVER_HOST', + help: 'The host to serve ParseServer on, defaults to 0.0.0.0', + default: '0.0.0.0', + }, + mountPath: { + env: 'PARSE_SERVER_MOUNT_PATH', + help: 'Mount path for the server, defaults to /parse', + default: '/parse', + }, + cluster: { + env: 'PARSE_SERVER_CLUSTER', + help: + 'Run with cluster, optionally set the number of processes default to os.cpus().length', + action: parsers.numberOrBooleanParser, + }, + middleware: { + env: 'PARSE_SERVER_MIDDLEWARE', + help: 'middleware for express server, can be string or function', + }, + startLiveQueryServer: { + env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER', + help: 'Starts the liveQuery server', + action: parsers.booleanParser, + }, + liveQueryServerOptions: { + env: 'PARSE_SERVER_LIVE_QUERY_SERVER_OPTIONS', + help: + 'Live query server configuration options (will start the liveQuery server)', + action: parsers.objectParser, + }, }; module.exports.CustomPagesOptions = { - "invalidLink": { - "env": "PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK", - "help": "invalid link page path" - }, - "verifyEmailSuccess": { - "env": "PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS", - "help": "verify email success page path" - }, - "choosePassword": { - "env": "PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD", - "help": "choose password page path" - }, - "passwordResetSuccess": { - "env": "PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS", - "help": "password reset success page path" - } + invalidLink: { + env: 'PARSE_SERVER_CUSTOM_PAGES_INVALID_LINK', + help: 'invalid link page path', + }, + verifyEmailSuccess: { + env: 'PARSE_SERVER_CUSTOM_PAGES_VERIFY_EMAIL_SUCCESS', + help: 'verify email success page path', + }, + choosePassword: { + env: 'PARSE_SERVER_CUSTOM_PAGES_CHOOSE_PASSWORD', + help: 'choose password page path', + }, + passwordResetSuccess: { + env: 'PARSE_SERVER_CUSTOM_PAGES_PASSWORD_RESET_SUCCESS', + help: 'password reset success page path', + }, }; module.exports.LiveQueryOptions = { - "classNames": { - "env": "PARSE_SERVER_LIVEQUERY_CLASSNAMES", - "help": "parse-server's LiveQuery classNames", - "action": parsers.arrayParser - }, - "redisURL": { - "env": "PARSE_SERVER_LIVEQUERY_REDIS_URL", - "help": "parse-server's LiveQuery redisURL" - }, - "pubSubAdapter": { - "env": "PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER", - "help": "LiveQuery pubsub adapter", - "action": parsers.moduleOrObjectParser - } + classNames: { + env: 'PARSE_SERVER_LIVEQUERY_CLASSNAMES', + help: "parse-server's LiveQuery classNames", + action: parsers.arrayParser, + }, + redisURL: { + env: 'PARSE_SERVER_LIVEQUERY_REDIS_URL', + help: "parse-server's LiveQuery redisURL", + }, + pubSubAdapter: { + env: 'PARSE_SERVER_LIVEQUERY_PUB_SUB_ADAPTER', + help: 'LiveQuery pubsub adapter', + action: parsers.moduleOrObjectParser, + }, }; module.exports.LiveQueryServerOptions = { - "appId": { - "env": "PARSE_LIVE_QUERY_SERVER_APP_ID", - "help": "This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId." - }, - "masterKey": { - "env": "PARSE_LIVE_QUERY_SERVER_MASTER_KEY", - "help": "This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey." - }, - "serverURL": { - "env": "PARSE_LIVE_QUERY_SERVER_SERVER_URL", - "help": "This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL." - }, - "keyPairs": { - "env": "PARSE_LIVE_QUERY_SERVER_KEY_PAIRS", - "help": "A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.", - "action": parsers.objectParser - }, - "websocketTimeout": { - "env": "PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT", - "help": "Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).", - "action": parsers.numberParser("websocketTimeout") - }, - "cacheTimeout": { - "env": "PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT", - "help": "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).", - "action": parsers.numberParser("cacheTimeout") - }, - "logLevel": { - "env": "PARSE_LIVE_QUERY_SERVER_LOG_LEVEL", - "help": "This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO." - }, - "port": { - "env": "PARSE_LIVE_QUERY_SERVER_PORT", - "help": "The port to run the LiveQuery server, defaults to 1337.", - "action": parsers.numberParser("port"), - "default": 1337 - }, - "redisURL": { - "env": "PARSE_LIVE_QUERY_SERVER_REDIS_URL", - "help": "parse-server's LiveQuery redisURL" - }, - "pubSubAdapter": { - "env": "PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER", - "help": "LiveQuery pubsub adapter", - "action": parsers.moduleOrObjectParser - } + appId: { + env: 'PARSE_LIVE_QUERY_SERVER_APP_ID', + help: + 'This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.', + }, + masterKey: { + env: 'PARSE_LIVE_QUERY_SERVER_MASTER_KEY', + help: + 'This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.', + }, + serverURL: { + env: 'PARSE_LIVE_QUERY_SERVER_SERVER_URL', + help: + 'This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.', + }, + keyPairs: { + env: 'PARSE_LIVE_QUERY_SERVER_KEY_PAIRS', + help: + 'A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.', + action: parsers.objectParser, + }, + websocketTimeout: { + env: 'PARSE_LIVE_QUERY_SERVER_WEBSOCKET_TIMEOUT', + help: + 'Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).', + action: parsers.numberParser('websocketTimeout'), + }, + cacheTimeout: { + env: 'PARSE_LIVE_QUERY_SERVER_CACHE_TIMEOUT', + help: + "Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).", + action: parsers.numberParser('cacheTimeout'), + }, + logLevel: { + env: 'PARSE_LIVE_QUERY_SERVER_LOG_LEVEL', + help: + 'This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.', + }, + port: { + env: 'PARSE_LIVE_QUERY_SERVER_PORT', + help: 'The port to run the LiveQuery server, defaults to 1337.', + action: parsers.numberParser('port'), + default: 1337, + }, + redisURL: { + env: 'PARSE_LIVE_QUERY_SERVER_REDIS_URL', + help: "parse-server's LiveQuery redisURL", + }, + pubSubAdapter: { + env: 'PARSE_LIVE_QUERY_SERVER_PUB_SUB_ADAPTER', + help: 'LiveQuery pubsub adapter', + action: parsers.moduleOrObjectParser, + }, }; diff --git a/src/Options/docs.js b/src/Options/docs.js index adcfe81649..2bfac7e70c 100644 --- a/src/Options/docs.js +++ b/src/Options/docs.js @@ -90,4 +90,3 @@ * @property {String} redisURL parse-server's LiveQuery redisURL * @property {Adapter} pubSubAdapter LiveQuery pubsub adapter */ - diff --git a/src/Options/index.js b/src/Options/index.js index 9b55399d2b..fb816293e2 100644 --- a/src/Options/index.js +++ b/src/Options/index.js @@ -1,14 +1,14 @@ -import { AnalyticsAdapter } from "../Adapters/Analytics/AnalyticsAdapter"; -import { FilesAdapter } from "../Adapters/Files/FilesAdapter"; -import { LoggerAdapter } from "../Adapters/Logger/LoggerAdapter"; -import { StorageAdapter } from "../Adapters/Storage/StorageAdapter"; -import { CacheAdapter } from "../Adapters/Cache/CacheAdapter"; -import { MailAdapter } from "../Adapters/Email/MailAdapter"; -import { PubSubAdapter } from "../Adapters/PubSub/PubSubAdapter"; +import { AnalyticsAdapter } from '../Adapters/Analytics/AnalyticsAdapter'; +import { FilesAdapter } from '../Adapters/Files/FilesAdapter'; +import { LoggerAdapter } from '../Adapters/Logger/LoggerAdapter'; +import { StorageAdapter } from '../Adapters/Storage/StorageAdapter'; +import { CacheAdapter } from '../Adapters/Cache/CacheAdapter'; +import { MailAdapter } from '../Adapters/Email/MailAdapter'; +import { PubSubAdapter } from '../Adapters/PubSub/PubSubAdapter'; // @flow -type Adapter = string|any|T; -type NumberOrBoolean = number|boolean; +type Adapter = string | any | T; +type NumberOrBoolean = number | boolean; export interface ParseServerOptions { /* Your Parse Application ID @@ -20,7 +20,7 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_URL */ serverURL: string; /* Restrict masterKey to be used by only these ips, defaults to [] (allow all ips) */ - masterKeyIps: ?string[]; // = [] + masterKeyIps: ?(string[]); // = [] /* Sets the app name */ appName: ?string; /* Adapter module for the analytics */ @@ -76,7 +76,7 @@ export interface ParseServerOptions { :ENV: PARSE_SERVER_PRESERVE_FILE_NAME */ preserveFileName: ?boolean; // = false /* Personally identifiable information fields in the user table the should be removed for non-authorized users. */ - userSensitiveFields: ?string[]; // = ["email"] + userSensitiveFields: ?(string[]); // = ["email"] /* Enable (or disable) anon users, defaults to true :ENV: PARSE_SERVER_ENABLE_ANON_USERS */ enableAnonymousUsers: ?boolean; // = true @@ -122,7 +122,7 @@ export interface ParseServerOptions { /* Sets the TTL for the in memory cache (in ms), defaults to 5000 (5 seconds) */ cacheTTL: ?number; // = 5000 /* Sets the maximum size for the in memory cache, defaults to 10000 */ - cacheMaxSize : ?number; // = 10000 + cacheMaxSize: ?number; // = 10000 /* Use a single schema cache shared across requests. Reduces number of queries made to _SCHEMA, defaults to false, i.e. unique schema cache per request. */ enableSingleSchemaCache: ?boolean; // = false /* Enables the default express error handler for all errors */ @@ -139,13 +139,13 @@ export interface ParseServerOptions { /* Run with cluster, optionally set the number of processes default to os.cpus().length */ cluster: ?NumberOrBoolean; /* middleware for express server, can be string or function */ - middleware: ?((()=>void)|string); + middleware: ?((() => void) | string); /* Starts the liveQuery server */ startLiveQueryServer: ?boolean; /* Live query server configuration options (will start the liveQuery server) */ liveQueryServerOptions: ?LiveQueryServerOptions; - __indexBuildCompletionCallbackForTests: ?()=>void; + __indexBuildCompletionCallbackForTests: ?() => void; } export interface CustomPagesOptions { @@ -162,32 +162,32 @@ export interface CustomPagesOptions { export interface LiveQueryOptions { /* parse-server's LiveQuery classNames :ENV: PARSE_SERVER_LIVEQUERY_CLASSNAMES */ - classNames: ?string[], + classNames: ?(string[]); /* parse-server's LiveQuery redisURL */ - redisURL: ?string, + redisURL: ?string; /* LiveQuery pubsub adapter */ - pubSubAdapter: ?Adapter, + pubSubAdapter: ?Adapter; } export interface LiveQueryServerOptions { /* This string should match the appId in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same appId.*/ - appId: ?string, + appId: ?string; /* This string should match the masterKey in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same masterKey.*/ - masterKey: ?string, + masterKey: ?string; /* This string should match the serverURL in use by your Parse Server. If you deploy the LiveQuery server alongside Parse Server, the LiveQuery server will try to use the same serverURL.*/ - serverURL: ?string, + serverURL: ?string; /* A JSON object that serves as a whitelist of keys. It is used for validating clients when they try to connect to the LiveQuery server. Check the following Security section and our protocol specification for details.*/ - keyPairs: ?any, + keyPairs: ?any; /* Number of milliseconds between ping/pong frames. The WebSocket server sends ping/pong frames to the clients to keep the WebSocket alive. This value defines the interval of the ping/pong frame from the server to clients, defaults to 10 * 1000 ms (10 s).*/ - websocketTimeout: ?number, + websocketTimeout: ?number; /* Number in milliseconds. When clients provide the sessionToken to the LiveQuery server, the LiveQuery server will try to fetch its ParseUser's objectId from parse server and store it in the cache. The value defines the duration of the cache. Check the following Security section and our protocol specification for details, defaults to 30 * 24 * 60 * 60 * 1000 ms (~30 days).*/ - cacheTimeout: ?number, + cacheTimeout: ?number; /* This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE, defaults to INFO.*/ - logLevel: ?string, + logLevel: ?string; /* The port to run the LiveQuery server, defaults to 1337.*/ - port: ?number, // = 1337 + port: ?number; // = 1337 /* parse-server's LiveQuery redisURL */ - redisURL: ?string, + redisURL: ?string; /* LiveQuery pubsub adapter */ - pubSubAdapter: ?Adapter, + pubSubAdapter: ?Adapter; } diff --git a/src/Options/parsers.js b/src/Options/parsers.js index 752a27f8f7..e03fcaaa03 100644 --- a/src/Options/parsers.js +++ b/src/Options/parsers.js @@ -5,7 +5,7 @@ function numberParser(key) { throw new Error(`Key ${key} has invalid value ${opt}`); } return intOpt; - } + }; } function numberOrBoolParser(key) { @@ -20,14 +20,14 @@ function numberOrBoolParser(key) { return false; } return numberParser(key)(opt); - } + }; } function objectParser(opt) { if (typeof opt == 'object') { return opt; } - return JSON.parse(opt) + return JSON.parse(opt); } function arrayParser(opt) { @@ -41,12 +41,14 @@ function arrayParser(opt) { } function moduleOrObjectParser(opt) { - if (typeof opt == 'object') { + if (typeof opt == 'object') { return opt; } try { return JSON.parse(opt); - } catch(e) { /* */ } + } catch (e) { + /* */ + } return opt; } @@ -71,5 +73,5 @@ module.exports = { booleanParser, moduleOrObjectParser, arrayParser, - objectParser + objectParser, }; diff --git a/src/ParseMessageQueue.js b/src/ParseMessageQueue.js index 7195642400..1dcf55d525 100644 --- a/src/ParseMessageQueue.js +++ b/src/ParseMessageQueue.js @@ -1,26 +1,30 @@ import { loadAdapter } from './Adapters/AdapterLoader'; -import { - EventEmitterMQ -} from './Adapters/MessageQueue/EventEmitterMQ'; +import { EventEmitterMQ } from './Adapters/MessageQueue/EventEmitterMQ'; const ParseMessageQueue = {}; ParseMessageQueue.createPublisher = function(config: any): any { - const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config); + const adapter = loadAdapter( + config.messageQueueAdapter, + EventEmitterMQ, + config + ); if (typeof adapter.createPublisher !== 'function') { throw 'pubSubAdapter should have createPublisher()'; } return adapter.createPublisher(config); -} +}; ParseMessageQueue.createSubscriber = function(config: any): void { - const adapter = loadAdapter(config.messageQueueAdapter, EventEmitterMQ, config) + const adapter = loadAdapter( + config.messageQueueAdapter, + EventEmitterMQ, + config + ); if (typeof adapter.createSubscriber !== 'function') { throw 'messageQueueAdapter should have createSubscriber()'; } return adapter.createSubscriber(config); -} +}; -export { - ParseMessageQueue -} +export { ParseMessageQueue }; diff --git a/src/ParseServer.js b/src/ParseServer.js index 81fb7f77e9..ceca44b2a7 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -7,34 +7,33 @@ var batch = require('./batch'), Parse = require('parse/node').Parse, path = require('path'); -import { ParseServerOptions, - LiveQueryServerOptions } from './Options'; -import defaults from './defaults'; -import * as logging from './logger'; -import Config from './Config'; -import PromiseRouter from './PromiseRouter'; -import requiredParameter from './requiredParameter'; -import { AnalyticsRouter } from './Routers/AnalyticsRouter'; -import { ClassesRouter } from './Routers/ClassesRouter'; -import { FeaturesRouter } from './Routers/FeaturesRouter'; -import { FilesRouter } from './Routers/FilesRouter'; -import { FunctionsRouter } from './Routers/FunctionsRouter'; -import { GlobalConfigRouter } from './Routers/GlobalConfigRouter'; -import { HooksRouter } from './Routers/HooksRouter'; -import { IAPValidationRouter } from './Routers/IAPValidationRouter'; -import { InstallationsRouter } from './Routers/InstallationsRouter'; -import { LogsRouter } from './Routers/LogsRouter'; +import { ParseServerOptions, LiveQueryServerOptions } from './Options'; +import defaults from './defaults'; +import * as logging from './logger'; +import Config from './Config'; +import PromiseRouter from './PromiseRouter'; +import requiredParameter from './requiredParameter'; +import { AnalyticsRouter } from './Routers/AnalyticsRouter'; +import { ClassesRouter } from './Routers/ClassesRouter'; +import { FeaturesRouter } from './Routers/FeaturesRouter'; +import { FilesRouter } from './Routers/FilesRouter'; +import { FunctionsRouter } from './Routers/FunctionsRouter'; +import { GlobalConfigRouter } from './Routers/GlobalConfigRouter'; +import { HooksRouter } from './Routers/HooksRouter'; +import { IAPValidationRouter } from './Routers/IAPValidationRouter'; +import { InstallationsRouter } from './Routers/InstallationsRouter'; +import { LogsRouter } from './Routers/LogsRouter'; import { ParseLiveQueryServer } from './LiveQuery/ParseLiveQueryServer'; -import { PublicAPIRouter } from './Routers/PublicAPIRouter'; -import { PushRouter } from './Routers/PushRouter'; -import { CloudCodeRouter } from './Routers/CloudCodeRouter'; -import { RolesRouter } from './Routers/RolesRouter'; -import { SchemasRouter } from './Routers/SchemasRouter'; -import { SessionsRouter } from './Routers/SessionsRouter'; -import { UsersRouter } from './Routers/UsersRouter'; -import { PurgeRouter } from './Routers/PurgeRouter'; -import { AudiencesRouter } from './Routers/AudiencesRouter'; -import { AggregateRouter } from './Routers/AggregateRouter'; +import { PublicAPIRouter } from './Routers/PublicAPIRouter'; +import { PushRouter } from './Routers/PushRouter'; +import { CloudCodeRouter } from './Routers/CloudCodeRouter'; +import { RolesRouter } from './Routers/RolesRouter'; +import { SchemasRouter } from './Routers/SchemasRouter'; +import { SessionsRouter } from './Routers/SessionsRouter'; +import { UsersRouter } from './Routers/UsersRouter'; +import { PurgeRouter } from './Routers/PurgeRouter'; +import { AudiencesRouter } from './Routers/AudiencesRouter'; +import { AggregateRouter } from './Routers/AggregateRouter'; import { ParseServerRESTController } from './ParseServerRESTController'; import * as controllers from './Controllers'; @@ -72,7 +71,7 @@ class ParseServer { /** * @constructor * @param {ParseServerOptions} options the parse server initialization options - */ + */ constructor(options: ParseServerOptions) { injectDefaults(options); const { @@ -108,7 +107,7 @@ class ParseServer { if (cloud) { addParseCloud(); if (typeof cloud === 'function') { - cloud(Parse) + cloud(Parse); } else if (typeof cloud === 'string') { require(path.resolve(process.cwd(), cloud)); } else { @@ -135,25 +134,33 @@ class ParseServer { * @static * Create an express app for the parse server * @param {Object} options let you specify the maxUploadSize when creating the express app */ - static app({maxUploadSize = '20mb', appId}) { + static app({ maxUploadSize = '20mb', appId }) { // This app serves the Parse API directly. // It's the equivalent of https://api.parse.com/1 in the hosted Parse API. var api = express(); //api.use("/apps", express.static(__dirname + "/public")); // File handling needs to be before default middlewares are applied - api.use('/', middlewares.allowCrossDomain, new FilesRouter().expressRouter({ - maxUploadSize: maxUploadSize - })); - - api.use('/health', (function(req, res) { + api.use( + '/', + middlewares.allowCrossDomain, + new FilesRouter().expressRouter({ + maxUploadSize: maxUploadSize, + }) + ); + + api.use('/health', function(req, res) { res.json({ - status: 'ok' + status: 'ok', }); - })); + }); - api.use('/', bodyParser.urlencoded({extended: false}), new PublicAPIRouter().expressRouter()); + api.use( + '/', + bodyParser.urlencoded({ extended: false }), + new PublicAPIRouter().expressRouter() + ); - api.use(bodyParser.json({ 'type': '*/*' , limit: maxUploadSize })); + api.use(bodyParser.json({ type: '*/*', limit: maxUploadSize })); api.use(middlewares.allowCrossDomain); api.use(middlewares.allowMethodOverride); api.use(middlewares.handleParseHeaders); @@ -167,9 +174,12 @@ class ParseServer { if (!process.env.TESTING) { //This causes tests to spew some useless warnings, so disable in test /* istanbul ignore next */ - process.on('uncaughtException', (err) => { - if (err.code === "EADDRINUSE") { // user-friendly message for this common error - process.stderr.write(`Unable to listen on port ${err.port}. The port is already in use.`); + process.on('uncaughtException', err => { + if (err.code === 'EADDRINUSE') { + // user-friendly message for this common error + process.stderr.write( + `Unable to listen on port ${err.port}. The port is already in use.` + ); process.exit(0); } else { throw err; @@ -182,12 +192,14 @@ class ParseServer { }); } if (process.env.PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS === '1') { - Parse.CoreManager.setRESTController(ParseServerRESTController(appId, appRouter)); + Parse.CoreManager.setRESTController( + ParseServerRESTController(appId, appRouter) + ); } return api; } - static promiseRouter({appId}) { + static promiseRouter({ appId }) { const routers = [ new ClassesRouter(), new UsersRouter(), @@ -206,7 +218,7 @@ class ParseServer { new HooksRouter(), new CloudCodeRouter(), new AudiencesRouter(), - new AggregateRouter() + new AggregateRouter(), ]; const routes = routers.reduce((memo, router) => { @@ -225,7 +237,7 @@ class ParseServer { * @param {Function} callback called when the server has started * @returns {ParseServer} the parse server instance */ - start(options: ParseServerOptions, callback: ?()=>void) { + start(options: ParseServerOptions, callback: ?() => void) { const app = express(); if (options.middleware) { let middleware; @@ -242,7 +254,10 @@ class ParseServer { this.server = server; if (options.startLiveQueryServer || options.liveQueryServerOptions) { - this.liveQueryServer = ParseServer.createLiveQueryServer(server, options.liveQueryServerOptions); + this.liveQueryServer = ParseServer.createLiveQueryServer( + server, + options.liveQueryServerOptions + ); } /* istanbul ignore next */ if (!process.env.TESTING) { @@ -258,7 +273,7 @@ class ParseServer { * @param {Function} callback called when the server has started * @returns {ParseServer} the parse server instance */ - static start(options: ParseServerOptions, callback: ?()=>void) { + static start(options: ParseServerOptions, callback: ?() => void) { const parseServer = new ParseServer(options); return parseServer.start(options, callback); } @@ -281,25 +296,36 @@ class ParseServer { static verifyServerUrl(callback) { // perform a health check on the serverURL value - if(Parse.serverURL) { + if (Parse.serverURL) { const request = require('request'); - request(Parse.serverURL.replace(/\/$/, "") + "/health", function (error, response, body) { + request(Parse.serverURL.replace(/\/$/, '') + '/health', function( + error, + response, + body + ) { let json; try { json = JSON.parse(body); - } catch(e) { + } catch (e) { json = null; } - if (error || response.statusCode !== 200 || !json || json && json.status !== 'ok') { + if ( + error || + response.statusCode !== 200 || + !json || + (json && json.status !== 'ok') + ) { /* eslint-disable no-console */ - console.warn(`\nWARNING, Unable to connect to '${Parse.serverURL}'.` + - ` Cloud code and push notifications may be unavailable!\n`); + console.warn( + `\nWARNING, Unable to connect to '${Parse.serverURL}'.` + + ` Cloud code and push notifications may be unavailable!\n` + ); /* eslint-enable no-console */ - if(callback) { + if (callback) { callback(false); } } else { - if(callback) { + if (callback) { callback(true); } } @@ -309,13 +335,13 @@ class ParseServer { } function addParseCloud() { - const ParseCloud = require("./cloud-code/Parse.Cloud"); + const ParseCloud = require('./cloud-code/Parse.Cloud'); Object.assign(Parse.Cloud, ParseCloud); global.Parse = Parse; } function injectDefaults(options: ParseServerOptions) { - Object.keys(defaults).forEach((key) => { + Object.keys(defaults).forEach(key => { if (!options.hasOwnProperty(key)) { options[key] = defaults[key]; } @@ -325,15 +351,20 @@ function injectDefaults(options: ParseServerOptions) { options.serverURL = `http://localhost:${options.port}${options.mountPath}`; } - options.userSensitiveFields = Array.from(new Set(options.userSensitiveFields.concat( - defaults.userSensitiveFields, - options.userSensitiveFields - ))); - - options.masterKeyIps = Array.from(new Set(options.masterKeyIps.concat( - defaults.masterKeyIps, - options.masterKeyIps - ))); + options.userSensitiveFields = Array.from( + new Set( + options.userSensitiveFields.concat( + defaults.userSensitiveFields, + options.userSensitiveFields + ) + ) + ); + + options.masterKeyIps = Array.from( + new Set( + options.masterKeyIps.concat(defaults.masterKeyIps, options.masterKeyIps) + ) + ); } // Those can't be tested as it requires a subprocess @@ -343,7 +374,7 @@ function configureListeners(parseServer) { const sockets = {}; /* Currently, express doesn't shut down immediately after receiving SIGINT/SIGTERM if it has client connections that haven't timed out. (This is a known issue with node - https://github.com/nodejs/node/issues/2642) This function, along with `destroyAliveConnections()`, intend to fix this behavior such that parse server will close all open connections and initiate the shutdown process as soon as it receives a SIGINT/SIGTERM signal. */ - server.on('connection', (socket) => { + server.on('connection', socket => { const socketId = socket.remoteAddress + ':' + socket.remotePort; sockets[socketId] = socket; socket.on('close', () => { @@ -355,9 +386,11 @@ function configureListeners(parseServer) { for (const socketId in sockets) { try { sockets[socketId].destroy(); - } catch (e) { /* */ } + } catch (e) { + /* */ + } } - } + }; const handleShutdown = function() { process.stdout.write('Termination signal received. Shutting down.'); diff --git a/src/ParseServerRESTController.js b/src/ParseServerRESTController.js index 547260a4e0..b91fbdbd1e 100644 --- a/src/ParseServerRESTController.js +++ b/src/ParseServerRESTController.js @@ -14,20 +14,22 @@ function getSessionToken(options) { function getAuth(options = {}, config) { const installationId = options.installationId || 'cloud'; if (options.useMasterKey) { - return Promise.resolve(new Auth.Auth({config, isMaster: true, installationId })); + return Promise.resolve( + new Auth.Auth({ config, isMaster: true, installationId }) + ); } - return getSessionToken(options).then((sessionToken) => { + return getSessionToken(options).then(sessionToken => { if (sessionToken) { options.sessionToken = sessionToken; return Auth.getAuthForSessionToken({ config, sessionToken: sessionToken, - installationId + installationId, }); } else { return Promise.resolve(new Auth.Auth({ config, installationId })); } - }) + }); } function ParseServerRESTController(applicationId, router) { @@ -41,17 +43,27 @@ function ParseServerRESTController(applicationId, router) { path = path.slice(serverURL.path.length, path.length); } - if (path[0] !== "/") { - path = "/" + path; + if (path[0] !== '/') { + path = '/' + path; } if (path === '/batch') { - const promises = data.requests.map((request) => { - return handleRequest(request.method, request.path, request.body, options).then((response) => { - return Promise.resolve({success: response}); - }, (error) => { - return Promise.resolve({error: {code: error.code, error: error.message}}); - }); + const promises = data.requests.map(request => { + return handleRequest( + request.method, + request.path, + request.body, + options + ).then( + response => { + return Promise.resolve({ success: response }); + }, + error => { + return Promise.resolve({ + error: { code: error.code, error: error.message }, + }); + } + ); }); return Promise.all(promises); } @@ -62,37 +74,44 @@ function ParseServerRESTController(applicationId, router) { } return new Promise((resolve, reject) => { - getAuth(options, config).then((auth) => { + getAuth(options, config).then(auth => { const request = { body: data, config, auth, info: { applicationId: applicationId, - sessionToken: options.sessionToken + sessionToken: options.sessionToken, }, - query + query, }; - return Promise.resolve().then(() => { - return router.tryRouteRequest(method, path, request); - }).then((response) => { - resolve(response.response, response.status, response); - }, (err) => { - if (err instanceof Parse.Error && - err.code == Parse.Error.INVALID_JSON && - err.message == `cannot route ${method} ${path}`) { - RESTController.request.apply(null, args).then(resolve, reject); - } else { - reject(err); - } - }); + return Promise.resolve() + .then(() => { + return router.tryRouteRequest(method, path, request); + }) + .then( + response => { + resolve(response.response, response.status, response); + }, + err => { + if ( + err instanceof Parse.Error && + err.code == Parse.Error.INVALID_JSON && + err.message == `cannot route ${method} ${path}` + ) { + RESTController.request.apply(null, args).then(resolve, reject); + } else { + reject(err); + } + } + ); }, reject); }); } - return { + return { request: handleRequest, - ajax: RESTController.ajax + ajax: RESTController.ajax, }; } diff --git a/src/PromiseRouter.js b/src/PromiseRouter.js index e51bcd1002..755b5e5ed2 100644 --- a/src/PromiseRouter.js +++ b/src/PromiseRouter.js @@ -5,10 +5,10 @@ // themselves use our routing information, without disturbing express // components that external developers may be modifying. -import Parse from 'parse/node'; -import express from 'express'; -import log from './logger'; -import {inspect} from 'util'; +import Parse from 'parse/node'; +import express from 'express'; +import log from './logger'; +import { inspect } from 'util'; const Layer = require('express/lib/router/layer'); function validateParameter(key, value) { @@ -25,7 +25,6 @@ function validateParameter(key, value) { } } - export default class PromiseRouter { // Each entry should be an object with: // path: the path to route, in express format @@ -54,14 +53,14 @@ export default class PromiseRouter { } route(method, path, ...handlers) { - switch(method) { - case 'POST': - case 'GET': - case 'PUT': - case 'DELETE': - break; - default: - throw 'cannot route method: ' + method; + switch (method) { + case 'POST': + case 'GET': + case 'PUT': + case 'DELETE': + break; + default: + throw 'cannot route method: ' + method; } let handler = handlers[0]; @@ -73,14 +72,14 @@ export default class PromiseRouter { return handler(req); }); }, Promise.resolve()); - } + }; } this.routes.push({ path: path, method: method, handler: handler, - layer: new Layer(path, null, handler) + layer: new Layer(path, null, handler), }); } @@ -97,17 +96,17 @@ export default class PromiseRouter { const match = layer.match(path); if (match) { const params = layer.params; - Object.keys(params).forEach((key) => { + Object.keys(params).forEach(key => { params[key] = validateParameter(key, params[key]); }); - return {params: params, handler: route.handler}; + return { params: params, handler: route.handler }; } } } // Mount the routes on this router onto an express app (or express router) mountOnto(expressApp) { - this.routes.forEach((route) => { + this.routes.forEach(route => { const method = route.method.toLowerCase(); const handler = makeExpressHandler(this.appId, route.handler); expressApp[method].call(expressApp, route.path, handler); @@ -124,7 +123,8 @@ export default class PromiseRouter { if (!match) { throw new Parse.Error( Parse.Error.INVALID_JSON, - 'cannot route ' + method + ' ' + path); + 'cannot route ' + method + ' ' + path + ); } request.params = match.params; return new Promise((resolve, reject) => { @@ -148,55 +148,63 @@ function makeExpressHandler(appId, promiseHandler) { method, url, headers, - body - }); - promiseHandler(req).then((result) => { - if (!result.response && !result.location && !result.text) { - log.error('the handler did not include a "response" or a "location" field'); - throw 'control should not get here'; - } - - log.logResponse({ method, url, result }); - - var status = result.status || 200; - res.status(status); - - if (result.text) { - res.send(result.text); - return; - } - - if (result.location) { - res.set('Location', result.location); - // Override the default expressjs response - // as it double encodes %encoded chars in URL - if (!result.response) { - res.send('Found. Redirecting to ' + result.location); - return; - } - } - if (result.headers) { - Object.keys(result.headers).forEach((header) => { - res.set(header, result.headers[header]); - }) - } - res.json(result.response); - }, (error) => next(error)).catch((e) => { - log.error(`Error generating response. ${inspect(e)}`, {error: e}); - next(e); + body, }); + promiseHandler(req) + .then( + result => { + if (!result.response && !result.location && !result.text) { + log.error( + 'the handler did not include a "response" or a "location" field' + ); + throw 'control should not get here'; + } + + log.logResponse({ method, url, result }); + + var status = result.status || 200; + res.status(status); + + if (result.text) { + res.send(result.text); + return; + } + + if (result.location) { + res.set('Location', result.location); + // Override the default expressjs response + // as it double encodes %encoded chars in URL + if (!result.response) { + res.send('Found. Redirecting to ' + result.location); + return; + } + } + if (result.headers) { + Object.keys(result.headers).forEach(header => { + res.set(header, result.headers[header]); + }); + } + res.json(result.response); + }, + error => next(error) + ) + .catch(e => { + log.error(`Error generating response. ${inspect(e)}`, { error: e }); + next(e); + }); } catch (e) { - log.error(`Error handling request: ${inspect(e)}`, {error: e}); + log.error(`Error handling request: ${inspect(e)}`, { error: e }); next(e); } - } + }; } - function maskSensitiveUrl(req) { let maskUrl = req.originalUrl.toString(); - const shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login') - && !req.originalUrl.includes('classes'); + const shouldMaskUrl = + req.method === 'GET' && + req.originalUrl.includes('/login') && + !req.originalUrl.includes('classes'); if (shouldMaskUrl) { maskUrl = log.maskSensitiveUrl(maskUrl); } diff --git a/src/Push/PushQueue.js b/src/Push/PushQueue.js index 33c84994bd..f67f7c9e6b 100644 --- a/src/Push/PushQueue.js +++ b/src/Push/PushQueue.js @@ -1,5 +1,5 @@ -import { ParseMessageQueue } from '../ParseMessageQueue'; -import rest from '../rest'; +import { ParseMessageQueue } from '../ParseMessageQueue'; +import rest from '../rest'; import { applyDeviceTokenExists } from './utils'; import Parse from 'parse/node'; @@ -30,33 +30,39 @@ export class PushQueue { // Order by objectId so no impact on the DB const order = 'objectId'; - return Promise.resolve().then(() => { - return rest.find(config, - auth, - '_Installation', - where, - {limit: 0, count: true}); - }).then(({results, count}) => { - if (!results || count == 0) { - return pushStatus.complete(); - } - pushStatus.setRunning(Math.ceil(count / limit)); - let skip = 0; - while (skip < count) { - const query = { where, - limit, - skip, - order }; - - const pushWorkItem = { - body, - query, - pushStatus: { objectId: pushStatus.objectId }, - applicationId: config.applicationId + return Promise.resolve() + .then(() => { + return rest.find(config, auth, '_Installation', where, { + limit: 0, + count: true, + }); + }) + .then(({ results, count }) => { + if (!results || count == 0) { + return pushStatus.complete(); } - this.parsePublisher.publish(this.channel, JSON.stringify(pushWorkItem)); - skip += limit; - } - }); + pushStatus.setRunning(Math.ceil(count / limit)); + let skip = 0; + while (skip < count) { + const query = { + where, + limit, + skip, + order, + }; + + const pushWorkItem = { + body, + query, + pushStatus: { objectId: pushStatus.objectId }, + applicationId: config.applicationId, + }; + this.parsePublisher.publish( + this.channel, + JSON.stringify(pushWorkItem) + ); + skip += limit; + } + }); } } diff --git a/src/Push/PushWorker.js b/src/Push/PushWorker.js index 521ce3aa67..d16542aeac 100644 --- a/src/Push/PushWorker.js +++ b/src/Push/PushWorker.js @@ -1,16 +1,16 @@ // @flow // @flow-disable-next -import deepcopy from 'deepcopy'; -import AdaptableController from '../Controllers/AdaptableController'; -import { master } from '../Auth'; -import Config from '../Config'; -import { PushAdapter } from '../Adapters/Push/PushAdapter'; -import rest from '../rest'; -import { pushStatusHandler } from '../StatusHandler'; -import * as utils from './utils'; -import { ParseMessageQueue } from '../ParseMessageQueue'; -import { PushQueue } from './PushQueue'; -import logger from '../logger'; +import deepcopy from 'deepcopy'; +import AdaptableController from '../Controllers/AdaptableController'; +import { master } from '../Auth'; +import Config from '../Config'; +import { PushAdapter } from '../Adapters/Push/PushAdapter'; +import rest from '../rest'; +import { pushStatusHandler } from '../StatusHandler'; +import * as utils from './utils'; +import { ParseMessageQueue } from '../ParseMessageQueue'; +import { PushQueue } from './PushQueue'; +import logger from '../logger'; function groupByBadge(installations) { return installations.reduce((map, installation) => { @@ -48,15 +48,23 @@ export class PushWorker { const where = utils.applyDeviceTokenExists(query.where); delete query.where; pushStatus = pushStatusHandler(config, pushStatus.objectId); - return rest.find(config, auth, '_Installation', where, query).then(({results}) => { - if (results.length == 0) { - return; - } - return this.sendToAdapter(body, results, pushStatus, config, UTCOffset); - }); + return rest + .find(config, auth, '_Installation', where, query) + .then(({ results }) => { + if (results.length == 0) { + return; + } + return this.sendToAdapter(body, results, pushStatus, config, UTCOffset); + }); } - sendToAdapter(body: any, installations: any[], pushStatus: any, config: Config, UTCOffset: ?any): Promise<*> { + sendToAdapter( + body: any, + installations: any[], + pushStatus: any, + config: Config, + UTCOffset: ?any + ): Promise<*> { // Check if we have locales in the push body const locales = utils.getLocalesFromPush(body); if (locales.length > 0) { @@ -64,31 +72,48 @@ export class PushWorker { const bodiesPerLocales = utils.bodiesPerLocales(body, locales); // Group installations on the specified locales (en, fr, default etc...) - const grouppedInstallations = utils.groupByLocaleIdentifier(installations, locales); - const promises = Object.keys(grouppedInstallations).map((locale) => { + const grouppedInstallations = utils.groupByLocaleIdentifier( + installations, + locales + ); + const promises = Object.keys(grouppedInstallations).map(locale => { const installations = grouppedInstallations[locale]; const body = bodiesPerLocales[locale]; - return this.sendToAdapter(body, installations, pushStatus, config, UTCOffset); + return this.sendToAdapter( + body, + installations, + pushStatus, + config, + UTCOffset + ); }); return Promise.all(promises); } if (!utils.isPushIncrementing(body)) { logger.verbose(`Sending push to ${installations.length}`); - return this.adapter.send(body, installations, pushStatus.objectId).then((results) => { - return pushStatus.trackSent(results, UTCOffset).then(() => results); - }); + return this.adapter + .send(body, installations, pushStatus.objectId) + .then(results => { + return pushStatus.trackSent(results, UTCOffset).then(() => results); + }); } // Collect the badges to reduce the # of calls const badgeInstallationsMap = groupByBadge(installations); // Map the on the badges count and return the send result - const promises = Object.keys(badgeInstallationsMap).map((badge) => { + const promises = Object.keys(badgeInstallationsMap).map(badge => { const payload = deepcopy(body); payload.data.badge = parseInt(badge); const installations = badgeInstallationsMap[badge]; - return this.sendToAdapter(payload, installations, pushStatus, config, UTCOffset); + return this.sendToAdapter( + payload, + installations, + pushStatus, + config, + UTCOffset + ); }); return Promise.all(promises); } diff --git a/src/Push/utils.js b/src/Push/utils.js index f9b1a4118f..fe0b2d6c29 100644 --- a/src/Push/utils.js +++ b/src/Push/utils.js @@ -1,4 +1,4 @@ -import Parse from 'parse/node'; +import Parse from 'parse/node'; import deepcopy from 'deepcopy'; export function isPushIncrementing(body) { @@ -7,12 +7,16 @@ export function isPushIncrementing(body) { } const badge = body.data.badge; - if (typeof badge == 'string' && badge.toLowerCase() == "increment") { + if (typeof badge == 'string' && badge.toLowerCase() == 'increment') { return true; } - return typeof badge == 'object' && typeof badge.__op == 'string' && - badge.__op.toLowerCase() == "increment" && Number(badge.amount); + return ( + typeof badge == 'object' && + typeof badge.__op == 'string' && + badge.__op.toLowerCase() == 'increment' && + Number(badge.amount) + ); } const localizableKeys = ['alert', 'title']; @@ -22,14 +26,18 @@ export function getLocalesFromPush(body) { if (!data) { return []; } - return [...new Set(Object.keys(data).reduce((memo, key) => { - localizableKeys.forEach((localizableKey) => { - if (key.indexOf(`${localizableKey}-`) == 0) { - memo.push(key.slice(localizableKey.length + 1)); - } - }); - return memo; - }, []))]; + return [ + ...new Set( + Object.keys(data).reduce((memo, key) => { + localizableKeys.forEach(localizableKey => { + if (key.indexOf(`${localizableKey}-`) == 0) { + memo.push(key.slice(localizableKey.length + 1)); + } + }); + return memo; + }, []) + ), + ]; } export function transformPushBodyForLocale(body, locale) { @@ -38,7 +46,7 @@ export function transformPushBodyForLocale(body, locale) { return body; } body = deepcopy(body); - localizableKeys.forEach((key) => { + localizableKeys.forEach(key => { const localeValue = body.data[`${key}-${locale}`]; if (localeValue) { body.data[key] = localeValue; @@ -48,9 +56,11 @@ export function transformPushBodyForLocale(body, locale) { } export function stripLocalesFromBody(body) { - if (!body.data) { return body; } - Object.keys(body.data).forEach((key) => { - localizableKeys.forEach((localizableKey) => { + if (!body.data) { + return body; + } + Object.keys(body.data).forEach(key => { + localizableKeys.forEach(localizableKey => { if (key.indexOf(`${localizableKey}-`) == 0) { delete body.data[key]; } @@ -71,23 +81,29 @@ export function bodiesPerLocales(body, locales = []) { } export function groupByLocaleIdentifier(installations, locales = []) { - return installations.reduce((map, installation) => { - let added = false; - locales.forEach((locale) => { - if (added) { - return; - } - if (installation.localeIdentifier && installation.localeIdentifier.indexOf(locale) === 0) { - added = true; - map[locale] = map[locale] || []; - map[locale].push(installation); + return installations.reduce( + (map, installation) => { + let added = false; + locales.forEach(locale => { + if (added) { + return; + } + if ( + installation.localeIdentifier && + installation.localeIdentifier.indexOf(locale) === 0 + ) { + added = true; + map[locale] = map[locale] || []; + map[locale].push(installation); + } + }); + if (!added) { + map.default.push(installation); } - }); - if (!added) { - map.default.push(installation); - } - return map; - }, {default: []}); + return map; + }, + { default: [] } + ); } /** @@ -106,8 +122,10 @@ export function validatePushType(where = {}, validPushTypes = []) { for (var i = 0; i < deviceTypes.length; i++) { var deviceType = deviceTypes[i]; if (validPushTypes.indexOf(deviceType) < 0) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - deviceType + ' is not supported push type.'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + deviceType + ' is not supported push type.' + ); } } } @@ -115,7 +133,7 @@ export function validatePushType(where = {}, validPushTypes = []) { export function applyDeviceTokenExists(where) { where = deepcopy(where); if (!where.hasOwnProperty('deviceToken')) { - where['deviceToken'] = {'$exists': true}; + where['deviceToken'] = { $exists: true }; } return where; } diff --git a/src/RestQuery.js b/src/RestQuery.js index 20dcb1eab2..e39d52804f 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -14,8 +14,14 @@ const AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt', 'ACL']; // include // keys // redirectClassNameForKey -function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, clientSDK) { - +function RestQuery( + config, + auth, + className, + restWhere = {}, + restOptions = {}, + clientSDK +) { this.config = config; this.auth = auth; this.className = className; @@ -29,17 +35,22 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl if (!this.auth.isMaster) { if (this.className == '_Session') { if (!this.auth.user) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Invalid session token'); + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); } this.restWhere = { - '$and': [this.restWhere, { - 'user': { - __type: 'Pointer', - className: '_User', - objectId: this.auth.user.id - } - }] + $and: [ + this.restWhere, + { + user: { + __type: 'Pointer', + className: '_User', + objectId: this.auth.user.id, + }, + }, + ], }; } } @@ -58,14 +69,18 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl // If we have keys, we probably want to force some includes (n-1 level) // See issue: https://github.com/parse-community/parse-server/issues/3185 if (restOptions.hasOwnProperty('keys')) { - const keysForInclude = restOptions.keys.split(',').filter((key) => { - // At least 2 components - return key.split(".").length > 1; - }).map((key) => { - // Slice the last component (a.b.c -> a.b) - // Otherwise we'll include one level too much. - return key.slice(0, key.lastIndexOf(".")); - }).join(','); + const keysForInclude = restOptions.keys + .split(',') + .filter(key => { + // At least 2 components + return key.split('.').length > 1; + }) + .map(key => { + // Slice the last component (a.b.c -> a.b) + // Otherwise we'll include one level too much. + return key.slice(0, key.lastIndexOf('.')); + }) + .join(','); // Concat the possibly present include string with the one from the keys // Dedup / sorting is handle in 'include' case. @@ -73,79 +88,83 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl if (!restOptions.include || restOptions.include.length == 0) { restOptions.include = keysForInclude; } else { - restOptions.include += "," + keysForInclude; + restOptions.include += ',' + keysForInclude; } } } for (var option in restOptions) { - switch(option) { - case 'keys': { - const keys = restOptions.keys.split(',').concat(AlwaysSelectedKeys); - this.keys = Array.from(new Set(keys)); - break; - } - case 'count': - this.doCount = true; - break; - case 'includeAll': - this.includeAll = true; - break; - case 'distinct': - case 'pipeline': - case 'skip': - case 'limit': - case 'readPreference': - this.findOptions[option] = restOptions[option]; - break; - case 'order': - var fields = restOptions.order.split(','); - this.findOptions.sort = fields.reduce((sortMap, field) => { - field = field.trim(); - if (field === '$score') { - sortMap.score = {$meta: 'textScore'}; - } else if (field[0] == '-') { - sortMap[field.slice(1)] = -1; - } else { - sortMap[field] = 1; - } - return sortMap; - }, {}); - break; - case 'include': { - const paths = restOptions.include.split(','); - if (paths.includes('*')) { + switch (option) { + case 'keys': { + const keys = restOptions.keys.split(',').concat(AlwaysSelectedKeys); + this.keys = Array.from(new Set(keys)); + break; + } + case 'count': + this.doCount = true; + break; + case 'includeAll': this.includeAll = true; break; + case 'distinct': + case 'pipeline': + case 'skip': + case 'limit': + case 'readPreference': + this.findOptions[option] = restOptions[option]; + break; + case 'order': + var fields = restOptions.order.split(','); + this.findOptions.sort = fields.reduce((sortMap, field) => { + field = field.trim(); + if (field === '$score') { + sortMap.score = { $meta: 'textScore' }; + } else if (field[0] == '-') { + sortMap[field.slice(1)] = -1; + } else { + sortMap[field] = 1; + } + return sortMap; + }, {}); + break; + case 'include': { + const paths = restOptions.include.split(','); + if (paths.includes('*')) { + this.includeAll = true; + break; + } + // Load the existing includes (from keys) + const pathSet = paths.reduce((memo, path) => { + // Split each paths on . (a.b.c -> [a,b,c]) + // reduce to create all paths + // ([a,b,c] -> {a: true, 'a.b': true, 'a.b.c': true}) + return path.split('.').reduce((memo, path, index, parts) => { + memo[parts.slice(0, index + 1).join('.')] = true; + return memo; + }, memo); + }, {}); + + this.include = Object.keys(pathSet) + .map(s => { + return s.split('.'); + }) + .sort((a, b) => { + return a.length - b.length; // Sort by number of components + }); + break; } - // Load the existing includes (from keys) - const pathSet = paths.reduce((memo, path) => { - // Split each paths on . (a.b.c -> [a,b,c]) - // reduce to create all paths - // ([a,b,c] -> {a: true, 'a.b': true, 'a.b.c': true}) - return path.split('.').reduce((memo, path, index, parts) => { - memo[parts.slice(0, index + 1).join('.')] = true; - return memo; - }, memo); - }, {}); - - this.include = Object.keys(pathSet).map((s) => { - return s.split('.'); - }).sort((a, b) => { - return a.length - b.length; // Sort by number of components - }); - break; - } - case 'redirectClassNameForKey': - this.redirectKey = restOptions.redirectClassNameForKey; - this.redirectClassName = null; - break; - case 'includeReadPreference': - case 'subqueryReadPreference': - break; - default: - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'bad option: ' + option); + case 'redirectClassNameForKey': + this.redirectKey = restOptions.redirectClassNameForKey; + this.redirectClassName = null; + break; + case 'includeReadPreference': + case 'subqueryReadPreference': + break; + default: + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'bad option: ' + option + ); } } } @@ -156,48 +175,63 @@ function RestQuery(config, auth, className, restWhere = {}, restOptions = {}, cl // 'results' and 'count'. // TODO: consolidate the replaceX functions RestQuery.prototype.execute = function(executeOptions) { - return Promise.resolve().then(() => { - return this.buildRestWhere(); - }).then(() => { - return this.handleIncludeAll(); - }).then(() => { - return this.runFind(executeOptions); - }).then(() => { - return this.runCount(); - }).then(() => { - return this.handleInclude(); - }).then(() => { - return this.runAfterFindTrigger(); - }).then(() => { - return this.response; - }); + return Promise.resolve() + .then(() => { + return this.buildRestWhere(); + }) + .then(() => { + return this.handleIncludeAll(); + }) + .then(() => { + return this.runFind(executeOptions); + }) + .then(() => { + return this.runCount(); + }) + .then(() => { + return this.handleInclude(); + }) + .then(() => { + return this.runAfterFindTrigger(); + }) + .then(() => { + return this.response; + }); }; RestQuery.prototype.buildRestWhere = function() { - return Promise.resolve().then(() => { - return this.getUserAndRoleACL(); - }).then(() => { - return this.redirectClassNameForKey(); - }).then(() => { - return this.validateClientClassCreation(); - }).then(() => { - return this.replaceSelect(); - }).then(() => { - return this.replaceDontSelect(); - }).then(() => { - return this.replaceInQuery(); - }).then(() => { - return this.replaceNotInQuery(); - }).then(() => { - return this.replaceEquality(); - }); -} + return Promise.resolve() + .then(() => { + return this.getUserAndRoleACL(); + }) + .then(() => { + return this.redirectClassNameForKey(); + }) + .then(() => { + return this.validateClientClassCreation(); + }) + .then(() => { + return this.replaceSelect(); + }) + .then(() => { + return this.replaceDontSelect(); + }) + .then(() => { + return this.replaceInQuery(); + }) + .then(() => { + return this.replaceNotInQuery(); + }) + .then(() => { + return this.replaceEquality(); + }); +}; // Marks the query for a write attempt, so we read the proper ACL (write instead of read) RestQuery.prototype.forWrite = function() { this.isWrite = true; return this; -} +}; // Uses the Auth object to get the list of roles, adds the user id RestQuery.prototype.getUserAndRoleACL = function() { @@ -208,8 +242,10 @@ RestQuery.prototype.getUserAndRoleACL = function() { this.findOptions.acl = ['*']; if (this.auth.user) { - return this.auth.getUserRoles().then((roles) => { - this.findOptions.acl = this.findOptions.acl.concat(roles, [this.auth.user.id]); + return this.auth.getUserRoles().then(roles => { + this.findOptions.acl = this.findOptions.acl.concat(roles, [ + this.auth.user.id, + ]); return; }); } else { @@ -225,8 +261,9 @@ RestQuery.prototype.redirectClassNameForKey = function() { } // We need to change the class name based on the schema - return this.config.database.redirectClassNameForKey(this.className, this.redirectKey) - .then((newClassName) => { + return this.config.database + .redirectClassNameForKey(this.className, this.redirectKey) + .then(newClassName => { this.className = newClassName; this.redirectClassName = newClassName; }); @@ -234,15 +271,22 @@ RestQuery.prototype.redirectClassNameForKey = function() { // Validates this operation against the allowClientClassCreation config. RestQuery.prototype.validateClientClassCreation = function() { - if (this.config.allowClientClassCreation === false && !this.auth.isMaster - && SchemaController.systemClasses.indexOf(this.className) === -1) { - return this.config.database.loadSchema() + if ( + this.config.allowClientClassCreation === false && + !this.auth.isMaster && + 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, + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, 'This user is not allowed to access ' + - 'non-existent class: ' + this.className); + 'non-existent class: ' + + this.className + ); } }); } else { @@ -256,7 +300,7 @@ function transformInQuery(inQueryObject, className, results) { values.push({ __type: 'Pointer', className: className, - objectId: result.objectId + objectId: result.objectId, }); } delete inQueryObject['$inQuery']; @@ -280,12 +324,14 @@ RestQuery.prototype.replaceInQuery = function() { // The inQuery value must have precisely two keys - where and className var inQueryValue = inQueryObject['$inQuery']; if (!inQueryValue.where || !inQueryValue.className) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, - 'improper usage of $inQuery'); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'improper usage of $inQuery' + ); } const additionalOptions = { - redirectClassNameForKey: inQueryValue.redirectClassNameForKey + redirectClassNameForKey: inQueryValue.redirectClassNameForKey, }; if (this.restOptions.subqueryReadPreference) { @@ -294,9 +340,13 @@ RestQuery.prototype.replaceInQuery = function() { } var subquery = new RestQuery( - this.config, this.auth, inQueryValue.className, - inQueryValue.where, additionalOptions); - return subquery.execute().then((response) => { + this.config, + this.auth, + inQueryValue.className, + inQueryValue.where, + additionalOptions + ); + return subquery.execute().then(response => { transformInQuery(inQueryObject, subquery.className, response.results); // Recurse to repeat return this.replaceInQuery(); @@ -309,7 +359,7 @@ function transformNotInQuery(notInQueryObject, className, results) { values.push({ __type: 'Pointer', className: className, - objectId: result.objectId + objectId: result.objectId, }); } delete notInQueryObject['$notInQuery']; @@ -333,12 +383,14 @@ RestQuery.prototype.replaceNotInQuery = function() { // The notInQuery value must have precisely two keys - where and className var notInQueryValue = notInQueryObject['$notInQuery']; if (!notInQueryValue.where || !notInQueryValue.className) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, - 'improper usage of $notInQuery'); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'improper usage of $notInQuery' + ); } const additionalOptions = { - redirectClassNameForKey: notInQueryValue.redirectClassNameForKey + redirectClassNameForKey: notInQueryValue.redirectClassNameForKey, }; if (this.restOptions.subqueryReadPreference) { @@ -347,19 +399,23 @@ RestQuery.prototype.replaceNotInQuery = function() { } var subquery = new RestQuery( - this.config, this.auth, notInQueryValue.className, - notInQueryValue.where, additionalOptions); - return subquery.execute().then((response) => { + this.config, + this.auth, + notInQueryValue.className, + notInQueryValue.where, + additionalOptions + ); + return subquery.execute().then(response => { transformNotInQuery(notInQueryObject, subquery.className, response.results); // Recurse to repeat return this.replaceNotInQuery(); }); }; -const transformSelect = (selectObject, key ,objects) => { +const transformSelect = (selectObject, key, objects) => { var values = []; for (var result of objects) { - values.push(key.split('.').reduce((o,i)=>o[i], result)); + values.push(key.split('.').reduce((o, i) => o[i], result)); } delete selectObject['$select']; if (Array.isArray(selectObject['$in'])) { @@ -367,7 +423,7 @@ const transformSelect = (selectObject, key ,objects) => { } else { selectObject['$in'] = values; } -} +}; // Replaces a $select clause by running the subquery, if there is a // $select clause. @@ -383,17 +439,21 @@ RestQuery.prototype.replaceSelect = function() { // The select value must have precisely two keys - query and key var selectValue = selectObject['$select']; // iOS SDK don't send where if not set, let it pass - if (!selectValue.query || - !selectValue.key || - typeof selectValue.query !== 'object' || - !selectValue.query.className || - Object.keys(selectValue).length !== 2) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, - 'improper usage of $select'); + if ( + !selectValue.query || + !selectValue.key || + typeof selectValue.query !== 'object' || + !selectValue.query.className || + Object.keys(selectValue).length !== 2 + ) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'improper usage of $select' + ); } const additionalOptions = { - redirectClassNameForKey: selectValue.query.redirectClassNameForKey + redirectClassNameForKey: selectValue.query.redirectClassNameForKey, }; if (this.restOptions.subqueryReadPreference) { @@ -402,19 +462,23 @@ RestQuery.prototype.replaceSelect = function() { } var subquery = new RestQuery( - this.config, this.auth, selectValue.query.className, - selectValue.query.where, additionalOptions); - return subquery.execute().then((response) => { + this.config, + this.auth, + selectValue.query.className, + selectValue.query.where, + additionalOptions + ); + return subquery.execute().then(response => { 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(key.split('.').reduce((o,i)=>o[i], result)); + values.push(key.split('.').reduce((o, i) => o[i], result)); } delete dontSelectObject['$dontSelect']; if (Array.isArray(dontSelectObject['$nin'])) { @@ -422,7 +486,7 @@ const transformDontSelect = (dontSelectObject, key, objects) => { } else { dontSelectObject['$nin'] = values; } -} +}; // Replaces a $dontSelect clause by running the subquery, if there is a // $dontSelect clause. @@ -437,16 +501,20 @@ RestQuery.prototype.replaceDontSelect = function() { // The dontSelect value must have precisely two keys - query and key var dontSelectValue = dontSelectObject['$dontSelect']; - if (!dontSelectValue.query || - !dontSelectValue.key || - typeof dontSelectValue.query !== 'object' || - !dontSelectValue.query.className || - Object.keys(dontSelectValue).length !== 2) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, - 'improper usage of $dontSelect'); + if ( + !dontSelectValue.query || + !dontSelectValue.key || + typeof dontSelectValue.query !== 'object' || + !dontSelectValue.query.className || + Object.keys(dontSelectValue).length !== 2 + ) { + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'improper usage of $dontSelect' + ); } const additionalOptions = { - redirectClassNameForKey: dontSelectValue.query.redirectClassNameForKey + redirectClassNameForKey: dontSelectValue.query.redirectClassNameForKey, }; if (this.restOptions.subqueryReadPreference) { @@ -455,16 +523,24 @@ RestQuery.prototype.replaceDontSelect = function() { } var subquery = new RestQuery( - this.config, this.auth, dontSelectValue.query.className, - dontSelectValue.query.where, additionalOptions); - return subquery.execute().then((response) => { - transformDontSelect(dontSelectObject, dontSelectValue.key, response.results); + this.config, + this.auth, + dontSelectValue.query.className, + dontSelectValue.query.where, + additionalOptions + ); + return subquery.execute().then(response => { + transformDontSelect( + dontSelectObject, + dontSelectValue.key, + response.results + ); // Keep replacing $dontSelect clauses return this.replaceDontSelect(); - }) + }); }; -const cleanResultOfSensitiveUserInfo = function (result, auth, config) { +const cleanResultOfSensitiveUserInfo = function(result, auth, config) { delete result.password; if (auth.isMaster || (auth.user && auth.user.id === result.objectId)) { @@ -476,9 +552,9 @@ const cleanResultOfSensitiveUserInfo = function (result, auth, config) { } }; -const cleanResultAuthData = function (result) { +const cleanResultAuthData = function(result) { if (result.authData) { - Object.keys(result.authData).forEach((provider) => { + Object.keys(result.authData).forEach(provider => { if (result.authData[provider] === null) { delete result.authData[provider]; } @@ -490,7 +566,7 @@ const cleanResultAuthData = function (result) { } }; -const replaceEqualityConstraint = (constraint) => { +const replaceEqualityConstraint = constraint => { if (typeof constraint !== 'object') { return constraint; } @@ -507,12 +583,12 @@ const replaceEqualityConstraint = (constraint) => { } if (hasDirectConstraint && hasOperatorConstraint) { constraint['$eq'] = equalToObject; - Object.keys(equalToObject).forEach((key) => { + Object.keys(equalToObject).forEach(key => { delete constraint[key]; }); } return constraint; -} +}; RestQuery.prototype.replaceEquality = function() { if (typeof this.restWhere !== 'object') { @@ -521,18 +597,18 @@ RestQuery.prototype.replaceEquality = function() { for (const key in this.restWhere) { this.restWhere[key] = replaceEqualityConstraint(this.restWhere[key]); } -} +}; // Returns a promise for whether it was successful. // Populates this.response with an object that only has 'results'. RestQuery.prototype.runFind = function(options = {}) { if (this.findOptions.limit === 0) { - this.response = {results: []}; + this.response = { results: [] }; return Promise.resolve(); } const findOptions = Object.assign({}, this.findOptions); if (this.keys) { - findOptions.keys = this.keys.map((key) => { + findOptions.keys = this.keys.map(key => { return key.split('.')[0]; }); } @@ -542,8 +618,9 @@ RestQuery.prototype.runFind = function(options = {}) { if (this.isWrite) { findOptions.isWrite = true; } - return this.config.database.find(this.className, this.restWhere, findOptions) - .then((results) => { + return this.config.database + .find(this.className, this.restWhere, findOptions) + .then(results => { if (this.className === '_User') { for (var result of results) { cleanResultOfSensitiveUserInfo(result, this.auth, this.config); @@ -558,7 +635,7 @@ RestQuery.prototype.runFind = function(options = {}) { r.className = this.redirectClassName; } } - this.response = {results: results}; + this.response = { results: results }; }); }; @@ -571,8 +648,9 @@ RestQuery.prototype.runCount = function() { this.findOptions.count = true; delete this.findOptions.skip; delete this.findOptions.limit; - return this.config.database.find(this.className, this.restWhere, this.findOptions) - .then((c) => { + return this.config.database + .find(this.className, this.restWhere, this.findOptions) + .then(c => { this.response.count = c; }); }; @@ -582,13 +660,17 @@ RestQuery.prototype.handleIncludeAll = function() { if (!this.includeAll) { return; } - return this.config.database.loadSchema() + return this.config.database + .loadSchema() .then(schemaController => schemaController.getOneSchema(this.className)) .then(schema => { const includeFields = []; const keyFields = []; for (const field in schema.fields) { - if (schema.fields[field].type && schema.fields[field].type === 'Pointer') { + if ( + schema.fields[field].type && + schema.fields[field].type === 'Pointer' + ) { includeFields.push([field]); keyFields.push(field); } @@ -608,10 +690,15 @@ RestQuery.prototype.handleInclude = function() { return; } - var pathResponse = includePath(this.config, this.auth, - this.response, this.include[0], this.restOptions); + var pathResponse = includePath( + this.config, + this.auth, + this.response, + this.include[0], + this.restOptions + ); if (pathResponse.then) { - return pathResponse.then((newResponse) => { + return pathResponse.then(newResponse => { this.response = newResponse; this.include = this.include.slice(1); return this.handleInclude(); @@ -630,7 +717,11 @@ RestQuery.prototype.runAfterFindTrigger = function() { return; } // Avoid doing any setup for triggers if there is no 'afterFind' trigger for this class. - const hasAfterFindHook = triggers.triggerExists(this.className, triggers.Types.afterFind, this.config.applicationId); + const hasAfterFindHook = triggers.triggerExists( + this.className, + triggers.Types.afterFind, + this.config.applicationId + ); if (!hasAfterFindHook) { return Promise.resolve(); } @@ -639,20 +730,28 @@ RestQuery.prototype.runAfterFindTrigger = function() { return Promise.resolve(); } // Run afterFind trigger and set the new results - return triggers.maybeRunAfterFindTrigger(triggers.Types.afterFind, this.auth, this.className,this.response.results, this.config).then((results) => { - // Ensure we properly set the className back - if (this.redirectClassName) { - this.response.results = results.map((object) => { - if (object instanceof Parse.Object) { - object = object.toJSON(); - } - object.className = this.redirectClassName; - return object; - }); - } else { - this.response.results = results; - } - }); + return triggers + .maybeRunAfterFindTrigger( + triggers.Types.afterFind, + this.auth, + this.className, + this.response.results, + this.config + ) + .then(results => { + // Ensure we properly set the className back + if (this.redirectClassName) { + this.response.results = results.map(object => { + if (object instanceof Parse.Object) { + object = object.toJSON(); + } + object.className = this.redirectClassName; + return object; + }); + } else { + this.response.results = results; + } + }); }; // Adds included values to the response. @@ -698,42 +797,49 @@ function includePath(config, auth, response, path, restOptions = {}) { if (restOptions.includeReadPreference) { includeRestOptions.readPreference = restOptions.includeReadPreference; - includeRestOptions.includeReadPreference = restOptions.includeReadPreference; + includeRestOptions.includeReadPreference = + restOptions.includeReadPreference; } - const queryPromises = Object.keys(pointersHash).map((className) => { + const queryPromises = Object.keys(pointersHash).map(className => { const objectIds = Array.from(pointersHash[className]); let where; if (objectIds.length === 1) { - where = {'objectId': objectIds[0]}; + where = { objectId: objectIds[0] }; } else { - where = {'objectId': {'$in': objectIds}}; + where = { objectId: { $in: objectIds } }; } - var query = new RestQuery(config, auth, className, where, includeRestOptions); - return query.execute({op: 'get'}).then((results) => { + var query = new RestQuery( + config, + auth, + className, + where, + includeRestOptions + ); + return query.execute({ op: 'get' }).then(results => { results.className = className; return Promise.resolve(results); - }) - }) + }); + }); // Get the objects for all these object ids - return Promise.all(queryPromises).then((responses) => { + 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 (obj.className == "_User" && !auth.isMaster) { + if (obj.className == '_User' && !auth.isMaster) { delete obj.sessionToken; delete obj.authData; } replace[obj.objectId] = obj; } return replace; - }, {}) + }, {}); var resp = { - results: replacePointers(response.results, path, replace) + results: replacePointers(response.results, path, replace), }; if (response.count) { resp.count = response.count; @@ -782,8 +888,9 @@ function findPointers(object, path) { // pointers inflated. function replacePointers(object, path, replace) { if (object instanceof Array) { - return object.map((obj) => replacePointers(obj, path, replace)) - .filter((obj) => typeof obj !== 'undefined'); + return object + .map(obj => replacePointers(obj, path, replace)) + .filter(obj => typeof obj !== 'undefined'); } if (typeof object !== 'object' || !object) { diff --git a/src/RestWrite.js b/src/RestWrite.js index e7bb8eefd6..59d77356f2 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -12,8 +12,8 @@ var Parse = require('parse/node'); var triggers = require('./triggers'); var ClientSDK = require('./ClientSDK'); import RestQuery from './RestQuery'; -import _ from 'lodash'; -import logger from './logger'; +import _ from 'lodash'; +import logger from './logger'; // query and data are both provided in REST API format. So data // types are encoded by plain old objects. @@ -24,9 +24,20 @@ import logger from './logger'; // RestWrite will handle objectId, createdAt, and updatedAt for // everything. It also knows to use triggers and special modifications // for the _User class. -function RestWrite(config, auth, className, query, data, originalData, clientSDK) { +function RestWrite( + config, + auth, + className, + query, + data, + originalData, + clientSDK +) { if (auth.isReadOnly) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Cannot perform a write operation when using readOnlyMasterKey'); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + 'Cannot perform a write operation when using readOnlyMasterKey' + ); } this.config = config; this.auth = auth; @@ -36,7 +47,10 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK this.runOptions = {}; this.context = {}; 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 @@ -62,41 +76,58 @@ function RestWrite(config, auth, className, query, data, originalData, clientSDK // Returns a promise for a {response, status, location} object. // status and location are optional. RestWrite.prototype.execute = function() { - return Promise.resolve().then(() => { - return this.getUserAndRoleACL(); - }).then(() => { - return this.validateClientClassCreation(); - }).then(() => { - return this.handleInstallation(); - }).then(() => { - return this.handleSession(); - }).then(() => { - return this.validateAuthData(); - }).then(() => { - return this.runBeforeTrigger(); - }).then(() => { - return this.validateSchema(); - }).then(() => { - return this.setRequiredFieldsIfNeeded(); - }).then(() => { - return this.transformUser(); - }).then(() => { - return this.expandFilesForExistingObjects(); - }).then(() => { - return this.destroyDuplicatedSessions(); - }).then(() => { - return this.runDatabaseOperation(); - }).then(() => { - return this.createSessionTokenIfNeeded(); - }).then(() => { - return this.handleFollowup(); - }).then(() => { - return this.runAfterTrigger(); - }).then(() => { - return this.cleanUserAuthData(); - }).then(() => { - return this.response; - }) + return Promise.resolve() + .then(() => { + return this.getUserAndRoleACL(); + }) + .then(() => { + return this.validateClientClassCreation(); + }) + .then(() => { + return this.handleInstallation(); + }) + .then(() => { + return this.handleSession(); + }) + .then(() => { + return this.validateAuthData(); + }) + .then(() => { + return this.runBeforeTrigger(); + }) + .then(() => { + return this.validateSchema(); + }) + .then(() => { + return this.setRequiredFieldsIfNeeded(); + }) + .then(() => { + return this.transformUser(); + }) + .then(() => { + return this.expandFilesForExistingObjects(); + }) + .then(() => { + return this.destroyDuplicatedSessions(); + }) + .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 @@ -108,8 +139,10 @@ RestWrite.prototype.getUserAndRoleACL = function() { this.runOptions.acl = ['*']; if (this.auth.user) { - return this.auth.getUserRoles().then((roles) => { - this.runOptions.acl = this.runOptions.acl.concat(roles, [this.auth.user.id]); + return this.auth.getUserRoles().then(roles => { + this.runOptions.acl = this.runOptions.acl.concat(roles, [ + this.auth.user.id, + ]); return; }); } else { @@ -119,15 +152,22 @@ RestWrite.prototype.getUserAndRoleACL = function() { // Validates this operation against the allowClientClassCreation config. RestWrite.prototype.validateClientClassCreation = function() { - if (this.config.allowClientClassCreation === false && !this.auth.isMaster - && SchemaController.systemClasses.indexOf(this.className) === -1) { - return this.config.database.loadSchema() + if ( + this.config.allowClientClassCreation === false && + !this.auth.isMaster && + 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, + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, 'This user is not allowed to access ' + - 'non-existent class: ' + this.className); + 'non-existent class: ' + + this.className + ); } }); } else { @@ -137,7 +177,12 @@ RestWrite.prototype.validateClientClassCreation = function() { // Validates this operation against the schema. RestWrite.prototype.validateSchema = function() { - return this.config.database.validateObject(this.className, this.data, this.query, this.runOptions); + return this.config.database.validateObject( + this.className, + this.data, + this.query, + this.runOptions + ); }; // Runs any beforeSave triggers against this operation. @@ -148,12 +193,18 @@ RestWrite.prototype.runBeforeTrigger = function() { } // Avoid doing any setup for triggers if there is no 'beforeSave' trigger for this class. - if (!triggers.triggerExists(this.className, triggers.Types.beforeSave, this.config.applicationId)) { + if ( + !triggers.triggerExists( + this.className, + triggers.Types.beforeSave, + this.config.applicationId + ) + ) { return Promise.resolve(); } // Cloud code gets a bit of extra data for its objects - var extraData = {className: this.className}; + var extraData = { className: this.className }; if (this.query && this.query.objectId) { extraData.objectId = this.query.objectId; } @@ -165,23 +216,36 @@ RestWrite.prototype.runBeforeTrigger = function() { originalObject = triggers.inflate(extraData, this.originalData); } - return Promise.resolve().then(() => { - return triggers.maybeRunTrigger(triggers.Types.beforeSave, this.auth, updatedObject, originalObject, this.config, this.context); - }).then((response) => { - if (response && response.object) { - this.storage.fieldsChangedByTrigger = _.reduce(response.object, (result, value, key) => { - if (!_.isEqual(this.data[key], value)) { - result.push(key); + return Promise.resolve() + .then(() => { + return triggers.maybeRunTrigger( + triggers.Types.beforeSave, + this.auth, + updatedObject, + originalObject, + this.config, + this.context + ); + }) + .then(response => { + if (response && response.object) { + this.storage.fieldsChangedByTrigger = _.reduce( + response.object, + (result, value, key) => { + if (!_.isEqual(this.data[key], value)) { + result.push(key); + } + return result; + }, + [] + ); + this.data = response.object; + // We should delete the objectId for an update write + if (this.query && this.query.objectId) { + delete this.data.objectId; } - return result; - }, []); - this.data = response.object; - // We should delete the objectId for an update write - if (this.query && this.query.objectId) { - delete this.data.objectId } - } - }); + }); }; RestWrite.prototype.setRequiredFieldsIfNeeded = function() { @@ -209,13 +273,23 @@ RestWrite.prototype.validateAuthData = function() { } if (!this.query && !this.data.authData) { - if (typeof this.data.username !== 'string' || _.isEmpty(this.data.username)) { - throw new Parse.Error(Parse.Error.USERNAME_MISSING, - 'bad or missing username'); + if ( + typeof this.data.username !== 'string' || + _.isEmpty(this.data.username) + ) { + throw new Parse.Error( + Parse.Error.USERNAME_MISSING, + 'bad or missing username' + ); } - if (typeof this.data.password !== 'string' || _.isEmpty(this.data.password)) { - throw new Parse.Error(Parse.Error.PASSWORD_MISSING, - 'password is required'); + if ( + typeof this.data.password !== 'string' || + _.isEmpty(this.data.password) + ) { + throw new Parse.Error( + Parse.Error.PASSWORD_MISSING, + 'password is required' + ); } } @@ -228,78 +302,86 @@ RestWrite.prototype.validateAuthData = function() { if (providers.length > 0) { const canHandleAuthData = providers.reduce((canHandle, provider) => { var providerAuthData = authData[provider]; - var hasToken = (providerAuthData && providerAuthData.id); + var hasToken = providerAuthData && providerAuthData.id; return canHandle && (hasToken || providerAuthData == null); }, true); if (canHandleAuthData) { return this.handleAuthData(authData); } } - throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, - 'This authentication method is unsupported.'); + throw new Parse.Error( + Parse.Error.UNSUPPORTED_SERVICE, + 'This authentication method is unsupported.' + ); }; RestWrite.prototype.handleAuthDataValidation = function(authData) { - const validations = Object.keys(authData).map((provider) => { + const validations = Object.keys(authData).map(provider => { if (authData[provider] === null) { return Promise.resolve(); } - const validateAuthData = this.config.authDataManager.getValidatorForProvider(provider); + const validateAuthData = this.config.authDataManager.getValidatorForProvider( + provider + ); if (!validateAuthData) { - throw new Parse.Error(Parse.Error.UNSUPPORTED_SERVICE, - 'This authentication method is unsupported.'); + throw new Parse.Error( + Parse.Error.UNSUPPORTED_SERVICE, + 'This authentication method is unsupported.' + ); } return validateAuthData(authData[provider]); }); return Promise.all(validations); -} +}; RestWrite.prototype.findUsersWithAuthData = function(authData) { const providers = Object.keys(authData); - const query = providers.reduce((memo, provider) => { - if (!authData[provider]) { + const query = providers + .reduce((memo, provider) => { + if (!authData[provider]) { + return memo; + } + const queryKey = `authData.${provider}.id`; + const query = {}; + query[queryKey] = authData[provider].id; + memo.push(query); return memo; - } - const queryKey = `authData.${provider}.id`; - const query = {}; - query[queryKey] = authData[provider].id; - memo.push(query); - return memo; - }, []).filter((q) => { - return typeof q !== 'undefined'; - }); + }, []) + .filter(q => { + return typeof q !== 'undefined'; + }); let findPromise = Promise.resolve([]); if (query.length > 0) { - findPromise = this.config.database.find( - this.className, - {'$or': query}, {}) + findPromise = this.config.database.find(this.className, { $or: query }, {}); } return findPromise; -} +}; RestWrite.prototype.filteredObjectsByACL = function(objects) { if (this.auth.isMaster) { return objects; } - return objects.filter((object) => { + return objects.filter(object => { if (!object.ACL) { return true; // legacy users that have no ACL field on them } // Regular users that have been locked out. return object.ACL && Object.keys(object.ACL).length > 0; }); -} +}; RestWrite.prototype.handleAuthData = function(authData) { let results; - return this.findUsersWithAuthData(authData).then((r) => { + return this.findUsersWithAuthData(authData).then(r => { results = this.filteredObjectsByACL(r); if (results.length > 1) { // More than 1 user with the passed id's - throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, - 'this auth is already used'); + throw new Parse.Error( + Parse.Error.ACCOUNT_ALREADY_LINKED, + 'this auth is already used' + ); } this.storage['authProvider'] = Object.keys(authData).join(','); @@ -307,7 +389,7 @@ RestWrite.prototype.handleAuthData = function(authData) { if (results.length > 0) { const userResult = results[0]; const mutatedAuthData = {}; - Object.keys(authData).forEach((provider) => { + Object.keys(authData).forEach(provider => { const providerData = authData[provider]; const userAuthData = userResult.authData[provider]; if (!_.isEqual(providerData, userAuthData)) { @@ -321,7 +403,8 @@ RestWrite.prototype.handleAuthData = function(authData) { } else if (this.auth && this.auth.user && this.auth.user.id) { userId = this.auth.user.id; } - if (!userId || userId === userResult.objectId) { // no user making the call + if (!userId || userId === userResult.objectId) { + // no user making the call // OR the user making the call is the right one // Login with auth data delete results[0].password; @@ -329,10 +412,11 @@ RestWrite.prototype.handleAuthData = function(authData) { // need to set the objectId first otherwise location has trailing undefined this.data.objectId = userResult.objectId; - if (!this.query || !this.query.objectId) { // this a login call, no userId passed + if (!this.query || !this.query.objectId) { + // this a login call, no userId passed this.response = { response: userResult, - location: this.location() + location: this.location(), }; } // If we didn't change the auth data, just keep going @@ -350,21 +434,29 @@ RestWrite.prototype.handleAuthData = function(authData) { // If we're not logging in, but just updating the current user, we can safely skip that part if (this.response) { // Assign the new authData in the response - Object.keys(mutatedAuthData).forEach((provider) => { - this.response.response.authData[provider] = mutatedAuthData[provider]; + Object.keys(mutatedAuthData).forEach(provider => { + this.response.response.authData[provider] = + mutatedAuthData[provider]; }); // Run the DB update directly, as 'master' // Just update the authData part // Then we're good for the user, early exit of sorts - return this.config.database.update(this.className, {objectId: this.data.objectId}, {authData: mutatedAuthData}, {}); + return this.config.database.update( + this.className, + { objectId: this.data.objectId }, + { authData: mutatedAuthData }, + {} + ); } }); } else if (userId) { // Trying to update auth data but users // are different if (userResult.objectId !== userId) { - throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, - 'this auth is already used'); + throw new Parse.Error( + Parse.Error.ACCOUNT_ALREADY_LINKED, + 'this auth is already used' + ); } // No auth data was mutated, just keep going if (!hasMutatedAuthData) { @@ -374,8 +466,7 @@ RestWrite.prototype.handleAuthData = function(authData) { } return this.handleAuthDataValidation(authData); }); -} - +}; // The non-third-party parts of User transformation RestWrite.prototype.transformUser = function() { @@ -385,8 +476,8 @@ RestWrite.prototype.transformUser = function() { return promise; } - if (!this.auth.isMaster && "emailVerified" in this.data) { - const error = `Clients aren't allowed to manually update email verification.` + if (!this.auth.isMaster && 'emailVerified' in this.data) { + const error = `Clients aren't allowed to manually update email verification.`; throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); } @@ -396,45 +487,51 @@ RestWrite.prototype.transformUser = function() { // session tokens, and remove them from the cache. promise = new RestQuery(this.config, Auth.master(this.config), '_Session', { user: { - __type: "Pointer", - className: "_User", + __type: 'Pointer', + className: '_User', objectId: this.objectId(), - } - }).execute() + }, + }) + .execute() .then(results => { - results.results.forEach(session => this.config.cacheController.user.del(session.sessionToken)); + results.results.forEach(session => + this.config.cacheController.user.del(session.sessionToken) + ); }); } - return promise.then(() => { - // Transform the password - if (this.data.password === undefined) { // ignore only if undefined. should proceed if empty ('') - return Promise.resolve(); - } + return promise + .then(() => { + // Transform the password + if (this.data.password === undefined) { + // ignore only if undefined. should proceed if empty ('') + return Promise.resolve(); + } - if (this.query) { - this.storage['clearSessions'] = true; - // Generate a new session only if the user requested - if (!this.auth.isMaster) { - this.storage['generateNewSession'] = true; + if (this.query) { + this.storage['clearSessions'] = true; + // Generate a new session only if the user requested + if (!this.auth.isMaster) { + this.storage['generateNewSession'] = true; + } } - } - return this._validatePasswordPolicy().then(() => { - return passwordCrypto.hash(this.data.password).then((hashedPassword) => { - this.data._hashed_password = hashedPassword; - delete this.data.password; + return this._validatePasswordPolicy().then(() => { + return passwordCrypto.hash(this.data.password).then(hashedPassword => { + this.data._hashed_password = hashedPassword; + delete this.data.password; + }); }); + }) + .then(() => { + return this._validateUserName(); + }) + .then(() => { + return this._validateEmail(); }); - - }).then(() => { - return this._validateUserName(); - }).then(() => { - return this._validateEmail(); - }); }; -RestWrite.prototype._validateUserName = function () { +RestWrite.prototype._validateUserName = function() { // Check for username uniqueness if (!this.data.username) { if (!this.query) { @@ -445,16 +542,21 @@ RestWrite.prototype._validateUserName = function () { } // 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; - }); + 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; + }); }; RestWrite.prototype._validateEmail = function() { @@ -463,61 +565,84 @@ RestWrite.prototype._validateEmail = function() { } // Validate basic email address format if (!this.data.email.match(/^.+@.+$/)) { - return Promise.reject(new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'Email address format is invalid.')); + return Promise.reject( + new Parse.Error( + Parse.Error.INVALID_EMAIL_ADDRESS, + 'Email address format is invalid.' + ) + ); } // 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.'); - } - if ( - !this.data.authData || - !Object.keys(this.data.authData).length || - Object.keys(this.data.authData).length === 1 && Object.keys(this.data.authData)[0] === 'anonymous' - ) { - // We updated the email, send a new validation - this.storage['sendVerificationEmail'] = true; - this.config.userController.setEmailVerifyToken(this.data); - } - }); + 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.' + ); + } + if ( + !this.data.authData || + !Object.keys(this.data.authData).length || + (Object.keys(this.data.authData).length === 1 && + Object.keys(this.data.authData)[0] === 'anonymous') + ) { + // We updated the email, send a new validation + this.storage['sendVerificationEmail'] = true; + this.config.userController.setEmailVerifyToken(this.data); + } + }); }; RestWrite.prototype._validatePasswordPolicy = function() { - if (!this.config.passwordPolicy) - return Promise.resolve(); + if (!this.config.passwordPolicy) return Promise.resolve(); return this._validatePasswordRequirements().then(() => { return this._validatePasswordHistory(); }); }; - RestWrite.prototype._validatePasswordRequirements = function() { // check if the password conforms to the defined password policy if configured - const policyError = 'Password does not meet the Password Policy requirements.'; + const policyError = + 'Password does not meet the Password Policy requirements.'; // check whether the password meets the password strength requirements - if (this.config.passwordPolicy.patternValidator && !this.config.passwordPolicy.patternValidator(this.data.password) || - this.config.passwordPolicy.validatorCallback && !this.config.passwordPolicy.validatorCallback(this.data.password)) { - return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError)); + if ( + (this.config.passwordPolicy.patternValidator && + !this.config.passwordPolicy.patternValidator(this.data.password)) || + (this.config.passwordPolicy.validatorCallback && + !this.config.passwordPolicy.validatorCallback(this.data.password)) + ) { + return Promise.reject( + new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError) + ); } // check whether password contain username if (this.config.passwordPolicy.doNotAllowUsername === true) { - if (this.data.username) { // username is not passed during password reset + if (this.data.username) { + // username is not passed during password reset if (this.data.password.indexOf(this.data.username) >= 0) - return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError)); - } else { // retrieve the User object using objectId during password reset - return this.config.database.find('_User', {objectId: this.objectId()}) + return Promise.reject( + new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError) + ); + } else { + // retrieve the User object using objectId during password reset + return this.config.database + .find('_User', { objectId: this.objectId() }) .then(results => { if (results.length != 1) { throw undefined; } if (this.data.password.indexOf(results[0].username) >= 0) - return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError)); + return Promise.reject( + new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError) + ); return Promise.resolve(); }); } @@ -528,7 +653,12 @@ RestWrite.prototype._validatePasswordRequirements = function() { RestWrite.prototype._validatePasswordHistory = function() { // check whether password is repeating from specified history if (this.query && this.config.passwordPolicy.maxPasswordHistory) { - return this.config.database.find('_User', {objectId: this.objectId()}, {keys: ["_password_history", "_hashed_password"]}) + return this.config.database + .find( + '_User', + { objectId: this.objectId() }, + { keys: ['_password_history', '_hashed_password'] } + ) .then(results => { if (results.length != 1) { throw undefined; @@ -536,25 +666,39 @@ RestWrite.prototype._validatePasswordHistory = function() { const user = results[0]; let oldPasswords = []; if (user._password_history) - oldPasswords = _.take(user._password_history, this.config.passwordPolicy.maxPasswordHistory - 1); + oldPasswords = _.take( + user._password_history, + this.config.passwordPolicy.maxPasswordHistory - 1 + ); oldPasswords.push(user.password); const newPassword = this.data.password; // compare the new password hash with all old password hashes - const promises = oldPasswords.map(function (hash) { - return passwordCrypto.compare(newPassword, hash).then((result) => { - if (result) // reject if there is a match - return Promise.reject("REPEAT_PASSWORD"); + const promises = oldPasswords.map(function(hash) { + return passwordCrypto.compare(newPassword, hash).then(result => { + if (result) + // reject if there is a match + return Promise.reject('REPEAT_PASSWORD'); return Promise.resolve(); - }) + }); }); // wait for all comparisons to complete - return Promise.all(promises).then(() => { - return Promise.resolve(); - }).catch(err => { - if (err === "REPEAT_PASSWORD") // a match was found - return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, `New password should not be the same as last ${this.config.passwordPolicy.maxPasswordHistory} passwords.`)); - throw err; - }); + return Promise.all(promises) + .then(() => { + return Promise.resolve(); + }) + .catch(err => { + if (err === 'REPEAT_PASSWORD') + // a match was found + return Promise.reject( + new Parse.Error( + Parse.Error.VALIDATION_ERROR, + `New password should not be the same as last ${ + this.config.passwordPolicy.maxPasswordHistory + } passwords.` + ) + ); + throw err; + }); }); } return Promise.resolve(); @@ -567,13 +711,16 @@ RestWrite.prototype.createSessionTokenIfNeeded = function() { if (this.query) { return; } - if (!this.storage['authProvider'] // signup call, with - && this.config.preventLoginWithUnverifiedEmail // no login without verification - && this.config.verifyUserEmails) { // verification is on + if ( + !this.storage['authProvider'] && // signup call, with + this.config.preventLoginWithUnverifiedEmail && // no login without verification + this.config.verifyUserEmails + ) { + // verification is on return; // do not create the session token in that case! } return this.createSessionToken(); -} +}; RestWrite.prototype.createSessionToken = function() { // cloud installationId from Cloud Code, @@ -582,14 +729,11 @@ RestWrite.prototype.createSessionToken = function() { return; } - const { - sessionData, - createSession, - } = Auth.createSession(this.config, { + const { sessionData, createSession } = Auth.createSession(this.config, { userId: this.objectId(), createdWith: { - 'action': this.storage['authProvider'] ? 'login' : 'signup', - 'authProvider': this.storage['authProvider'] || 'password' + action: this.storage['authProvider'] ? 'login' : 'signup', + authProvider: this.storage['authProvider'] || 'password', }, installationId: this.auth.installationId, }); @@ -599,7 +743,7 @@ RestWrite.prototype.createSessionToken = function() { } return createSession(); -} +}; RestWrite.prototype.destroyDuplicatedSessions = function() { // Only for _Session, and at creation time @@ -607,12 +751,8 @@ RestWrite.prototype.destroyDuplicatedSessions = function() { return; } // Destroy the sessions in 'Background' - const { - user, - installationId, - sessionToken, - } = this.data; - if (!user || !installationId) { + const { user, installationId, sessionToken } = this.data; + if (!user || !installationId) { return; } if (!user.objectId) { @@ -621,29 +761,33 @@ RestWrite.prototype.destroyDuplicatedSessions = function() { this.config.database.destroy('_Session', { user, installationId, - sessionToken: { '$ne': sessionToken }, + sessionToken: { $ne: sessionToken }, }); -} +}; // Handles any followup logic RestWrite.prototype.handleFollowup = function() { - if (this.storage && this.storage['clearSessions'] && this.config.revokeSessionOnPasswordReset) { + if ( + this.storage && + this.storage['clearSessions'] && + this.config.revokeSessionOnPasswordReset + ) { var sessionQuery = { user: { __type: 'Pointer', className: '_User', - objectId: this.objectId() - } + objectId: this.objectId(), + }, }; delete this.storage['clearSessions']; - return this.config.database.destroy('_Session', sessionQuery) + return this.config.database + .destroy('_Session', sessionQuery) .then(this.handleFollowup.bind(this)); } if (this.storage && this.storage['generateNewSession']) { delete this.storage['generateNewSession']; - return this.createSessionToken() - .then(this.handleFollowup.bind(this)); + return this.createSessionToken().then(this.handleFollowup.bind(this)); } if (this.storage && this.storage['sendVerificationEmail']) { @@ -662,18 +806,26 @@ RestWrite.prototype.handleSession = function() { } if (!this.auth.user && !this.auth.isMaster) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token required.'); + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Session token required.' + ); } // TODO: Verify proper error to throw if (this.data.ACL) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Cannot set ' + - 'ACL on a Session.'); + throw new Parse.Error( + Parse.Error.INVALID_KEY_NAME, + 'Cannot set ' + 'ACL on a Session.' + ); } if (this.query) { - if (this.data.user && !this.auth.isMaster && this.data.user.objectId != this.auth.user.id) { + if ( + this.data.user && + !this.auth.isMaster && + this.data.user.objectId != this.auth.user.id + ) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); } else if (this.data.installationId) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME); @@ -696,19 +848,21 @@ RestWrite.prototype.handleSession = function() { createdWith: { action: 'create', }, - additionalSessionData + additionalSessionData, }); - return createSession().then((results) => { + return createSession().then(results => { if (!results.response) { - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, - 'Error creating session.'); + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + 'Error creating session.' + ); } sessionData['objectId'] = results.response['objectId']; this.response = { status: 201, location: results.location, - response: sessionData + response: sessionData, }; }); } @@ -724,10 +878,17 @@ RestWrite.prototype.handleInstallation = function() { return; } - if (!this.query && !this.data.deviceToken && !this.data.installationId && !this.auth.installationId) { - throw new Parse.Error(135, + if ( + !this.query && + !this.data.deviceToken && + !this.data.installationId && + !this.auth.installationId + ) { + throw new Parse.Error( + 135, 'at least one ID field (deviceToken, installationId) ' + - 'must be specified in this operation'); + 'must be specified in this operation' + ); } // If the device token is 64 characters long, we assume it is for iOS @@ -753,8 +914,12 @@ RestWrite.prototype.handleInstallation = function() { } // Updating _Installation but not updating anything critical - if (this.query && !this.data.deviceToken - && !installationId && !this.data.deviceType) { + if ( + this.query && + !this.data.deviceToken && + !installationId && + !this.data.deviceType + ) { return; } @@ -769,111 +934,140 @@ RestWrite.prototype.handleInstallation = function() { const orQueries = []; if (this.query && this.query.objectId) { orQueries.push({ - objectId: this.query.objectId + objectId: this.query.objectId, }); } if (installationId) { orQueries.push({ - 'installationId': installationId + installationId: installationId, }); } if (this.data.deviceToken) { - orQueries.push({'deviceToken': this.data.deviceToken}); + orQueries.push({ deviceToken: this.data.deviceToken }); } if (orQueries.length == 0) { return; } - promise = promise.then(() => { - return this.config.database.find('_Installation', { - '$or': orQueries - }, {}); - }).then((results) => { - results.forEach((result) => { - if (this.query && this.query.objectId && result.objectId == this.query.objectId) { - objectIdMatch = result; - } - if (result.installationId == installationId) { - installationIdMatch = result; - } - if (result.deviceToken == this.data.deviceToken) { - deviceTokenMatches.push(result); - } - }); + promise = promise + .then(() => { + return this.config.database.find( + '_Installation', + { + $or: orQueries, + }, + {} + ); + }) + .then(results => { + results.forEach(result => { + if ( + this.query && + this.query.objectId && + result.objectId == this.query.objectId + ) { + objectIdMatch = result; + } + if (result.installationId == installationId) { + installationIdMatch = result; + } + if (result.deviceToken == this.data.deviceToken) { + deviceTokenMatches.push(result); + } + }); - // Sanity checks when running a query - if (this.query && this.query.objectId) { - if (!objectIdMatch) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found for update.'); - } - if (this.data.installationId && objectIdMatch.installationId && - this.data.installationId !== objectIdMatch.installationId) { - throw new Parse.Error(136, - 'installationId may not be changed in this ' + - 'operation'); - } - if (this.data.deviceToken && objectIdMatch.deviceToken && + // Sanity checks when running a query + if (this.query && this.query.objectId) { + if (!objectIdMatch) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found for update.' + ); + } + if ( + this.data.installationId && + objectIdMatch.installationId && + this.data.installationId !== objectIdMatch.installationId + ) { + throw new Parse.Error( + 136, + 'installationId may not be changed in this ' + 'operation' + ); + } + if ( + this.data.deviceToken && + objectIdMatch.deviceToken && this.data.deviceToken !== objectIdMatch.deviceToken && - !this.data.installationId && !objectIdMatch.installationId) { - throw new Parse.Error(136, - 'deviceToken may not be changed in this ' + - 'operation'); - } - if (this.data.deviceType && this.data.deviceType && - this.data.deviceType !== objectIdMatch.deviceType) { - throw new Parse.Error(136, - 'deviceType may not be changed in this ' + - 'operation'); + !this.data.installationId && + !objectIdMatch.installationId + ) { + throw new Parse.Error( + 136, + 'deviceToken may not be changed in this ' + 'operation' + ); + } + if ( + this.data.deviceType && + this.data.deviceType && + this.data.deviceType !== objectIdMatch.deviceType + ) { + throw new Parse.Error( + 136, + 'deviceType may not be changed in this ' + 'operation' + ); + } } - } - if (this.query && this.query.objectId && objectIdMatch) { - idMatch = objectIdMatch; - } - - if (installationId && installationIdMatch) { - idMatch = installationIdMatch; - } - // need to specify deviceType only if it's new - if (!this.query && !this.data.deviceType && !idMatch) { - throw new Parse.Error(135, - 'deviceType must be specified in this operation'); - } + if (this.query && this.query.objectId && objectIdMatch) { + idMatch = objectIdMatch; + } - }).then(() => { - if (!idMatch) { - if (!deviceTokenMatches.length) { - return; - } else if (deviceTokenMatches.length == 1 && - (!deviceTokenMatches[0]['installationId'] || !installationId) - ) { - // Single match on device token but none on installationId, and either - // the passed object or the match is missing an installationId, so we - // can just return the match. - return deviceTokenMatches[0]['objectId']; - } else if (!this.data.installationId) { - throw new Parse.Error(132, - 'Must specify installationId when deviceToken ' + - 'matches multiple Installation objects'); - } else { - // Multiple device token matches and we specified an installation ID, - // or a single match where both the passed and matching objects have - // an installation ID. Try cleaning out old installations that match - // the deviceToken, and return nil to signal that a new object should - // be created. - var delQuery = { - 'deviceToken': this.data.deviceToken, - 'installationId': { - '$ne': installationId + if (installationId && installationIdMatch) { + idMatch = installationIdMatch; + } + // need to specify deviceType only if it's new + if (!this.query && !this.data.deviceType && !idMatch) { + throw new Parse.Error( + 135, + 'deviceType must be specified in this operation' + ); + } + }) + .then(() => { + if (!idMatch) { + if (!deviceTokenMatches.length) { + return; + } else if ( + deviceTokenMatches.length == 1 && + (!deviceTokenMatches[0]['installationId'] || !installationId) + ) { + // Single match on device token but none on installationId, and either + // the passed object or the match is missing an installationId, so we + // can just return the match. + return deviceTokenMatches[0]['objectId']; + } else if (!this.data.installationId) { + throw new Parse.Error( + 132, + 'Must specify installationId when deviceToken ' + + 'matches multiple Installation objects' + ); + } else { + // Multiple device token matches and we specified an installation ID, + // or a single match where both the passed and matching objects have + // an installation ID. Try cleaning out old installations that match + // the deviceToken, and return nil to signal that a new object should + // be created. + var delQuery = { + deviceToken: this.data.deviceToken, + installationId: { + $ne: installationId, + }, + }; + if (this.data.appIdentifier) { + delQuery['appIdentifier'] = this.data.appIdentifier; } - }; - if (this.data.appIdentifier) { - delQuery['appIdentifier'] = this.data.appIdentifier; - } - this.config.database.destroy('_Installation', delQuery) - .catch(err => { + this.config.database.destroy('_Installation', delQuery).catch(err => { if (err.code == Parse.Error.OBJECT_NOT_FOUND) { // no deletions were made. Can be ignored. return; @@ -881,77 +1075,87 @@ RestWrite.prototype.handleInstallation = function() { // rethrow the error throw err; }); - return; - } - } else { - if (deviceTokenMatches.length == 1 && - !deviceTokenMatches[0]['installationId']) { - // Exactly one device token match and it doesn't have an installation - // ID. This is the one case where we want to merge with the existing - // object. - const delQuery = {objectId: idMatch.objectId}; - return this.config.database.destroy('_Installation', delQuery) - .then(() => { - return deviceTokenMatches[0]['objectId']; - }) - .catch(err => { - if (err.code == Parse.Error.OBJECT_NOT_FOUND) { - // no deletions were made. Can be ignored - return; - } - // rethrow the error - throw err; - }); + return; + } } else { - if (this.data.deviceToken && - idMatch.deviceToken != this.data.deviceToken) { - // We're setting the device token on an existing installation, so - // we should try cleaning out old installations that match this - // device token. - const delQuery = { - 'deviceToken': this.data.deviceToken, - }; - // 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; - } - this.config.database.destroy('_Installation', delQuery) + if ( + deviceTokenMatches.length == 1 && + !deviceTokenMatches[0]['installationId'] + ) { + // Exactly one device token match and it doesn't have an installation + // ID. This is the one case where we want to merge with the existing + // object. + const delQuery = { objectId: idMatch.objectId }; + return this.config.database + .destroy('_Installation', delQuery) + .then(() => { + return deviceTokenMatches[0]['objectId']; + }) .catch(err => { if (err.code == Parse.Error.OBJECT_NOT_FOUND) { - // no deletions were made. Can be ignored. + // no deletions were made. Can be ignored return; } // rethrow the error throw err; }); + } else { + if ( + this.data.deviceToken && + idMatch.deviceToken != this.data.deviceToken + ) { + // We're setting the device token on an existing installation, so + // we should try cleaning out old installations that match this + // device token. + const delQuery = { + deviceToken: this.data.deviceToken, + }; + // 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; + } + this.config.database + .destroy('_Installation', delQuery) + .catch(err => { + if (err.code == Parse.Error.OBJECT_NOT_FOUND) { + // no deletions were made. Can be ignored. + return; + } + // rethrow the error + throw err; + }); + } + // In non-merge scenarios, just return the installation match id + return idMatch.objectId; } - // In non-merge scenarios, just return the installation match id - return idMatch.objectId; } - } - }).then((objId) => { - if (objId) { - this.query = {objectId: objId}; - delete this.data.objectId; - delete this.data.createdAt; - } - // TODO: Validate ops (add/remove on channels, $inc on badge, etc.) - }); + }) + .then(objId => { + if (objId) { + this.query = { objectId: objId }; + delete this.data.objectId; + delete this.data.createdAt; + } + // TODO: Validate ops (add/remove on channels, $inc on badge, etc.) + }); return promise; }; @@ -961,7 +1165,10 @@ RestWrite.prototype.handleInstallation = function() { RestWrite.prototype.expandFilesForExistingObjects = function() { // Check whether we have a short-circuited response - only then run expansion. if (this.response && this.response.response) { - this.config.filesController.expandFilesInObject(this.config, this.response.response); + this.config.filesController.expandFilesInObject( + this.config, + this.response.response + ); } }; @@ -974,10 +1181,15 @@ RestWrite.prototype.runDatabaseOperation = function() { this.config.cacheController.role.clear(); } - if (this.className === '_User' && - this.query && - this.auth.isUnauthenticated()) { - throw new Parse.Error(Parse.Error.SESSION_MISSING, `Cannot modify user ${this.query.objectId}.`); + if ( + this.className === '_User' && + this.query && + this.auth.isUnauthenticated() + ) { + throw new Parse.Error( + Parse.Error.SESSION_MISSING, + `Cannot modify user ${this.query.objectId}.` + ); } if (this.className === '_Product' && this.data.download) { @@ -993,11 +1205,20 @@ 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.auth.isMaster !== true) { + if ( + this.className === '_User' && + this.data.ACL && + this.auth.isMaster !== true + ) { this.data.ACL[this.query.objectId] = { read: true, write: true }; } // update password timestamp if user password is being changed - if (this.className === '_User' && this.data._hashed_password && this.config.passwordPolicy && this.config.passwordPolicy.maxPasswordAge) { + if ( + this.className === '_User' && + this.data._hashed_password && + this.config.passwordPolicy && + this.config.passwordPolicy.maxPasswordAge + ) { this.data._password_changed_at = Parse._encode(new Date()); } // Ignore createdAt when update @@ -1005,28 +1226,46 @@ RestWrite.prototype.runDatabaseOperation = function() { let defer = Promise.resolve(); // if password history is enabled then save the current password to history - if (this.className === '_User' && this.data._hashed_password && this.config.passwordPolicy && this.config.passwordPolicy.maxPasswordHistory) { - defer = this.config.database.find('_User', {objectId: this.objectId()}, {keys: ["_password_history", "_hashed_password"]}).then(results => { - if (results.length != 1) { - throw undefined; - } - const user = results[0]; - let oldPasswords = []; - if (user._password_history) { - oldPasswords = _.take(user._password_history, this.config.passwordPolicy.maxPasswordHistory); - } - //n-1 passwords go into history including last password - while (oldPasswords.length > this.config.passwordPolicy.maxPasswordHistory - 2) { - oldPasswords.shift(); - } - oldPasswords.push(user.password); - this.data._password_history = oldPasswords; - }); + if ( + this.className === '_User' && + this.data._hashed_password && + this.config.passwordPolicy && + this.config.passwordPolicy.maxPasswordHistory + ) { + defer = this.config.database + .find( + '_User', + { objectId: this.objectId() }, + { keys: ['_password_history', '_hashed_password'] } + ) + .then(results => { + if (results.length != 1) { + throw undefined; + } + const user = results[0]; + let oldPasswords = []; + if (user._password_history) { + oldPasswords = _.take( + user._password_history, + this.config.passwordPolicy.maxPasswordHistory + ); + } + //n-1 passwords go into history including last password + while ( + oldPasswords.length > + this.config.passwordPolicy.maxPasswordHistory - 2 + ) { + oldPasswords.shift(); + } + oldPasswords.push(user.password); + this.data._password_history = oldPasswords; + }); } return defer.then(() => { // Run an update - return this.config.database.update(this.className, this.query, this.data, this.runOptions) + return this.config.database + .update(this.className, this.query, this.data, this.runOptions) .then(response => { response.updatedAt = this.updatedAt; this._updateResponseWithData(response, this.data); @@ -1046,51 +1285,85 @@ RestWrite.prototype.runDatabaseOperation = function() { ACL[this.data.objectId] = { read: true, write: true }; this.data.ACL = ACL; // password timestamp to be used when password expiry policy is enforced - if (this.config.passwordPolicy && this.config.passwordPolicy.maxPasswordAge) { + if ( + this.config.passwordPolicy && + this.config.passwordPolicy.maxPasswordAge + ) { this.data._password_changed_at = Parse._encode(new Date()); } } // Run a create - return this.config.database.create(this.className, this.data, this.runOptions) + return this.config.database + .create(this.className, this.data, this.runOptions) .catch(error => { - if (this.className !== '_User' || error.code !== Parse.Error.DUPLICATE_VALUE) { + if ( + this.className !== '_User' || + error.code !== Parse.Error.DUPLICATE_VALUE + ) { throw error; } // Quick check, if we were able to infer the duplicated field name - if (error && error.userInfo && error.userInfo.duplicated_field === 'username') { - throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); + if ( + error && + error.userInfo && + error.userInfo.duplicated_field === 'username' + ) { + throw new Parse.Error( + Parse.Error.USERNAME_TAKEN, + 'Account already exists for this username.' + ); } - if (error && error.userInfo && error.userInfo.duplicated_field === 'email') { - throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); + if ( + error && + error.userInfo && + error.userInfo.duplicated_field === 'email' + ) { + throw new Parse.Error( + Parse.Error.EMAIL_TAKEN, + 'Account already exists for this email address.' + ); } // 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. // Fallback to the original method // 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 } - ) + 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.'); + throw new Parse.Error( + Parse.Error.USERNAME_TAKEN, + 'Account already exists for this username.' + ); } return this.config.database.find( this.className, - { email: this.data.email, objectId: {'$ne': this.objectId()} }, + { 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.'); + throw new Parse.Error( + Parse.Error.EMAIL_TAKEN, + 'Account already exists for this email address.' + ); } - throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + throw new Parse.Error( + Parse.Error.DUPLICATE_VALUE, + 'A duplicate value for a field with unique values was provided' + ); }); }) .then(response => { @@ -1104,7 +1377,7 @@ RestWrite.prototype.runDatabaseOperation = function() { this.response = { status: 201, response, - location: this.location() + location: this.location(), }; }); } @@ -1117,13 +1390,19 @@ RestWrite.prototype.runAfterTrigger = function() { } // Avoid doing any setup for triggers if there is no 'afterSave' trigger for this class. - const hasAfterSaveHook = triggers.triggerExists(this.className, triggers.Types.afterSave, this.config.applicationId); - const hasLiveQuery = this.config.liveQueryController.hasLiveQuery(this.className); + const hasAfterSaveHook = triggers.triggerExists( + this.className, + triggers.Types.afterSave, + this.config.applicationId + ); + const hasLiveQuery = this.config.liveQueryController.hasLiveQuery( + this.className + ); if (!hasAfterSaveHook && !hasLiveQuery) { return Promise.resolve(); } - var extraData = {className: this.className}; + var extraData = { className: this.className }; if (this.query && this.query.objectId) { extraData.objectId = this.query.objectId; } @@ -1137,22 +1416,37 @@ RestWrite.prototype.runAfterTrigger = function() { // Build the inflated object, different from beforeSave, originalData is not empty // since developers can change data in the beforeSave. const updatedObject = this.buildUpdatedObject(extraData); - updatedObject._handleSaveResponse(this.response.response, this.response.status || 200); + updatedObject._handleSaveResponse( + this.response.response, + this.response.status || 200 + ); // Notifiy LiveQueryServer if possible - this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject); + this.config.liveQueryController.onAfterSave( + updatedObject.className, + updatedObject, + originalObject + ); // Run afterSave trigger - return triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config, this.context) + return triggers + .maybeRunTrigger( + triggers.Types.afterSave, + this.auth, + updatedObject, + originalObject, + this.config, + this.context + ) .catch(function(err) { logger.warn('afterSave caught an error', err); - }) + }); }; // A helper to figure out what location this operation happens at. RestWrite.prototype.location = function() { - var middle = (this.className === '_User' ? '/users/' : - '/classes/' + this.className + '/'); + var middle = + this.className === '_User' ? '/users/' : '/classes/' + this.className + '/'; return this.config.mount + middle + this.data.objectId; }; @@ -1166,24 +1460,24 @@ RestWrite.prototype.objectId = function() { RestWrite.prototype.sanitizedData = function() { const data = Object.keys(this.data).reduce((data, key) => { // Regexp comes from Parse.Object.prototype.validate - if (!(/^[A-Za-z][0-9A-Za-z_]*$/).test(key)) { + if (!/^[A-Za-z][0-9A-Za-z_]*$/.test(key)) { delete data[key]; } return data; }, deepcopy(this.data)); return Parse._decode(undefined, data); -} +}; // Returns an updated copy of the object -RestWrite.prototype.buildUpdatedObject = function (extraData) { +RestWrite.prototype.buildUpdatedObject = function(extraData) { const updatedObject = triggers.inflate(extraData, this.originalData); - Object.keys(this.data).reduce(function (data, key) { - if (key.indexOf(".") > 0) { + Object.keys(this.data).reduce(function(data, key) { + if (key.indexOf('.') > 0) { // subdocument key with dot notation ('x.y':v => 'x':{'y':v}) - const splittedKey = key.split("."); + const splittedKey = key.split('.'); const parentProp = splittedKey[0]; let parentVal = updatedObject.get(parentProp); - if(typeof parentVal !== 'object') { + if (typeof parentVal !== 'object') { parentVal = {}; } parentVal[splittedKey[1]] = data[key]; @@ -1201,7 +1495,7 @@ RestWrite.prototype.cleanUserAuthData = function() { if (this.response && this.response.response && this.className === '_User') { const user = this.response.response; if (user.authData) { - Object.keys(user.authData).forEach((provider) => { + Object.keys(user.authData).forEach(provider => { if (user.authData[provider] === null) { delete user.authData[provider]; } @@ -1221,7 +1515,7 @@ RestWrite.prototype._updateResponseWithData = function(response, data) { this.storage.fieldsChangedByTrigger.forEach(fieldName => { const dataValue = data[fieldName]; - if(!response.hasOwnProperty(fieldName)) { + if (!response.hasOwnProperty(fieldName)) { response[fieldName] = dataValue; } @@ -1234,7 +1528,7 @@ RestWrite.prototype._updateResponseWithData = function(response, data) { } }); return response; -} +}; export default RestWrite; module.exports = RestWrite; diff --git a/src/Routers/AggregateRouter.js b/src/Routers/AggregateRouter.js index b72640048a..90ee52f16a 100644 --- a/src/Routers/AggregateRouter.js +++ b/src/Routers/AggregateRouter.js @@ -1,8 +1,8 @@ import ClassesRouter from './ClassesRouter'; import rest from '../rest'; import * as middleware from '../middlewares'; -import Parse from 'parse/node'; -import UsersRouter from './UsersRouter'; +import Parse from 'parse/node'; +import UsersRouter from './UsersRouter'; const BASE_KEYS = ['where', 'distinct', 'pipeline']; @@ -37,9 +37,11 @@ const PIPELINE_KEYS = [ const ALLOWED_KEYS = [...BASE_KEYS, ...PIPELINE_KEYS]; export class AggregateRouter extends ClassesRouter { - handleFind(req) { - const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); + const body = Object.assign( + req.body, + ClassesRouter.JSONFromQuery(req.query) + ); const options = {}; if (body.distinct) { options.distinct = String(body.distinct); @@ -48,14 +50,23 @@ export class AggregateRouter extends ClassesRouter { if (typeof body.where === 'string') { body.where = JSON.parse(body.where); } - return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK).then((response) => { - for(const result of response.results) { - if(typeof result === 'object') { - UsersRouter.removeHiddenProperties(result); + return rest + .find( + req.config, + req.auth, + this.className(req), + body.where, + options, + req.info.clientSDK + ) + .then(response => { + for (const result of response.results) { + if (typeof result === 'object') { + UsersRouter.removeHiddenProperties(result); + } } - } - return { response }; - }); + return { response }; + }); } /* Builds a pipeline from the body. Originally the body could be passed as a single object, @@ -87,13 +98,17 @@ export class AggregateRouter extends ClassesRouter { let pipeline = body.pipeline || body; if (!Array.isArray(pipeline)) { - pipeline = Object.keys(pipeline).map((key) => { return { [key]: pipeline[key] } }); + pipeline = Object.keys(pipeline).map(key => { + return { [key]: pipeline[key] }; + }); } - return pipeline.map((stage) => { + return pipeline.map(stage => { const keys = Object.keys(stage); if (keys.length != 1) { - throw new Error(`Pipeline stages should only have one key found ${keys.join(', ')}`); + throw new Error( + `Pipeline stages should only have one key found ${keys.join(', ')}` + ); } return AggregateRouter.transformStage(keys[0], stage); }); @@ -126,7 +141,14 @@ export class AggregateRouter extends ClassesRouter { } mountRoutes() { - this.route('GET','/aggregate/:className', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleFind(req); }); + this.route( + 'GET', + '/aggregate/:className', + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.handleFind(req); + } + ); } } diff --git a/src/Routers/AnalyticsRouter.js b/src/Routers/AnalyticsRouter.js index 511781d807..90ffcdcc4a 100644 --- a/src/Routers/AnalyticsRouter.js +++ b/src/Routers/AnalyticsRouter.js @@ -11,10 +11,9 @@ function trackEvent(req) { return analyticsController.trackEvent(req); } - export class AnalyticsRouter extends PromiseRouter { mountRoutes() { - this.route('POST','/events/AppOpened', appOpened); - this.route('POST','/events/:eventName', trackEvent); + this.route('POST', '/events/AppOpened', appOpened); + this.route('POST', '/events/:eventName', trackEvent); } } diff --git a/src/Routers/AudiencesRouter.js b/src/Routers/AudiencesRouter.js index 3dbb94cf85..312ea9b5ac 100644 --- a/src/Routers/AudiencesRouter.js +++ b/src/Routers/AudiencesRouter.js @@ -3,41 +3,84 @@ import rest from '../rest'; import * as middleware from '../middlewares'; export class AudiencesRouter extends ClassesRouter { - className() { return '_Audience'; } handleFind(req) { - const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); + const body = Object.assign( + req.body, + ClassesRouter.JSONFromQuery(req.query) + ); const options = ClassesRouter.optionsFromBody(body); - return rest.find(req.config, req.auth, '_Audience', body.where, options, req.info.clientSDK) - .then((response) => { - - response.results.forEach((item) => { + return rest + .find( + req.config, + req.auth, + '_Audience', + body.where, + options, + req.info.clientSDK + ) + .then(response => { + response.results.forEach(item => { item.query = JSON.parse(item.query); }); - return {response: response}; + return { response: response }; }); } handleGet(req) { - return super.handleGet(req) - .then((data) => { - data.response.query = JSON.parse(data.response.query); + return super.handleGet(req).then(data => { + data.response.query = JSON.parse(data.response.query); - return data; - }); + return data; + }); } mountRoutes() { - this.route('GET','/push_audiences', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleFind(req); }); - this.route('GET','/push_audiences/:objectId', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleGet(req); }); - this.route('POST','/push_audiences', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleCreate(req); }); - this.route('PUT','/push_audiences/:objectId', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleUpdate(req); }); - this.route('DELETE','/push_audiences/:objectId', middleware.promiseEnforceMasterKeyAccess, req => { return this.handleDelete(req); }); + this.route( + 'GET', + '/push_audiences', + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.handleFind(req); + } + ); + this.route( + 'GET', + '/push_audiences/:objectId', + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.handleGet(req); + } + ); + this.route( + 'POST', + '/push_audiences', + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.handleCreate(req); + } + ); + this.route( + 'PUT', + '/push_audiences/:objectId', + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.handleUpdate(req); + } + ); + this.route( + 'DELETE', + '/push_audiences/:objectId', + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.handleDelete(req); + } + ); } } diff --git a/src/Routers/ClassesRouter.js b/src/Routers/ClassesRouter.js index e7205015b8..b4269c055e 100644 --- a/src/Routers/ClassesRouter.js +++ b/src/Routers/ClassesRouter.js @@ -1,21 +1,22 @@ - import PromiseRouter from '../PromiseRouter'; -import rest from '../rest'; -import _ from 'lodash'; -import Parse from 'parse/node'; +import rest from '../rest'; +import _ from 'lodash'; +import Parse from 'parse/node'; const ALLOWED_GET_QUERY_KEYS = ['keys', 'include']; export class ClassesRouter extends PromiseRouter { - className(req) { return req.params.className; } handleFind(req) { - const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); + const body = Object.assign( + req.body, + ClassesRouter.JSONFromQuery(req.query) + ); const options = ClassesRouter.optionsFromBody(body); - if (req.config.maxLimit && (body.limit > req.config.maxLimit)) { + if (req.config.maxLimit && body.limit > req.config.maxLimit) { // Silently replace the limit on the query with the max configured options.limit = Number(req.config.maxLimit); } @@ -25,20 +26,34 @@ export class ClassesRouter extends PromiseRouter { if (typeof body.where === 'string') { body.where = JSON.parse(body.where); } - return rest.find(req.config, req.auth, this.className(req), body.where, options, req.info.clientSDK) - .then((response) => { + return rest + .find( + req.config, + req.auth, + this.className(req), + body.where, + options, + req.info.clientSDK + ) + .then(response => { return { response: response }; }); } // Returns a promise for a {response} object. handleGet(req) { - const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); + const body = Object.assign( + req.body, + ClassesRouter.JSONFromQuery(req.query) + ); const options = {}; for (const 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'); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + 'Improper encode of parameter' + ); } } @@ -49,17 +64,27 @@ export class ClassesRouter extends PromiseRouter { options.include = String(body.include); } - return rest.get(req.config, req.auth, this.className(req), req.params.objectId, options, req.info.clientSDK) - .then((response) => { + return rest + .get( + req.config, + req.auth, + this.className(req), + req.params.objectId, + options, + req.info.clientSDK + ) + .then(response => { if (!response.results || response.results.length == 0) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); } - if (this.className(req) === "_User") { - + if (this.className(req) === '_User') { delete response.results[0].sessionToken; - const user = response.results[0]; + const user = response.results[0]; if (req.auth.user && user.objectId == req.auth.user.id) { // Force the session token @@ -71,18 +96,38 @@ export class ClassesRouter extends PromiseRouter { } handleCreate(req) { - return rest.create(req.config, req.auth, this.className(req), req.body, req.info.clientSDK); + return rest.create( + req.config, + req.auth, + this.className(req), + req.body, + req.info.clientSDK + ); } handleUpdate(req) { - const where = { objectId: req.params.objectId } - return rest.update(req.config, req.auth, this.className(req), where, req.body, req.info.clientSDK); + const where = { objectId: req.params.objectId }; + return rest.update( + req.config, + req.auth, + this.className(req), + where, + req.body, + req.info.clientSDK + ); } handleDelete(req) { - return rest.del(req.config, req.auth, this.className(req), req.params.objectId, req.info.clientSDK) + return rest + .del( + req.config, + req.auth, + this.className(req), + req.params.objectId, + req.info.clientSDK + ) .then(() => { - return {response: {}}; + return { response: {} }; }); } @@ -95,16 +140,28 @@ export class ClassesRouter extends PromiseRouter { json[key] = value; } } - return json + return json; } static optionsFromBody(body) { - const allowConstraints = ['skip', 'limit', 'order', 'count', 'keys', - 'include', 'includeAll', 'redirectClassNameForKey', 'where']; + const allowConstraints = [ + 'skip', + 'limit', + 'order', + 'count', + 'keys', + 'include', + 'includeAll', + 'redirectClassNameForKey', + 'where', + ]; for (const key of Object.keys(body)) { if (allowConstraints.indexOf(key) === -1) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid parameter for query: ${key}`); + throw new Parse.Error( + Parse.Error.INVALID_QUERY, + `Invalid parameter for query: ${key}` + ); } } const options = {}; @@ -135,11 +192,21 @@ export class ClassesRouter extends PromiseRouter { } mountRoutes() { - this.route('GET', '/classes/:className', (req) => { return this.handleFind(req); }); - this.route('GET', '/classes/:className/:objectId', (req) => { return this.handleGet(req); }); - this.route('POST', '/classes/:className', (req) => { return this.handleCreate(req); }); - this.route('PUT', '/classes/:className/:objectId', (req) => { return this.handleUpdate(req); }); - this.route('DELETE', '/classes/:className/:objectId', (req) => { return this.handleDelete(req); }); + this.route('GET', '/classes/:className', req => { + return this.handleFind(req); + }); + this.route('GET', '/classes/:className/:objectId', req => { + return this.handleGet(req); + }); + this.route('POST', '/classes/:className', req => { + return this.handleCreate(req); + }); + this.route('PUT', '/classes/:className/:objectId', req => { + return this.handleUpdate(req); + }); + this.route('DELETE', '/classes/:className/:objectId', req => { + return this.handleDelete(req); + }); } } diff --git a/src/Routers/CloudCodeRouter.js b/src/Routers/CloudCodeRouter.js index d1ca9009e6..dea27faec7 100644 --- a/src/Routers/CloudCodeRouter.js +++ b/src/Routers/CloudCodeRouter.js @@ -1,8 +1,8 @@ -import PromiseRouter from '../PromiseRouter'; -import Parse from 'parse/node'; -import rest from '../rest'; -const triggers = require('../triggers'); -const middleware = require('../middlewares'); +import PromiseRouter from '../PromiseRouter'; +import Parse from 'parse/node'; +import rest from '../rest'; +const triggers = require('../triggers'); +const middleware = require('../middlewares'); function formatJobSchedule(job_schedule) { if (typeof job_schedule.startAfter === 'undefined') { @@ -14,63 +14,111 @@ function formatJobSchedule(job_schedule) { function validateJobSchedule(config, job_schedule) { const jobs = triggers.getJobs(config.applicationId) || {}; if (job_schedule.jobName && !jobs[job_schedule.jobName]) { - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Cannot Schedule a job that is not deployed'); + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + 'Cannot Schedule a job that is not deployed' + ); } } export class CloudCodeRouter extends PromiseRouter { mountRoutes() { - this.route('GET', '/cloud_code/jobs', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.getJobs); - this.route('GET', '/cloud_code/jobs/data', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.getJobsData); - this.route('POST', '/cloud_code/jobs', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.createJob); - this.route('PUT', '/cloud_code/jobs/:objectId', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.editJob); - this.route('DELETE', '/cloud_code/jobs/:objectId', middleware.promiseEnforceMasterKeyAccess, CloudCodeRouter.deleteJob); + this.route( + 'GET', + '/cloud_code/jobs', + middleware.promiseEnforceMasterKeyAccess, + CloudCodeRouter.getJobs + ); + this.route( + 'GET', + '/cloud_code/jobs/data', + middleware.promiseEnforceMasterKeyAccess, + CloudCodeRouter.getJobsData + ); + this.route( + 'POST', + '/cloud_code/jobs', + middleware.promiseEnforceMasterKeyAccess, + CloudCodeRouter.createJob + ); + this.route( + 'PUT', + '/cloud_code/jobs/:objectId', + middleware.promiseEnforceMasterKeyAccess, + CloudCodeRouter.editJob + ); + this.route( + 'DELETE', + '/cloud_code/jobs/:objectId', + middleware.promiseEnforceMasterKeyAccess, + CloudCodeRouter.deleteJob + ); } static getJobs(req) { - return rest.find(req.config, req.auth, '_JobSchedule', {}, {}).then((scheduledJobs) => { - return { - response: scheduledJobs.results - } - }); + return rest + .find(req.config, req.auth, '_JobSchedule', {}, {}) + .then(scheduledJobs => { + return { + response: scheduledJobs.results, + }; + }); } static getJobsData(req) { const config = req.config; const jobs = triggers.getJobs(config.applicationId) || {}; - return rest.find(req.config, req.auth, '_JobSchedule', {}, {}).then((scheduledJobs) => { - return { - response: { - in_use: scheduledJobs.results.map((job) => job.jobName), - jobs: Object.keys(jobs), - } - }; - }); + return rest + .find(req.config, req.auth, '_JobSchedule', {}, {}) + .then(scheduledJobs => { + return { + response: { + in_use: scheduledJobs.results.map(job => job.jobName), + jobs: Object.keys(jobs), + }, + }; + }); } static createJob(req) { const { job_schedule } = req.body; validateJobSchedule(req.config, job_schedule); - return rest.create(req.config, req.auth, '_JobSchedule', formatJobSchedule(job_schedule), req.client); + return rest.create( + req.config, + req.auth, + '_JobSchedule', + formatJobSchedule(job_schedule), + req.client + ); } static editJob(req) { const { objectId } = req.params; const { job_schedule } = req.body; validateJobSchedule(req.config, job_schedule); - return rest.update(req.config, req.auth, '_JobSchedule', { objectId }, formatJobSchedule(job_schedule)).then((response) => { - return { - response - } - }); + return rest + .update( + req.config, + req.auth, + '_JobSchedule', + { objectId }, + formatJobSchedule(job_schedule) + ) + .then(response => { + return { + response, + }; + }); } static deleteJob(req) { const { objectId } = req.params; - return rest.del(req.config, req.auth, '_JobSchedule', objectId).then((response) => { - return { - response - } - }); + return rest + .del(req.config, req.auth, '_JobSchedule', objectId) + .then(response => { + return { + response, + }; + }); } } diff --git a/src/Routers/FeaturesRouter.js b/src/Routers/FeaturesRouter.js index 47d1a0980c..48923087cf 100644 --- a/src/Routers/FeaturesRouter.js +++ b/src/Routers/FeaturesRouter.js @@ -1,56 +1,63 @@ -import { version } from '../../package.json'; -import PromiseRouter from '../PromiseRouter'; -import * as middleware from "../middlewares"; +import { version } from '../../package.json'; +import PromiseRouter from '../PromiseRouter'; +import * as middleware from '../middlewares'; export class FeaturesRouter extends PromiseRouter { mountRoutes() { - this.route('GET','/serverInfo', middleware.promiseEnforceMasterKeyAccess, req => { - const features = { - globalConfig: { - create: true, - read: true, - update: true, - delete: true, - }, - hooks: { - create: true, - read: true, - update: true, - delete: true, - }, - cloudCode: { - jobs: true, - }, - logs: { - level: true, - size: true, - order: true, - until: true, - from: true, - }, - push: { - immediatePush: req.config.hasPushSupport, - scheduledPush: req.config.hasPushScheduledSupport, - storedPushData: req.config.hasPushSupport, - pushAudiences: true, - localization: true, - }, - schemas: { - addField: true, - removeField: true, - addClass: true, - removeClass: true, - clearAllDataFromClass: true, - exportClass: false, - editClassLevelPermissions: true, - editPointerPermissions: true, - }, - }; + this.route( + 'GET', + '/serverInfo', + middleware.promiseEnforceMasterKeyAccess, + req => { + const features = { + globalConfig: { + create: true, + read: true, + update: true, + delete: true, + }, + hooks: { + create: true, + read: true, + update: true, + delete: true, + }, + cloudCode: { + jobs: true, + }, + logs: { + level: true, + size: true, + order: true, + until: true, + from: true, + }, + push: { + immediatePush: req.config.hasPushSupport, + scheduledPush: req.config.hasPushScheduledSupport, + storedPushData: req.config.hasPushSupport, + pushAudiences: true, + localization: true, + }, + schemas: { + addField: true, + removeField: true, + addClass: true, + removeClass: true, + clearAllDataFromClass: true, + exportClass: false, + editClassLevelPermissions: true, + editPointerPermissions: true, + }, + }; - return { response: { - features: features, - parseServerVersion: version, - } }; - }); + return { + response: { + features: features, + parseServerVersion: version, + }, + }; + } + ); } } diff --git a/src/Routers/FilesRouter.js b/src/Routers/FilesRouter.js index 8312526f30..623d3671e1 100644 --- a/src/Routers/FilesRouter.js +++ b/src/Routers/FilesRouter.js @@ -1,30 +1,37 @@ -import express from 'express'; -import BodyParser from 'body-parser'; -import * as Middlewares from '../middlewares'; -import Parse from 'parse/node'; -import Config from '../Config'; -import mime from 'mime'; -import logger from '../logger'; +import express from 'express'; +import BodyParser from 'body-parser'; +import * as Middlewares from '../middlewares'; +import Parse from 'parse/node'; +import Config from '../Config'; +import mime from 'mime'; +import logger from '../logger'; export class FilesRouter { - expressRouter({ maxUploadSize = '20Mb' } = {}) { var router = express.Router(); router.get('/files/:appId/:filename', this.getHandler); router.post('/files', function(req, res, next) { - next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, - 'Filename not provided.')); + next( + new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename not provided.') + ); }); - router.post('/files/:filename', + router.post( + '/files/:filename', Middlewares.allowCrossDomain, - BodyParser.raw({type: () => { return true; }, limit: maxUploadSize }), // Allow uploads without Content-Type, or with any Content-Type. + BodyParser.raw({ + type: () => { + return true; + }, + limit: maxUploadSize, + }), // Allow uploads without Content-Type, or with any Content-Type. Middlewares.handleParseHeaders, this.createHandler ); - router.delete('/files/:filename', + router.delete( + '/files/:filename', Middlewares.allowCrossDomain, Middlewares.handleParseHeaders, Middlewares.enforceMasterKeyAccess, @@ -39,43 +46,55 @@ export class FilesRouter { const filename = req.params.filename; const contentType = mime.getType(filename); if (isFileStreamable(req, filesController)) { - filesController.getFileStream(config, filename).then((stream) => { - handleFileStream(stream, req, res, contentType); - }).catch(() => { - res.status(404); - res.set('Content-Type', 'text/plain'); - res.end('File not found.'); - }); + filesController + .getFileStream(config, filename) + .then(stream => { + handleFileStream(stream, req, res, contentType); + }) + .catch(() => { + res.status(404); + res.set('Content-Type', 'text/plain'); + res.end('File not found.'); + }); } else { - filesController.getFileData(config, filename).then((data) => { - res.status(200); - res.set('Content-Type', contentType); - res.set('Content-Length', data.length); - res.end(data); - }).catch(() => { - res.status(404); - res.set('Content-Type', 'text/plain'); - res.end('File not found.'); - }); + filesController + .getFileData(config, filename) + .then(data => { + res.status(200); + res.set('Content-Type', contentType); + res.set('Content-Length', data.length); + res.end(data); + }) + .catch(() => { + res.status(404); + res.set('Content-Type', 'text/plain'); + res.end('File not found.'); + }); } } createHandler(req, res, next) { if (!req.body || !req.body.length) { - next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, - 'Invalid file upload.')); + next( + new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Invalid file upload.') + ); return; } if (req.params.filename.length > 128) { - next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, - 'Filename too long.')); + next( + new Parse.Error(Parse.Error.INVALID_FILE_NAME, 'Filename too long.') + ); return; } if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { - next(new Parse.Error(Parse.Error.INVALID_FILE_NAME, - 'Filename contains invalid characters.')); + next( + new Parse.Error( + Parse.Error.INVALID_FILE_NAME, + 'Filename contains invalid characters.' + ) + ); return; } @@ -84,35 +103,53 @@ export class FilesRouter { const config = req.config; const filesController = config.filesController; - filesController.createFile(config, filename, req.body, contentType).then((result) => { - res.status(201); - res.set('Location', result.url); - res.json(result); - }).catch((e) => { - logger.error(e.message, e); - next(new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.')); - }); + filesController + .createFile(config, filename, req.body, contentType) + .then(result => { + res.status(201); + res.set('Location', result.url); + res.json(result); + }) + .catch(e => { + logger.error(e.message, e); + next( + new Parse.Error(Parse.Error.FILE_SAVE_ERROR, 'Could not store file.') + ); + }); } deleteHandler(req, res, next) { const filesController = req.config.filesController; - filesController.deleteFile(req.config, req.params.filename).then(() => { - res.status(200); - // TODO: return useful JSON here? - res.end(); - }).catch(() => { - next(new Parse.Error(Parse.Error.FILE_DELETE_ERROR, - 'Could not delete file.')); - }); + filesController + .deleteFile(req.config, req.params.filename) + .then(() => { + res.status(200); + // TODO: return useful JSON here? + res.end(); + }) + .catch(() => { + next( + new Parse.Error( + Parse.Error.FILE_DELETE_ERROR, + 'Could not delete file.' + ) + ); + }); } } -function isFileStreamable(req, filesController){ - return req.get('Range') && typeof filesController.adapter.getFileStream === 'function'; +function isFileStreamable(req, filesController) { + return ( + req.get('Range') && + typeof filesController.adapter.getFileStream === 'function' + ); } function getRange(req) { - const parts = req.get('Range').replace(/bytes=/, "").split("-"); + const parts = req + .get('Range') + .replace(/bytes=/, '') + .split('-'); return { start: parseInt(parts[0], 10), end: parseInt(parts[1], 10) }; } @@ -121,12 +158,10 @@ function getRange(req) { function handleFileStream(stream, req, res, contentType) { const buffer_size = 1024 * 1024; //1024Kb // Range request, partiall stream the file - let { - start, end - } = getRange(req); + let { start, end } = getRange(req); - const notEnded = (!end && end !== 0); - const notStarted = (!start && start !== 0); + const notEnded = !end && end !== 0; + const notStarted = !start && start !== 0; // No end provided, we want all bytes if (notEnded) { end = stream.length - 1; @@ -142,7 +177,7 @@ function handleFileStream(stream, req, res, contentType) { end = start + buffer_size - 1; } - const contentLength = (end - start) + 1; + const contentLength = end - start + 1; res.writeHead(206, { 'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length, @@ -151,14 +186,14 @@ function handleFileStream(stream, req, res, contentType) { 'Content-Type': contentType, }); - stream.seek(start, function () { + stream.seek(start, function() { // get gridFile stream const gridFileStream = stream.stream(true); let bufferAvail = 0; let remainingBytesToWrite = contentLength; let totalBytesWritten = 0; // write to response - gridFileStream.on('data', function (data) { + gridFileStream.on('data', function(data) { bufferAvail += data.length; if (bufferAvail > 0) { // slice returns the same buffer if overflowing diff --git a/src/Routers/FunctionsRouter.js b/src/Routers/FunctionsRouter.js index e4add79c61..5e6f422718 100644 --- a/src/Routers/FunctionsRouter.js +++ b/src/Routers/FunctionsRouter.js @@ -11,7 +11,7 @@ import { logger } from '../logger'; function parseObject(obj) { if (Array.isArray(obj)) { - return obj.map((item) => { + return obj.map(item => { return parseObject(item); }); } else if (obj && obj.__type == 'Date') { @@ -30,12 +30,20 @@ function parseParams(params) { } export class FunctionsRouter extends PromiseRouter { - mountRoutes() { - this.route('POST', '/functions/:functionName', FunctionsRouter.handleCloudFunction); - this.route('POST', '/jobs/:jobName', promiseEnforceMasterKeyAccess, function(req) { - return FunctionsRouter.handleCloudJob(req); - }); + this.route( + 'POST', + '/functions/:functionName', + FunctionsRouter.handleCloudFunction + ); + this.route( + 'POST', + '/jobs/:jobName', + promiseEnforceMasterKeyAccess, + function(req) { + return FunctionsRouter.handleCloudJob(req); + } + ); this.route('POST', '/jobs', promiseEnforceMasterKeyAccess, function(req) { return FunctionsRouter.handleCloudJob(req); }); @@ -57,27 +65,32 @@ export class FunctionsRouter extends PromiseRouter { headers: req.config.headers, ip: req.config.ip, jobName, - message: jobHandler.setMessage.bind(jobHandler) + message: jobHandler.setMessage.bind(jobHandler), }; - return jobHandler.setRunning(jobName, params).then((jobStatus) => { - request.jobId = jobStatus.objectId + return jobHandler.setRunning(jobName, params).then(jobStatus => { + request.jobId = jobStatus.objectId; // run the function async process.nextTick(() => { - Promise.resolve().then(() => { - return jobFunction(request); - }).then((result) => { - jobHandler.setSucceeded(result); - }, (error) => { - jobHandler.setFailed(error); - }); + Promise.resolve() + .then(() => { + return jobFunction(request); + }) + .then( + result => { + jobHandler.setSucceeded(result); + }, + error => { + jobHandler.setFailed(error); + } + ); }); return { headers: { - 'X-Parse-Job-Status-Id': jobStatus.objectId + 'X-Parse-Job-Status-Id': jobStatus.objectId, }, - response: {} - } + response: {}, + }; }); } @@ -86,8 +99,8 @@ export class FunctionsRouter extends PromiseRouter { success: function(result) { resolve({ response: { - result: Parse._encode(result) - } + result: Parse._encode(result), + }, }); }, error: function(message) { @@ -106,17 +119,23 @@ export class FunctionsRouter extends PromiseRouter { } reject(new Parse.Error(code, message)); }, - message: message - } + message: message, + }; } static handleCloudFunction(req) { const functionName = req.params.functionName; const applicationId = req.config.applicationId; const theFunction = triggers.getFunction(functionName, applicationId); - const theValidator = triggers.getValidator(req.params.functionName, applicationId); + const theValidator = triggers.getValidator( + req.params.functionName, + applicationId + ); if (!theFunction) { - throw new Parse.Error(Parse.Error.SCRIPT_FAILED, `Invalid function: "${functionName}"`); + throw new Parse.Error( + Parse.Error.SCRIPT_FAILED, + `Invalid function: "${functionName}"` + ); } let params = Object.assign({}, req.body, req.query); params = parseParams(params); @@ -128,53 +147,65 @@ export class FunctionsRouter extends PromiseRouter { log: req.config.loggerController, headers: req.config.headers, ip: req.config.ip, - functionName + functionName, }; - if (theValidator && typeof theValidator === "function") { + if (theValidator && typeof theValidator === 'function') { var result = theValidator(request); if (!result) { - throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Validation failed.'); + throw new Parse.Error( + Parse.Error.VALIDATION_ERROR, + 'Validation failed.' + ); } } - return new Promise(function (resolve, reject) { - const userString = (req.auth && req.auth.user) ? req.auth.user.id : undefined; + return new Promise(function(resolve, reject) { + const userString = + req.auth && req.auth.user ? req.auth.user.id : undefined; const cleanInput = logger.truncateLogMessage(JSON.stringify(params)); - const { success, error, message } = FunctionsRouter.createResponseObject((result) => { - try { - const cleanResult = logger.truncateLogMessage(JSON.stringify(result.response.result)); - logger.info( - `Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput }\n Result: ${cleanResult }`, - { - functionName, - params, - user: userString, - } - ); - resolve(result); - } catch (e) { - reject(e); - } - }, (error) => { - try { - logger.error( - `Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` + JSON.stringify(error), - { - functionName, - error, - params, - user: userString - } - ); - reject(error); - } catch (e) { - reject(e); + const { success, error, message } = FunctionsRouter.createResponseObject( + result => { + try { + const cleanResult = logger.truncateLogMessage( + JSON.stringify(result.response.result) + ); + logger.info( + `Ran cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Result: ${cleanResult}`, + { + functionName, + params, + user: userString, + } + ); + resolve(result); + } catch (e) { + reject(e); + } + }, + error => { + try { + logger.error( + `Failed running cloud function ${functionName} for user ${userString} with:\n Input: ${cleanInput}\n Error: ` + + JSON.stringify(error), + { + functionName, + error, + params, + user: userString, + } + ); + reject(error); + } catch (e) { + reject(e); + } } - }); - return Promise.resolve().then(() => { - return theFunction(request, { message }); - }).then(success, error); + ); + return Promise.resolve() + .then(() => { + return theFunction(request, { message }); + }) + .then(success, error); }); } } diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 42ba581938..2b1d41bd9a 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -1,23 +1,28 @@ // global_config.js -import Parse from 'parse/node'; -import PromiseRouter from '../PromiseRouter'; -import * as middleware from "../middlewares"; +import Parse from 'parse/node'; +import PromiseRouter from '../PromiseRouter'; +import * as middleware from '../middlewares'; export class GlobalConfigRouter extends PromiseRouter { getGlobalConfig(req) { - return req.config.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: {} } }; - } - const globalConfig = results[0]; - return { response: { params: globalConfig.params } }; - }); + return req.config.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: {} } }; + } + const globalConfig = results[0]; + return { response: { params: globalConfig.params } }; + }); } updateGlobalConfig(req) { if (req.auth.isReadOnly) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'read-only masterKey isn\'t allowed to update the config.'); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to update the config." + ); } const params = req.body.params; // Transform in dot notation to make sure it works @@ -25,12 +30,23 @@ export class GlobalConfigRouter extends PromiseRouter { acc[`params.${key}`] = params[key]; return acc; }, {}); - return req.config.database.update('_GlobalConfig', {objectId: "1"}, update, {upsert: true}).then(() => ({ response: { result: true } })); + return req.config.database + .update('_GlobalConfig', { objectId: '1' }, update, { upsert: true }) + .then(() => ({ response: { result: true } })); } mountRoutes() { - this.route('GET', '/config', req => { return this.getGlobalConfig(req) }); - this.route('PUT', '/config', middleware.promiseEnforceMasterKeyAccess, req => { return this.updateGlobalConfig(req) }); + this.route('GET', '/config', req => { + return this.getGlobalConfig(req); + }); + this.route( + 'PUT', + '/config', + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.updateGlobalConfig(req); + } + ); } } diff --git a/src/Routers/HooksRouter.js b/src/Routers/HooksRouter.js index 6c299c82ca..404094cbb9 100644 --- a/src/Routers/HooksRouter.js +++ b/src/Routers/HooksRouter.js @@ -1,14 +1,18 @@ -import { Parse } from 'parse/node'; -import PromiseRouter from '../PromiseRouter'; -import * as middleware from "../middlewares"; +import { Parse } from 'parse/node'; +import PromiseRouter from '../PromiseRouter'; +import * as middleware from '../middlewares'; export class HooksRouter extends PromiseRouter { createHook(aHook, config) { - return config.hooksController.createHook(aHook).then((hook) => ({response: hook})); + return config.hooksController + .createHook(aHook) + .then(hook => ({ response: hook })); } updateHook(aHook, config) { - return config.hooksController.updateHook(aHook).then((hook) => ({response: hook})); + return config.hooksController + .updateHook(aHook) + .then(hook => ({ response: hook })); } handlePost(req) { @@ -18,67 +22,84 @@ export class HooksRouter extends PromiseRouter { handleGetFunctions(req) { var hooksController = req.config.hooksController; if (req.params.functionName) { - return hooksController.getFunction(req.params.functionName).then((foundFunction) => { - if (!foundFunction) { - throw new Parse.Error(143, `no function named: ${req.params.functionName} is defined`); - } - return Promise.resolve({response: foundFunction}); - }); + return hooksController + .getFunction(req.params.functionName) + .then(foundFunction => { + if (!foundFunction) { + throw new Parse.Error( + 143, + `no function named: ${req.params.functionName} is defined` + ); + } + return Promise.resolve({ response: foundFunction }); + }); } - return hooksController.getFunctions().then((functions) => { - return { response: functions || [] }; - }, (err) => { - throw err; - }); + return hooksController.getFunctions().then( + functions => { + return { response: functions || [] }; + }, + err => { + throw err; + } + ); } 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`); - } - return Promise.resolve({response: foundTrigger}); - }); + 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` + ); + } + return Promise.resolve({ response: foundTrigger }); + }); } - return hooksController.getTriggers().then((triggers) => ({ response: triggers || [] })); + return hooksController + .getTriggers() + .then(triggers => ({ response: triggers || [] })); } handleDelete(req) { var hooksController = req.config.hooksController; if (req.params.functionName) { - return hooksController.deleteFunction(req.params.functionName).then(() => ({response: {}})) - + return hooksController + .deleteFunction(req.params.functionName) + .then(() => ({ response: {} })); } else if (req.params.className && req.params.triggerName) { - return hooksController.deleteTrigger(req.params.className, req.params.triggerName).then(() => ({response: {}})) + return hooksController + .deleteTrigger(req.params.className, req.params.triggerName) + .then(() => ({ response: {} })); } - return Promise.resolve({response: {}}); + return Promise.resolve({ response: {} }); } handleUpdate(req) { var hook; if (req.params.functionName && req.body.url) { - hook = {} + hook = {}; hook.functionName = req.params.functionName; hook.url = req.body.url; } else if (req.params.className && req.params.triggerName && req.body.url) { - hook = {} + hook = {}; hook.className = req.params.className; hook.triggerName = req.params.triggerName; - hook.url = req.body.url + hook.url = req.body.url; } else { - throw new Parse.Error(143, "invalid hook declaration"); + throw new Parse.Error(143, 'invalid hook declaration'); } return this.updateHook(hook, req.config); } handlePut(req) { var body = req.body; - if (body.__op == "Delete") { + if (body.__op == 'Delete') { return this.handleDelete(req); } else { return this.handleUpdate(req); @@ -86,14 +107,54 @@ export class HooksRouter extends PromiseRouter { } mountRoutes() { - this.route('GET', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this)); - this.route('GET', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this)); - this.route('GET', '/hooks/functions/:functionName', middleware.promiseEnforceMasterKeyAccess, this.handleGetFunctions.bind(this)); - this.route('GET', '/hooks/triggers/:className/:triggerName', middleware.promiseEnforceMasterKeyAccess, this.handleGetTriggers.bind(this)); - this.route('POST', '/hooks/functions', middleware.promiseEnforceMasterKeyAccess, this.handlePost.bind(this)); - this.route('POST', '/hooks/triggers', middleware.promiseEnforceMasterKeyAccess, this.handlePost.bind(this)); - this.route('PUT', '/hooks/functions/:functionName', middleware.promiseEnforceMasterKeyAccess, this.handlePut.bind(this)); - this.route('PUT', '/hooks/triggers/:className/:triggerName', middleware.promiseEnforceMasterKeyAccess, this.handlePut.bind(this)); + this.route( + 'GET', + '/hooks/functions', + middleware.promiseEnforceMasterKeyAccess, + this.handleGetFunctions.bind(this) + ); + this.route( + 'GET', + '/hooks/triggers', + middleware.promiseEnforceMasterKeyAccess, + this.handleGetTriggers.bind(this) + ); + this.route( + 'GET', + '/hooks/functions/:functionName', + middleware.promiseEnforceMasterKeyAccess, + this.handleGetFunctions.bind(this) + ); + this.route( + 'GET', + '/hooks/triggers/:className/:triggerName', + middleware.promiseEnforceMasterKeyAccess, + this.handleGetTriggers.bind(this) + ); + this.route( + 'POST', + '/hooks/functions', + middleware.promiseEnforceMasterKeyAccess, + this.handlePost.bind(this) + ); + this.route( + 'POST', + '/hooks/triggers', + middleware.promiseEnforceMasterKeyAccess, + this.handlePost.bind(this) + ); + this.route( + 'PUT', + '/hooks/functions/:functionName', + middleware.promiseEnforceMasterKeyAccess, + this.handlePut.bind(this) + ); + this.route( + 'PUT', + '/hooks/triggers/:className/:triggerName', + middleware.promiseEnforceMasterKeyAccess, + this.handlePut.bind(this) + ); } } diff --git a/src/Routers/IAPValidationRouter.js b/src/Routers/IAPValidationRouter.js index 565b87079b..ea6646cfb1 100644 --- a/src/Routers/IAPValidationRouter.js +++ b/src/Routers/IAPValidationRouter.js @@ -1,81 +1,97 @@ import PromiseRouter from '../PromiseRouter'; -var request = require("request"); -var rest = require("../rest"); +var request = require('request'); +var rest = require('../rest'); import Parse from 'parse/node'; // TODO move validation logic in IAPValidationController -const IAP_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"; -const IAP_PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt"; +const IAP_SANDBOX_URL = 'https://sandbox.itunes.apple.com/verifyReceipt'; +const IAP_PRODUCTION_URL = 'https://buy.itunes.apple.com/verifyReceipt'; const APP_STORE_ERRORS = { - 21000: "The App Store could not read the JSON object you provided.", - 21002: "The data in the receipt-data property was malformed or missing.", - 21003: "The receipt could not be authenticated.", - 21004: "The shared secret you provided does not match the shared secret on file for your account.", - 21005: "The receipt server is not currently available.", - 21006: "This receipt is valid but the subscription has expired.", - 21007: "This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.", - 21008: "This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead." -} + 21000: 'The App Store could not read the JSON object you provided.', + 21002: 'The data in the receipt-data property was malformed or missing.', + 21003: 'The receipt could not be authenticated.', + 21004: 'The shared secret you provided does not match the shared secret on file for your account.', + 21005: 'The receipt server is not currently available.', + 21006: 'This receipt is valid but the subscription has expired.', + 21007: 'This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.', + 21008: 'This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.', +}; function appStoreError(status) { status = parseInt(status); - var errorString = APP_STORE_ERRORS[status] || "unknown error."; - return { status: status, error: errorString } + var errorString = APP_STORE_ERRORS[status] || 'unknown error.'; + return { status: status, error: errorString }; } function validateWithAppStore(url, receipt) { return new Promise(function(fulfill, reject) { - request.post({ - url: url, - body: { "receipt-data": receipt }, - json: true, - }, function(err, res, body) { - var status = body.status; - if (status == 0) { - // No need to pass anything, status is OK - return fulfill(); + request.post( + { + url: url, + body: { 'receipt-data': receipt }, + json: true, + }, + function(err, res, body) { + var status = body.status; + if (status == 0) { + // No need to pass anything, status is OK + return fulfill(); + } + // receipt is from test and should go to test + return reject(body); } - // receipt is from test and should go to test - return reject(body); - }); + ); }); } function getFileForProductIdentifier(productIdentifier, req) { - return rest.find(req.config, req.auth, '_Product', { productIdentifier: productIdentifier }, undefined, req.info.clientSDK).then(function(result){ - const products = result.results; - if (!products || products.length != 1) { - // Error not found or too many - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.') - } + return rest + .find( + req.config, + req.auth, + '_Product', + { productIdentifier: productIdentifier }, + undefined, + req.info.clientSDK + ) + .then(function(result) { + const products = result.results; + if (!products || products.length != 1) { + // Error not found or too many + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found.' + ); + } - var download = products[0].download; - return Promise.resolve({response: download}); - }); + var download = products[0].download; + return Promise.resolve({ response: download }); + }); } - export class IAPValidationRouter extends PromiseRouter { - handleRequest(req) { let receipt = req.body.receipt; const productIdentifier = req.body.productIdentifier; - if (!receipt || ! productIdentifier) { + if (!receipt || !productIdentifier) { // TODO: Error, malformed request - throw new Parse.Error(Parse.Error.INVALID_JSON, "missing receipt or productIdentifier"); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'missing receipt or productIdentifier' + ); } // Transform the object if there // otherwise assume it's in Base64 already - if (typeof receipt == "object") { - if (receipt["__type"] == "Bytes") { + if (typeof receipt == 'object') { + if (receipt['__type'] == 'Bytes') { receipt = receipt.base64; } } - if (process.env.TESTING == "1" && req.body.bypassAppStoreValidation) { + if (process.env.TESTING == '1' && req.body.bypassAppStoreValidation) { return getFileForProductIdentifier(productIdentifier, req); } @@ -84,28 +100,31 @@ export class IAPValidationRouter extends PromiseRouter { } function errorCallback(error) { - return Promise.resolve({response: appStoreError(error.status) }); + return Promise.resolve({ response: appStoreError(error.status) }); } - return validateWithAppStore(IAP_PRODUCTION_URL, receipt).then(() => { - - return successCallback(); - - }, (error) => { - if (error.status == 21007) { - return validateWithAppStore(IAP_SANDBOX_URL, receipt).then(() => { - return successCallback(); - }, (error) => { - return errorCallback(error); + return validateWithAppStore(IAP_PRODUCTION_URL, receipt).then( + () => { + return successCallback(); + }, + error => { + if (error.status == 21007) { + return validateWithAppStore(IAP_SANDBOX_URL, receipt).then( + () => { + return successCallback(); + }, + error => { + return errorCallback(error); + } + ); } - ); - } - return errorCallback(error); - }); + return errorCallback(error); + } + ); } mountRoutes() { - this.route("POST","/validate_purchase", this.handleRequest); + this.route('POST', '/validate_purchase', this.handleRequest); } } diff --git a/src/Routers/InstallationsRouter.js b/src/Routers/InstallationsRouter.js index 90ab113eb6..a35afd9bb1 100644 --- a/src/Routers/InstallationsRouter.js +++ b/src/Routers/InstallationsRouter.js @@ -9,21 +9,41 @@ export class InstallationsRouter extends ClassesRouter { } handleFind(req) { - const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); + const body = Object.assign( + req.body, + ClassesRouter.JSONFromQuery(req.query) + ); const options = ClassesRouter.optionsFromBody(body); - return rest.find(req.config, req.auth, - '_Installation', body.where, options, req.info.clientSDK) - .then((response) => { - return {response: response}; + return rest + .find( + req.config, + req.auth, + '_Installation', + body.where, + options, + req.info.clientSDK + ) + .then(response => { + return { response: response }; }); } mountRoutes() { - this.route('GET','/installations', req => { return this.handleFind(req); }); - this.route('GET','/installations/:objectId', req => { return this.handleGet(req); }); - this.route('POST','/installations', req => { return this.handleCreate(req); }); - this.route('PUT','/installations/:objectId', req => { return this.handleUpdate(req); }); - this.route('DELETE','/installations/:objectId', req => { return this.handleDelete(req); }); + this.route('GET', '/installations', req => { + return this.handleFind(req); + }); + this.route('GET', '/installations/:objectId', req => { + return this.handleGet(req); + }); + this.route('POST', '/installations', req => { + return this.handleCreate(req); + }); + this.route('PUT', '/installations/:objectId', req => { + return this.handleUpdate(req); + }); + this.route('DELETE', '/installations/:objectId', req => { + return this.handleDelete(req); + }); } } diff --git a/src/Routers/LogsRouter.js b/src/Routers/LogsRouter.js index 5848aa4738..0bc2e23455 100644 --- a/src/Routers/LogsRouter.js +++ b/src/Routers/LogsRouter.js @@ -1,19 +1,26 @@ import { Parse } from 'parse/node'; import PromiseRouter from '../PromiseRouter'; -import * as middleware from "../middlewares"; +import * as middleware from '../middlewares'; export class LogsRouter extends PromiseRouter { - mountRoutes() { - this.route('GET','/scriptlog', middleware.promiseEnforceMasterKeyAccess, this.validateRequest, (req) => { - return this.handleGET(req); - }); + this.route( + 'GET', + '/scriptlog', + middleware.promiseEnforceMasterKeyAccess, + this.validateRequest, + req => { + return this.handleGET(req); + } + ); } validateRequest(req) { if (!req.config || !req.config.loggerController) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Logger adapter is not available'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + 'Logger adapter is not available' + ); } } @@ -33,21 +40,21 @@ export class LogsRouter extends PromiseRouter { size = req.query.n; } - const order = req.query.order + const order = req.query.order; const level = req.query.level; const options = { from, until, size, order, - level + level, }; - return req.config.loggerController.getLogs(options).then((result) => { + return req.config.loggerController.getLogs(options).then(result => { return Promise.resolve({ - response: result + response: result, }); - }) + }); } } diff --git a/src/Routers/PublicAPIRouter.js b/src/Routers/PublicAPIRouter.js index a126423cb0..15d636c5e2 100644 --- a/src/Routers/PublicAPIRouter.js +++ b/src/Routers/PublicAPIRouter.js @@ -5,17 +5,16 @@ import path from 'path'; import fs from 'fs'; import qs from 'querystring'; -const public_html = path.resolve(__dirname, "../../public_html"); +const public_html = path.resolve(__dirname, '../../public_html'); const views = path.resolve(__dirname, '../../views'); export class PublicAPIRouter extends PromiseRouter { - verifyEmail(req) { const { token, username } = req.query; const appId = req.params.appId; const config = Config.get(appId); - if(!config){ + if (!config) { this.invalidRequest(); } @@ -28,15 +27,18 @@ export class PublicAPIRouter extends PromiseRouter { } const userController = config.userController; - return userController.verifyEmail(username, token).then(() => { - const params = qs.stringify({username}); - return Promise.resolve({ - status: 302, - location: `${config.verifyEmailSuccessURL}?${params}` - }); - }, ()=> { - return this.invalidVerificationLink(req); - }) + return userController.verifyEmail(username, token).then( + () => { + const params = qs.stringify({ username }); + return Promise.resolve({ + status: 302, + location: `${config.verifyEmailSuccessURL}?${params}`, + }); + }, + () => { + return this.invalidVerificationLink(req); + } + ); } resendVerificationEmail(req) { @@ -44,7 +46,7 @@ export class PublicAPIRouter extends PromiseRouter { const appId = req.params.appId; const config = Config.get(appId); - if(!config){ + if (!config) { this.invalidRequest(); } @@ -58,51 +60,60 @@ export class PublicAPIRouter extends PromiseRouter { const userController = config.userController; - return userController.resendVerificationEmail(username).then(() => { - return Promise.resolve({ - status: 302, - location: `${config.linkSendSuccessURL}` - }); - }, ()=> { - return Promise.resolve({ - status: 302, - location: `${config.linkSendFailURL}` - }); - }) + return userController.resendVerificationEmail(username).then( + () => { + return Promise.resolve({ + status: 302, + location: `${config.linkSendSuccessURL}`, + }); + }, + () => { + return Promise.resolve({ + status: 302, + location: `${config.linkSendFailURL}`, + }); + } + ); } changePassword(req) { return new Promise((resolve, reject) => { const config = Config.get(req.query.id); - if(!config){ + if (!config) { this.invalidRequest(); } if (!config.publicServerURL) { return resolve({ status: 404, - text: 'Not found.' + text: 'Not found.', }); } // Should we keep the file in memory or leave like that? - fs.readFile(path.resolve(views, "choose_password"), 'utf-8', (err, data) => { - if (err) { - return reject(err); + fs.readFile( + path.resolve(views, 'choose_password'), + 'utf-8', + (err, data) => { + if (err) { + return reject(err); + } + data = data.replace( + 'PARSE_SERVER_URL', + `'${config.publicServerURL}'` + ); + resolve({ + text: data, + }); } - data = data.replace("PARSE_SERVER_URL", `'${config.publicServerURL}'`); - resolve({ - text: data - }) - }); + ); }); } requestResetPassword(req) { - const config = req.config; - if(!config){ + if (!config) { this.invalidRequest(); } @@ -116,22 +127,29 @@ export class PublicAPIRouter extends PromiseRouter { return this.invalidLink(req); } - return config.userController.checkResetTokenValidity(username, token).then(() => { - const params = qs.stringify({token, id: config.applicationId, username, app: config.appName, }); - return Promise.resolve({ - status: 302, - location: `${config.choosePasswordURL}?${params}` - }) - }, () => { - return this.invalidLink(req); - }) + return config.userController.checkResetTokenValidity(username, token).then( + () => { + const params = qs.stringify({ + token, + id: config.applicationId, + username, + app: config.appName, + }); + return Promise.resolve({ + status: 302, + location: `${config.choosePasswordURL}?${params}`, + }); + }, + () => { + return this.invalidLink(req); + } + ); } resetPassword(req) { - const config = req.config; - if(!config){ + if (!config) { this.invalidRequest(); } @@ -139,46 +157,55 @@ export class PublicAPIRouter extends PromiseRouter { return this.missingPublicServerURL(); } - const { - username, - token, - new_password - } = req.body; + const { username, token, new_password } = req.body; if (!username || !token || !new_password) { return this.invalidLink(req); } - return config.userController.updatePassword(username, token, new_password).then(() => { - const params = qs.stringify({username: username}); - return Promise.resolve({ - status: 302, - location: `${config.passwordResetSuccessURL}?${params}` - }); - }, (err) => { - const params = qs.stringify({username: username, token: token, id: config.applicationId, error:err, app:config.appName}); - return Promise.resolve({ - status: 302, - location: `${config.choosePasswordURL}?${params}` - }); - }); - + return config.userController + .updatePassword(username, token, new_password) + .then( + () => { + const params = qs.stringify({ username: username }); + return Promise.resolve({ + status: 302, + location: `${config.passwordResetSuccessURL}?${params}`, + }); + }, + err => { + const params = qs.stringify({ + username: username, + token: token, + id: config.applicationId, + error: err, + app: config.appName, + }); + return Promise.resolve({ + status: 302, + location: `${config.choosePasswordURL}?${params}`, + }); + } + ); } invalidLink(req) { return Promise.resolve({ status: 302, - location: req.config.invalidLinkURL + location: req.config.invalidLinkURL, }); } invalidVerificationLink(req) { const config = req.config; if (req.query.username && req.params.appId) { - const params = qs.stringify({username: req.query.username, appId: req.params.appId}); + const params = qs.stringify({ + username: req.query.username, + appId: req.params.appId, + }); return Promise.resolve({ status: 302, - location: `${config.invalidVerificationLinkURL}?${params}` + location: `${config.invalidVerificationLinkURL}?${params}`, }); } else { return this.invalidLink(req); @@ -187,15 +214,15 @@ export class PublicAPIRouter extends PromiseRouter { missingPublicServerURL() { return Promise.resolve({ - text: 'Not found.', - status: 404 + text: 'Not found.', + status: 404, }); } invalidRequest() { const error = new Error(); error.status = 403; - error.message = "unauthorized"; + error.message = 'unauthorized'; throw error; } @@ -205,30 +232,59 @@ export class PublicAPIRouter extends PromiseRouter { } mountRoutes() { - this.route('GET','/apps/:appId/verify_email', - req => { this.setConfig(req) }, - req => { return this.verifyEmail(req); }); - - this.route('POST', '/apps/:appId/resend_verification_email', - req => { this.setConfig(req); }, - req => { return this.resendVerificationEmail(req); }); - - this.route('GET','/apps/choose_password', - req => { return this.changePassword(req); }); + this.route( + 'GET', + '/apps/:appId/verify_email', + req => { + this.setConfig(req); + }, + req => { + return this.verifyEmail(req); + } + ); + + this.route( + 'POST', + '/apps/:appId/resend_verification_email', + req => { + this.setConfig(req); + }, + req => { + return this.resendVerificationEmail(req); + } + ); - this.route('POST','/apps/:appId/request_password_reset', - req => { this.setConfig(req) }, - req => { return this.resetPassword(req); }); + this.route('GET', '/apps/choose_password', req => { + return this.changePassword(req); + }); - this.route('GET','/apps/:appId/request_password_reset', - req => { this.setConfig(req) }, - req => { return this.requestResetPassword(req); }); + this.route( + 'POST', + '/apps/:appId/request_password_reset', + req => { + this.setConfig(req); + }, + req => { + return this.resetPassword(req); + } + ); + + this.route( + 'GET', + '/apps/:appId/request_password_reset', + req => { + this.setConfig(req); + }, + req => { + return this.requestResetPassword(req); + } + ); } expressRouter() { const router = express.Router(); - router.use("/apps", express.static(public_html)); - router.use("/", super.expressRouter()); + router.use('/apps', express.static(public_html)); + router.use('/', super.expressRouter()); return router; } } diff --git a/src/Routers/PurgeRouter.js b/src/Routers/PurgeRouter.js index b9009453c6..6d0aca0c3e 100644 --- a/src/Routers/PurgeRouter.js +++ b/src/Routers/PurgeRouter.js @@ -3,12 +3,15 @@ import * as middleware from '../middlewares'; import Parse from 'parse/node'; export class PurgeRouter extends PromiseRouter { - handlePurge(req) { if (req.auth.isReadOnly) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'read-only masterKey isn\'t allowed to purge a schema.'); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to purge a schema." + ); } - return req.config.database.purgeCollection(req.params.className) + return req.config.database + .purgeCollection(req.params.className) .then(() => { var cacheAdapter = req.config.cacheController; if (req.params.className == '_Session') { @@ -16,17 +19,25 @@ export class PurgeRouter extends PromiseRouter { } else if (req.params.className == '_Role') { cacheAdapter.role.clear(); } - return {response: {}}; - }).catch((error) => { + return { response: {} }; + }) + .catch(error => { if (!error || (error && error.code === Parse.Error.OBJECT_NOT_FOUND)) { - return {response: {}}; + return { response: {} }; } throw error; }); } mountRoutes() { - this.route('DELETE', '/purge/:className', middleware.promiseEnforceMasterKeyAccess, (req) => { return this.handlePurge(req); }); + this.route( + 'DELETE', + '/purge/:className', + middleware.promiseEnforceMasterKeyAccess, + req => { + return this.handlePurge(req); + } + ); } } diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js index 14def667e6..b6542fce57 100644 --- a/src/Routers/PushRouter.js +++ b/src/Routers/PushRouter.js @@ -1,41 +1,56 @@ -import PromiseRouter from '../PromiseRouter'; -import * as middleware from "../middlewares"; -import { Parse } from "parse/node"; +import PromiseRouter from '../PromiseRouter'; +import * as middleware from '../middlewares'; +import { Parse } from 'parse/node'; export class PushRouter extends PromiseRouter { - mountRoutes() { - this.route("POST", "/push", middleware.promiseEnforceMasterKeyAccess, PushRouter.handlePOST); + this.route( + 'POST', + '/push', + middleware.promiseEnforceMasterKeyAccess, + PushRouter.handlePOST + ); } static handlePOST(req) { if (req.auth.isReadOnly) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'read-only masterKey isn\'t allowed to send push notifications.'); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to send push notifications." + ); } const pushController = req.config.pushController; if (!pushController) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Push controller is not set'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + 'Push controller is not set' + ); } const where = PushRouter.getQueryCondition(req); let resolve; - const promise = new Promise((_resolve) => { + const promise = new Promise(_resolve => { resolve = _resolve; }); let pushStatusId; - pushController.sendPush(req.body, where, req.config, req.auth, (objectId) => { - pushStatusId = objectId; - resolve({ - headers: { - 'X-Parse-Push-Status-Id': pushStatusId - }, - response: { - result: true - } + pushController + .sendPush(req.body, where, req.config, req.auth, objectId => { + pushStatusId = objectId; + resolve({ + headers: { + 'X-Parse-Push-Status-Id': pushStatusId, + }, + response: { + result: true, + }, + }); + }) + .catch(err => { + req.config.loggerController.error( + `_PushStatus ${pushStatusId}: error while sending push`, + err + ); }); - }).catch((err) => { - req.config.loggerController.error(`_PushStatus ${pushStatusId}: error while sending push`, err); - }); return promise; } @@ -51,18 +66,23 @@ export class PushRouter extends PromiseRouter { let where; if (hasWhere && hasChannels) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Channels and query can not be set at the same time.'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + 'Channels and query can not be set at the same time.' + ); } else if (hasWhere) { where = body.where; } else if (hasChannels) { where = { - "channels": { - "$in": body.channels - } - } + channels: { + $in: body.channels, + }, + }; } else { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'Sending a push requires either "channels" or a "where" query.'); + throw new Parse.Error( + Parse.Error.PUSH_MISCONFIGURED, + 'Sending a push requires either "channels" or a "where" query.' + ); } return where; } diff --git a/src/Routers/RolesRouter.js b/src/Routers/RolesRouter.js index 67fea13817..e6a10df77b 100644 --- a/src/Routers/RolesRouter.js +++ b/src/Routers/RolesRouter.js @@ -1,4 +1,3 @@ - import ClassesRouter from './ClassesRouter'; export class RolesRouter extends ClassesRouter { @@ -7,11 +6,21 @@ export class RolesRouter extends ClassesRouter { } mountRoutes() { - this.route('GET','/roles', req => { return this.handleFind(req); }); - this.route('GET','/roles/:objectId', req => { return this.handleGet(req); }); - this.route('POST','/roles', req => { return this.handleCreate(req); }); - this.route('PUT','/roles/:objectId', req => { return this.handleUpdate(req); }); - this.route('DELETE','/roles/:objectId', req => { return this.handleDelete(req); }); + this.route('GET', '/roles', req => { + return this.handleFind(req); + }); + this.route('GET', '/roles/:objectId', req => { + return this.handleGet(req); + }); + this.route('POST', '/roles', req => { + return this.handleCreate(req); + }); + this.route('PUT', '/roles/:objectId', req => { + return this.handleUpdate(req); + }); + this.route('DELETE', '/roles/:objectId', req => { + return this.handleDelete(req); + }); } } diff --git a/src/Routers/SchemasRouter.js b/src/Routers/SchemasRouter.js index f21c14c217..efc831dc04 100644 --- a/src/Routers/SchemasRouter.js +++ b/src/Routers/SchemasRouter.js @@ -3,8 +3,8 @@ var Parse = require('parse/node').Parse, SchemaController = require('../Controllers/SchemaController'); -import PromiseRouter from '../PromiseRouter'; -import * as middleware from "../middlewares"; +import PromiseRouter from '../PromiseRouter'; +import * as middleware from '../middlewares'; function classNameMismatchResponse(bodyClass, pathClass) { throw new Parse.Error( @@ -14,32 +14,46 @@ function classNameMismatchResponse(bodyClass, pathClass) { } function getAllSchemas(req) { - return req.config.database.loadSchema({ clearCache: true}) + return req.config.database + .loadSchema({ clearCache: true }) .then(schemaController => schemaController.getAllClasses(true)) .then(schemas => ({ response: { results: schemas } })); } function getOneSchema(req) { const className = req.params.className; - return req.config.database.loadSchema({ clearCache: true}) + return req.config.database + .loadSchema({ clearCache: true }) .then(schemaController => schemaController.getOneSchema(className, true)) .then(schema => ({ response: schema })) .catch(error => { if (error === undefined) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`); + throw new Parse.Error( + Parse.Error.INVALID_CLASS_NAME, + `Class ${className} does not exist.` + ); } else { - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error.'); + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + 'Database adapter error.' + ); } }); } function createSchema(req) { if (req.auth.isReadOnly) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'read-only masterKey isn\'t allowed to create a schema.'); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to create a schema." + ); } if (req.params.className && req.body.className) { if (req.params.className != req.body.className) { - return classNameMismatchResponse(req.body.className, req.params.className); + return classNameMismatchResponse( + req.body.className, + req.params.className + ); } } @@ -48,14 +62,25 @@ function createSchema(req) { throw new Parse.Error(135, `POST ${req.path} needs a class name.`); } - return req.config.database.loadSchema({ clearCache: true}) - .then(schema => schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions, req.body.indexes)) + return req.config.database + .loadSchema({ clearCache: true }) + .then(schema => + schema.addClassIfNotExists( + className, + req.body.fields, + req.body.classLevelPermissions, + req.body.indexes + ) + ) .then(schema => ({ response: schema })); } function modifySchema(req) { if (req.auth.isReadOnly) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'read-only masterKey isn\'t allowed to update a schema.'); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to update a schema." + ); } if (req.body.className && req.body.className != req.params.className) { return classNameMismatchResponse(req.body.className, req.params.className); @@ -64,29 +89,75 @@ function modifySchema(req) { const submittedFields = req.body.fields || {}; const className = req.params.className; - return req.config.database.loadSchema({ clearCache: true}) - .then(schema => schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.body.indexes, req.config.database)) - .then(result => ({response: result})); + return req.config.database + .loadSchema({ clearCache: true }) + .then(schema => + schema.updateClass( + className, + submittedFields, + req.body.classLevelPermissions, + req.body.indexes, + req.config.database + ) + ) + .then(result => ({ response: result })); } const deleteSchema = req => { if (req.auth.isReadOnly) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'read-only masterKey isn\'t allowed to delete a schema.'); + throw new Parse.Error( + Parse.Error.OPERATION_FORBIDDEN, + "read-only masterKey isn't allowed to delete a schema." + ); } if (!SchemaController.classNameIsValid(req.params.className)) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, SchemaController.invalidClassNameMessage(req.params.className)); + throw new Parse.Error( + Parse.Error.INVALID_CLASS_NAME, + SchemaController.invalidClassNameMessage(req.params.className) + ); } - return req.config.database.deleteSchema(req.params.className) + return req.config.database + .deleteSchema(req.params.className) .then(() => ({ response: {} })); -} +}; export class SchemasRouter extends PromiseRouter { mountRoutes() { - this.route('GET', '/schemas', middleware.promiseEnforceMasterKeyAccess, getAllSchemas); - this.route('GET', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, getOneSchema); - this.route('POST', '/schemas', middleware.promiseEnforceMasterKeyAccess, createSchema); - this.route('POST', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, createSchema); - this.route('PUT', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, modifySchema); - this.route('DELETE', '/schemas/:className', middleware.promiseEnforceMasterKeyAccess, deleteSchema); + this.route( + 'GET', + '/schemas', + middleware.promiseEnforceMasterKeyAccess, + getAllSchemas + ); + this.route( + 'GET', + '/schemas/:className', + middleware.promiseEnforceMasterKeyAccess, + getOneSchema + ); + this.route( + 'POST', + '/schemas', + middleware.promiseEnforceMasterKeyAccess, + createSchema + ); + this.route( + 'POST', + '/schemas/:className', + middleware.promiseEnforceMasterKeyAccess, + createSchema + ); + this.route( + 'PUT', + '/schemas/:className', + middleware.promiseEnforceMasterKeyAccess, + modifySchema + ); + this.route( + 'DELETE', + '/schemas/:className', + middleware.promiseEnforceMasterKeyAccess, + deleteSchema + ); } } diff --git a/src/Routers/SessionsRouter.js b/src/Routers/SessionsRouter.js index 52b9ba6f5a..4b8488bf43 100644 --- a/src/Routers/SessionsRouter.js +++ b/src/Routers/SessionsRouter.js @@ -1,11 +1,9 @@ - import ClassesRouter from './ClassesRouter'; -import Parse from 'parse/node'; -import rest from '../rest'; -import Auth from '../Auth'; +import Parse from 'parse/node'; +import rest from '../rest'; +import Auth from '../Auth'; export class SessionsRouter extends ClassesRouter { - className() { return '_Session'; } @@ -13,17 +11,29 @@ export class SessionsRouter extends ClassesRouter { handleMe(req) { // TODO: Verify correct behavior if (!req.info || !req.info.sessionToken) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token required.'); + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Session token required.' + ); } - return rest.find(req.config, Auth.master(req.config), '_Session', { sessionToken: req.info.sessionToken }, undefined, req.info.clientSDK) - .then((response) => { + return rest + .find( + req.config, + Auth.master(req.config), + '_Session', + { sessionToken: req.info.sessionToken }, + undefined, + req.info.clientSDK + ) + .then(response => { if (!response.results || response.results.length == 0) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, - 'Session token not found.'); + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Session token not found.' + ); } return { - response: response.results[0] + response: response.results[0], }; }); } @@ -36,37 +46,54 @@ export class SessionsRouter extends ClassesRouter { if (!user) { throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'invalid session'); } - const { - sessionData, - createSession - } = Auth.createSession(config, { + const { sessionData, createSession } = Auth.createSession(config, { userId: user.id, createdWith: { - 'action': 'upgrade', + action: 'upgrade', }, installationId: req.auth.installationId, }); - return createSession().then(() => { - // delete the session token, use the db to skip beforeSave - return config.database.update('_User', { - objectId: user.id - }, { - sessionToken: {__op: 'Delete'} + return createSession() + .then(() => { + // delete the session token, use the db to skip beforeSave + return config.database.update( + '_User', + { + objectId: user.id, + }, + { + sessionToken: { __op: 'Delete' }, + } + ); + }) + .then(() => { + return Promise.resolve({ response: sessionData }); }); - }).then(() => { - return Promise.resolve({ response: sessionData }); - }); } mountRoutes() { - this.route('GET','/sessions/me', req => { return this.handleMe(req); }); - this.route('GET', '/sessions', req => { return this.handleFind(req); }); - this.route('GET', '/sessions/:objectId', req => { return this.handleGet(req); }); - this.route('POST', '/sessions', req => { return this.handleCreate(req); }); - this.route('PUT', '/sessions/:objectId', req => { return this.handleUpdate(req); }); - this.route('DELETE', '/sessions/:objectId', req => { return this.handleDelete(req); }); - this.route('POST', '/upgradeToRevocableSession', req => { return this.handleUpdateToRevocableSession(req); }) + this.route('GET', '/sessions/me', req => { + return this.handleMe(req); + }); + this.route('GET', '/sessions', req => { + return this.handleFind(req); + }); + this.route('GET', '/sessions/:objectId', req => { + return this.handleGet(req); + }); + this.route('POST', '/sessions', req => { + return this.handleCreate(req); + }); + this.route('PUT', '/sessions/:objectId', req => { + return this.handleUpdate(req); + }); + this.route('DELETE', '/sessions/:objectId', req => { + return this.handleDelete(req); + }); + this.route('POST', '/upgradeToRevocableSession', req => { + return this.handleUpdateToRevocableSession(req); + }); } } diff --git a/src/Routers/UsersRouter.js b/src/Routers/UsersRouter.js index 3c8690ea50..73b46415d5 100644 --- a/src/Routers/UsersRouter.js +++ b/src/Routers/UsersRouter.js @@ -9,7 +9,6 @@ import Auth from '../Auth'; import passwordCrypto from '../password'; export class UsersRouter extends ClassesRouter { - className() { return '_User'; } @@ -22,7 +21,7 @@ export class UsersRouter extends ClassesRouter { for (var key in obj) { if (obj.hasOwnProperty(key)) { // Regexp comes from Parse.Object.prototype.validate - if (key !== "__type" && !(/^[A-Za-z][0-9A-Za-z_]*$/).test(key)) { + if (key !== '__type' && !/^[A-Za-z][0-9A-Za-z_]*$/.test(key)) { delete obj[key]; } } @@ -39,26 +38,36 @@ export class UsersRouter extends ClassesRouter { return new Promise((resolve, reject) => { // Use query parameters instead if provided in url let payload = req.body; - if (!payload.username && req.query.username || !payload.email && req.query.email) { + if ( + (!payload.username && req.query.username) || + (!payload.email && req.query.email) + ) { payload = req.query; } - const { - username, - email, - password, - } = payload; + const { username, email, password } = payload; // TODO: use the right error codes / descriptions. if (!username && !email) { - throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'username/email is required.'); + throw new Parse.Error( + Parse.Error.USERNAME_MISSING, + 'username/email is required.' + ); } if (!password) { - throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'password is required.'); + throw new Parse.Error( + Parse.Error.PASSWORD_MISSING, + 'password is required.' + ); } - if (typeof password !== 'string' - || email && typeof email !== 'string' - || username && typeof username !== 'string') { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); + if ( + typeof password !== 'string' || + (email && typeof email !== 'string') || + (username && typeof username !== 'string') + ) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Invalid username/password.' + ); } let user; @@ -71,39 +80,63 @@ export class UsersRouter extends ClassesRouter { } else { query = { $or: [{ username }, { email: username }] }; } - return req.config.database.find('_User', query) - .then((results) => { + return req.config.database + .find('_User', query) + .then(results => { if (!results.length) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Invalid username/password.' + ); } - if (results.length > 1) { // corner case where user1 has username == user2 email - req.config.loggerController.warn('There is a user which email is the same as another user\'s username, logging in based on username'); - user = results.filter((user) => user.username === username)[0]; + if (results.length > 1) { + // corner case where user1 has username == user2 email + req.config.loggerController.warn( + "There is a user which email is the same as another user's username, logging in based on username" + ); + user = results.filter(user => user.username === username)[0]; } else { user = results[0]; } return passwordCrypto.compare(password, user.password); }) - .then((correct) => { + .then(correct => { isValidPassword = correct; const accountLockoutPolicy = new AccountLockout(user, req.config); return accountLockoutPolicy.handleLoginAttempt(isValidPassword); }) .then(() => { if (!isValidPassword) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Invalid username/password.' + ); } // Ensure the user isn't locked out // A locked out user won't be able to login // To lock a user out, just set the ACL to `masterKey` only ({}). // Empty ACL is OK - if (!req.auth.isMaster && user.ACL && Object.keys(user.ACL).length == 0) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); + if ( + !req.auth.isMaster && + user.ACL && + Object.keys(user.ACL).length == 0 + ) { + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Invalid username/password.' + ); } - if (req.config.verifyUserEmails && req.config.preventLoginWithUnverifiedEmail && !user.emailVerified) { - throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); + if ( + req.config.verifyUserEmails && + req.config.preventLoginWithUnverifiedEmail && + !user.emailVerified + ) { + throw new Parse.Error( + Parse.Error.EMAIL_NOT_FOUND, + 'User email is not verified.' + ); } delete user.password; @@ -111,7 +144,7 @@ export class UsersRouter extends ClassesRouter { // Sometimes the authData still has null on that keys // https://github.com/parse-community/parse-server/issues/935 if (user.authData) { - Object.keys(user.authData).forEach((provider) => { + Object.keys(user.authData).forEach(provider => { if (user.authData[provider] === null) { delete user.authData[provider]; } @@ -122,7 +155,8 @@ export class UsersRouter extends ClassesRouter { } return resolve(user); - }).catch((error) => { + }) + .catch(error => { return reject(error); }); }); @@ -130,17 +164,31 @@ export class UsersRouter extends ClassesRouter { handleMe(req) { if (!req.info || !req.info.sessionToken) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); } const sessionToken = req.info.sessionToken; - return rest.find(req.config, Auth.master(req.config), '_Session', - { sessionToken }, - { include: 'user' }, req.info.clientSDK) - .then((response) => { - if (!response.results || + return rest + .find( + req.config, + Auth.master(req.config), + '_Session', + { sessionToken }, + { include: 'user' }, + req.info.clientSDK + ) + .then(response => { + if ( + !response.results || response.results.length == 0 || - !response.results[0].user) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + !response.results[0].user + ) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); } else { const user = response.results[0].user; // Send token back on the login, because SDKs expect that. @@ -157,43 +205,54 @@ export class UsersRouter extends ClassesRouter { handleLogIn(req) { let user; return this._authenticateUserFromRequest(req) - .then((res) => { - + .then(res => { user = res; // handle password expiry policy - if (req.config.passwordPolicy && req.config.passwordPolicy.maxPasswordAge) { + if ( + req.config.passwordPolicy && + req.config.passwordPolicy.maxPasswordAge + ) { let changedAt = user._password_changed_at; if (!changedAt) { // password was created before expiry policy was enabled. // simply update _User object so that it will start enforcing from now changedAt = new Date(); - req.config.database.update('_User', { username: user.username }, - { _password_changed_at: Parse._encode(changedAt) }); + req.config.database.update( + '_User', + { username: user.username }, + { _password_changed_at: Parse._encode(changedAt) } + ); } else { // check whether the password has expired if (changedAt.__type == 'Date') { changedAt = new Date(changedAt.iso); } // Calculate the expiry time. - const expiresAt = new Date(changedAt.getTime() + 86400000 * req.config.passwordPolicy.maxPasswordAge); - if (expiresAt < new Date()) // fail of current time is past password expiry time - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Your password has expired. Please reset your password.'); + const expiresAt = new Date( + changedAt.getTime() + + 86400000 * req.config.passwordPolicy.maxPasswordAge + ); + if (expiresAt < new Date()) + // fail of current time is past password expiry time + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Your password has expired. Please reset your password.' + ); } } // Remove hidden properties. UsersRouter.removeHiddenProperties(user); - const { - sessionData, - createSession - } = Auth.createSession(req.config, { - userId: user.objectId, createdWith: { - 'action': 'login', - 'authProvider': 'password' - }, installationId: req.info.installationId + const { sessionData, createSession } = Auth.createSession(req.config, { + userId: user.objectId, + createdWith: { + action: 'login', + authProvider: 'password', + }, + installationId: req.info.installationId, }); user.sessionToken = sessionData.sessionToken; @@ -209,13 +268,13 @@ export class UsersRouter extends ClassesRouter { handleVerifyPassword(req) { return this._authenticateUserFromRequest(req) - .then((user) => { - + .then(user => { // Remove hidden properties. UsersRouter.removeHiddenProperties(user); return { response: user }; - }).catch((error) => { + }) + .catch(error => { throw error; }); } @@ -223,18 +282,30 @@ export class UsersRouter extends ClassesRouter { handleLogOut(req) { const success = { response: {} }; if (req.info && req.info.sessionToken) { - return rest.find(req.config, Auth.master(req.config), '_Session', - { sessionToken: req.info.sessionToken }, undefined, req.info.clientSDK - ).then((records) => { - if (records.results && records.results.length) { - return rest.del(req.config, Auth.master(req.config), '_Session', - records.results[0].objectId - ).then(() => { - return Promise.resolve(success); - }); - } - return Promise.resolve(success); - }); + return rest + .find( + req.config, + Auth.master(req.config), + '_Session', + { sessionToken: req.info.sessionToken }, + undefined, + req.info.clientSDK + ) + .then(records => { + if (records.results && records.results.length) { + return rest + .del( + req.config, + Auth.master(req.config), + '_Session', + records.results[0].objectId + ) + .then(() => { + return Promise.resolve(success); + }); + } + return Promise.resolve(success); + }); } return Promise.resolve(success); } @@ -245,12 +316,16 @@ export class UsersRouter extends ClassesRouter { emailAdapter: req.config.userController.adapter, appName: req.config.appName, publicServerURL: req.config.publicServerURL, - emailVerifyTokenValidityDuration: req.config.emailVerifyTokenValidityDuration + emailVerifyTokenValidityDuration: + req.config.emailVerifyTokenValidityDuration, }); } catch (e) { if (typeof e === 'string') { // Maybe we need a Bad Configuration error, but the SDKs won't understand it. For now, Internal Server Error. - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.'); + throw new Parse.Error( + Parse.Error.INTERNAL_SERVER_ERROR, + 'An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.' + ); } else { throw e; } @@ -262,23 +337,35 @@ export class UsersRouter extends ClassesRouter { const { email } = req.body; if (!email) { - throw new Parse.Error(Parse.Error.EMAIL_MISSING, "you must provide an email"); + throw new Parse.Error( + Parse.Error.EMAIL_MISSING, + 'you must provide an email' + ); } if (typeof email !== 'string') { - throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string'); + throw new Parse.Error( + Parse.Error.INVALID_EMAIL_ADDRESS, + 'you must provide a valid email string' + ); } const userController = req.config.userController; - return userController.sendPasswordResetEmail(email).then(() => { - return Promise.resolve({ - response: {} - }); - }, err => { - if (err.code === Parse.Error.OBJECT_NOT_FOUND) { - throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}.`); - } else { - throw err; + return userController.sendPasswordResetEmail(email).then( + () => { + return Promise.resolve({ + response: {}, + }); + }, + err => { + if (err.code === Parse.Error.OBJECT_NOT_FOUND) { + throw new Parse.Error( + Parse.Error.EMAIL_NOT_FOUND, + `No user found with email ${email}.` + ); + } else { + throw err; + } } - }); + ); } handleVerificationEmailRequest(req) { @@ -286,15 +373,24 @@ export class UsersRouter extends ClassesRouter { const { email } = req.body; if (!email) { - throw new Parse.Error(Parse.Error.EMAIL_MISSING, 'you must provide an email'); + throw new Parse.Error( + Parse.Error.EMAIL_MISSING, + 'you must provide an email' + ); } if (typeof email !== 'string') { - throw new Parse.Error(Parse.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string'); + throw new Parse.Error( + Parse.Error.INVALID_EMAIL_ADDRESS, + 'you must provide a valid email string' + ); } - return req.config.database.find('_User', { email: email }).then((results) => { + return req.config.database.find('_User', { email: email }).then(results => { if (!results.length || results.length < 1) { - throw new Parse.Error(Parse.Error.EMAIL_NOT_FOUND, `No user found with email ${email}`); + throw new Parse.Error( + Parse.Error.EMAIL_NOT_FOUND, + `No user found with email ${email}` + ); } const user = results[0]; @@ -302,7 +398,10 @@ export class UsersRouter extends ClassesRouter { delete user.password; if (user.emailVerified) { - throw new Parse.Error(Parse.Error.OTHER_CAUSE, `Email ${email} is already verified.`); + throw new Parse.Error( + Parse.Error.OTHER_CAUSE, + `Email ${email} is already verified.` + ); } const userController = req.config.userController; @@ -313,20 +412,43 @@ export class UsersRouter extends ClassesRouter { }); } - mountRoutes() { - this.route('GET', '/users', req => { return this.handleFind(req); }); - this.route('POST', '/users', req => { return this.handleCreate(req); }); - this.route('GET', '/users/me', req => { return this.handleMe(req); }); - this.route('GET', '/users/:objectId', req => { return this.handleGet(req); }); - this.route('PUT', '/users/:objectId', req => { return this.handleUpdate(req); }); - this.route('DELETE', '/users/:objectId', req => { return this.handleDelete(req); }); - this.route('GET', '/login', req => { return this.handleLogIn(req); }); - this.route('POST', '/login', req => { return this.handleLogIn(req); }); - this.route('POST', '/logout', req => { return this.handleLogOut(req); }); - this.route('POST', '/requestPasswordReset', req => { return this.handleResetRequest(req); }); - this.route('POST', '/verificationEmailRequest', req => { return this.handleVerificationEmailRequest(req); }); - this.route('GET', '/verifyPassword', req => { return this.handleVerifyPassword(req); }); + this.route('GET', '/users', req => { + return this.handleFind(req); + }); + this.route('POST', '/users', req => { + return this.handleCreate(req); + }); + this.route('GET', '/users/me', req => { + return this.handleMe(req); + }); + this.route('GET', '/users/:objectId', req => { + return this.handleGet(req); + }); + this.route('PUT', '/users/:objectId', req => { + return this.handleUpdate(req); + }); + this.route('DELETE', '/users/:objectId', req => { + return this.handleDelete(req); + }); + this.route('GET', '/login', req => { + return this.handleLogIn(req); + }); + this.route('POST', '/login', req => { + return this.handleLogIn(req); + }); + this.route('POST', '/logout', req => { + return this.handleLogOut(req); + }); + this.route('POST', '/requestPasswordReset', req => { + return this.handleResetRequest(req); + }); + this.route('POST', '/verificationEmailRequest', req => { + return this.handleVerificationEmailRequest(req); + }); + this.route('GET', '/verifyPassword', req => { + return this.handleVerifyPassword(req); + }); } } diff --git a/src/StatusHandler.js b/src/StatusHandler.js index 601fbfe53f..43cd10750e 100644 --- a/src/StatusHandler.js +++ b/src/StatusHandler.js @@ -1,24 +1,24 @@ import { md5Hash, newObjectId } from './cryptoUtils'; -import { logger } from './logger'; -import rest from './rest'; -import Auth from './Auth'; +import { logger } from './logger'; +import rest from './rest'; +import Auth from './Auth'; const PUSH_STATUS_COLLECTION = '_PushStatus'; const JOB_STATUS_COLLECTION = '_JobStatus'; const incrementOp = function(object = {}, key, amount = 1) { if (!object[key]) { - object[key] = {__op: 'Increment', amount: amount} + object[key] = { __op: 'Increment', amount: amount }; } else { object[key].amount += amount; } return object[key]; -} +}; export function flatten(array) { var flattened = []; - for(var i = 0; i < array.length; i++) { - if(Array.isArray(array[i])) { + for (var i = 0; i < array.length; i++) { + if (Array.isArray(array[i])) { flattened = flattened.concat(flatten(array[i])); } else { flattened.push(array[i]); @@ -48,8 +48,8 @@ function statusHandler(className, database) { return Object.freeze({ create, - update - }) + update, + }); } function restStatusHandler(className, config) { @@ -57,7 +57,8 @@ function restStatusHandler(className, config) { const auth = Auth.master(config); function create(object) { lastPromise = lastPromise.then(() => { - return rest.create(config, auth, className, object) + return rest + .create(config, auth, className, object) .then(({ response }) => { // merge the objects return Promise.resolve(Object.assign({}, object, response)); @@ -69,7 +70,8 @@ function restStatusHandler(className, config) { function update(where, object) { // TODO: when we have updateWhere, use that for proper interfacing lastPromise = lastPromise.then(() => { - return rest.update(config, auth, className, { objectId: where.objectId }, object) + return rest + .update(config, auth, className, { objectId: where.objectId }, object) .then(({ response }) => { // merge the objects return Promise.resolve(Object.assign({}, object, response)); @@ -80,8 +82,8 @@ function restStatusHandler(className, config) { return Object.freeze({ create, - update - }) + update, + }); } export function jobStatusHandler(config) { @@ -99,26 +101,26 @@ export function jobStatusHandler(config) { source: 'api', createdAt: now, // lockdown! - ACL: {} - } + ACL: {}, + }; return handler.create(jobStatus); - } + }; const setMessage = function(message) { if (!message || typeof message !== 'string') { return Promise.resolve(); } return handler.update({ objectId }, { message }); - } + }; const setSucceeded = function(message) { return setFinalStatus('succeeded', message); - } + }; const setFailed = function(message) { return setFinalStatus('failed', message); - } + }; const setFinalStatus = function(status, message = undefined) { const finishedAt = new Date(); @@ -127,23 +129,22 @@ export function jobStatusHandler(config) { update.message = message; } return handler.update({ objectId }, update); - } + }; return Object.freeze({ setRunning, setSucceeded, setMessage, - setFailed + setFailed, }); } export function pushStatusHandler(config, existingObjectId) { - let pushStatus; const database = config.database; const handler = restStatusHandler(PUSH_STATUS_COLLECTION, config); let objectId = existingObjectId; - const setInitial = function(body = {}, where, options = {source: 'rest'}) { + const setInitial = function(body = {}, where, options = { source: 'rest' }) { const now = new Date(); let pushTime = now.toISOString(); let status = 'pending'; @@ -152,12 +153,14 @@ export function pushStatusHandler(config, existingObjectId) { pushTime = body.push_time; status = 'scheduled'; } else { - logger.warn('Trying to schedule a push while server is not configured.'); + logger.warn( + 'Trying to schedule a push while server is not configured.' + ); logger.warn('Push will be sent immediately'); } } - const data = body.data || {}; + const data = body.data || {}; const payloadString = JSON.stringify(data); let pushHash; if (typeof data.alert === 'string') { @@ -179,35 +182,43 @@ export function pushStatusHandler(config, existingObjectId) { numSent: 0, pushHash, // lockdown! - ACL: {} - } - return handler.create(object).then((result) => { + ACL: {}, + }; + return handler.create(object).then(result => { objectId = result.objectId; pushStatus = { - objectId + objectId, }; return Promise.resolve(pushStatus); }); - } + }; const setRunning = function(batches) { - logger.verbose(`_PushStatus ${objectId}: sending push to installations with %d batches`, batches); + logger.verbose( + `_PushStatus ${objectId}: sending push to installations with %d batches`, + batches + ); return handler.update( { - status:"pending", - objectId: objectId + status: 'pending', + objectId: objectId, }, { - status: "running", - count: batches + status: 'running', + count: batches, } ); - } + }; - const trackSent = function(results, UTCOffset, cleanupInstallations = process.env.PARSE_SERVER_CLEANUP_INVALID_INSTALLATIONS) { + const trackSent = function( + results, + UTCOffset, + cleanupInstallations = process.env + .PARSE_SERVER_CLEANUP_INVALID_INSTALLATIONS + ) { const update = { numSent: 0, - numFailed: 0 + numFailed: 0, }; const devicesToRemove = []; if (Array.isArray(results)) { @@ -218,16 +229,26 @@ export function pushStatusHandler(config, existingObjectId) { return memo; } const deviceType = result.device.deviceType; - const key = result.transmitted ? `sentPerType.${deviceType}` : `failedPerType.${deviceType}`; + const key = result.transmitted + ? `sentPerType.${deviceType}` + : `failedPerType.${deviceType}`; memo[key] = incrementOp(memo, key); if (typeof UTCOffset !== 'undefined') { - const offsetKey = result.transmitted ? `sentPerUTCOffset.${UTCOffset}` : `failedPerUTCOffset.${UTCOffset}`; + const offsetKey = result.transmitted + ? `sentPerUTCOffset.${UTCOffset}` + : `failedPerUTCOffset.${UTCOffset}`; memo[offsetKey] = incrementOp(memo, offsetKey); } if (result.transmitted) { memo.numSent++; } else { - if (result && result.response && result.response.error && result.device && result.device.deviceToken) { + if ( + result && + result.response && + result.response.error && + result.device && + result.device.deviceToken + ) { const token = result.device.deviceToken; const error = result.response.error; // GCM errors @@ -245,13 +266,19 @@ export function pushStatusHandler(config, existingObjectId) { }, update); } - logger.verbose(`_PushStatus ${objectId}: sent push! %d success, %d failures`, update.numSent, update.numFailed); - logger.verbose(`_PushStatus ${objectId}: needs cleanup`, { devicesToRemove }); - ['numSent', 'numFailed'].forEach((key) => { + logger.verbose( + `_PushStatus ${objectId}: sent push! %d success, %d failures`, + update.numSent, + update.numFailed + ); + logger.verbose(`_PushStatus ${objectId}: needs cleanup`, { + devicesToRemove, + }); + ['numSent', 'numFailed'].forEach(key => { if (update[key] > 0) { update[key] = { __op: 'Increment', - amount: update[key] + amount: update[key], }; } else { delete update[key]; @@ -259,29 +286,39 @@ export function pushStatusHandler(config, existingObjectId) { }); if (devicesToRemove.length > 0 && cleanupInstallations) { - logger.info(`Removing device tokens on ${devicesToRemove.length} _Installations`); - database.update('_Installation', { deviceToken: { '$in': devicesToRemove }}, { deviceToken: {"__op": "Delete"} }, { - acl: undefined, - many: true - }); + logger.info( + `Removing device tokens on ${devicesToRemove.length} _Installations` + ); + database.update( + '_Installation', + { deviceToken: { $in: devicesToRemove } }, + { deviceToken: { __op: 'Delete' } }, + { + acl: undefined, + many: true, + } + ); } // indicate this batch is complete incrementOp(update, 'count', -1); - return handler.update({ objectId }, update).then((res) => { + return handler.update({ objectId }, update).then(res => { if (res && res.count === 0) { return this.complete(); } - }) - } + }); + }; const complete = function() { - return handler.update({ objectId }, { - status: 'succeeded', - count: {__op: 'Delete'} - }); - } + return handler.update( + { objectId }, + { + status: 'succeeded', + count: { __op: 'Delete' }, + } + ); + }; const fail = function(err) { if (typeof err === 'string') { @@ -289,22 +326,22 @@ export function pushStatusHandler(config, existingObjectId) { } const update = { errorMessage: err, - status: 'failed' - } + status: 'failed', + }; return handler.update({ objectId }, update); - } + }; const rval = { setInitial, setRunning, trackSent, complete, - fail + fail, }; // define objectId to be dynamic - Object.defineProperty(rval, "objectId", { - get: () => objectId + Object.defineProperty(rval, 'objectId', { + get: () => objectId, }); return Object.freeze(rval); diff --git a/src/TestUtils.js b/src/TestUtils.js index ef2fdee81e..7772bcd65b 100644 --- a/src/TestUtils.js +++ b/src/TestUtils.js @@ -8,12 +8,14 @@ export function destroyAllDataPermanently(fast) { if (!process.env.TESTING) { throw 'Only supported in test environment'; } - return Promise.all(Object.keys(AppCache.cache).map(appId => { - const app = AppCache.get(appId); - if (app.databaseController) { - return app.databaseController.deleteEverything(fast); - } else { - return Promise.resolve(); - } - })); + return Promise.all( + Object.keys(AppCache.cache).map(appId => { + const app = AppCache.get(appId); + if (app.databaseController) { + return app.databaseController.deleteEverything(fast); + } else { + return Promise.resolve(); + } + }) + ); } diff --git a/src/batch.js b/src/batch.js index 9f16a93917..0a584d1dae 100644 --- a/src/batch.js +++ b/src/batch.js @@ -6,14 +6,14 @@ const batchPath = '/batch'; // Mounts a batch-handler onto a PromiseRouter. function mountOnto(router) { - router.route('POST', batchPath, (req) => { + router.route('POST', batchPath, req => { return handleBatch(router, req); }); } function parseURL(URL) { if (typeof URL === 'string') { - return url.parse(URL) + return url.parse(URL); } return undefined; } @@ -30,13 +30,13 @@ function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) { if (requestPath.slice(0, apiPrefix.length) != apiPrefix) { throw new Parse.Error( Parse.Error.INVALID_JSON, - 'cannot route batch path ' + requestPath); + 'cannot route batch path ' + requestPath + ); } return path.posix.join('/', requestPath.slice(apiPrefix.length)); - } + }; - if (serverURL && publicServerURL - && (serverURL.path != publicServerURL.path)) { + if (serverURL && publicServerURL && serverURL.path != publicServerURL.path) { const localPath = serverURL.path; const publicPath = publicServerURL.path; // Override the api prefix @@ -44,10 +44,15 @@ function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) { return function(requestPath) { // Build the new path by removing the public path // and joining with the local path - const newPath = path.posix.join('/', localPath, '/' , requestPath.slice(publicPath.length)); + const newPath = path.posix.join( + '/', + localPath, + '/', + requestPath.slice(publicPath.length) + ); // Use the method for local routing return makeRoutablePath(newPath); - } + }; } return makeRoutablePath; @@ -57,8 +62,10 @@ function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) { // TODO: pass along auth correctly function handleBatch(router, req) { if (!Array.isArray(req.body.requests)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'requests must be an array'); + throw new Parse.Error( + Parse.Error.INVALID_JSON, + 'requests must be an array' + ); } // The batch paths are all from the root of our domain. @@ -70,31 +77,40 @@ function handleBatch(router, req) { throw 'internal routing problem - expected url to end with batch'; } - const makeRoutablePath = makeBatchRoutingPathFunction(req.originalUrl, req.config.serverURL, req.config.publicServerURL); + const makeRoutablePath = makeBatchRoutingPathFunction( + req.originalUrl, + req.config.serverURL, + req.config.publicServerURL + ); - const promises = req.body.requests.map((restRequest) => { + const promises = req.body.requests.map(restRequest => { const routablePath = makeRoutablePath(restRequest.path); // Construct a request that we can send to a handler const request = { body: restRequest.body, config: req.config, auth: req.auth, - info: req.info + info: req.info, }; - return router.tryRouteRequest(restRequest.method, routablePath, request).then((response) => { - return {success: response.response}; - }, (error) => { - return {error: {code: error.code, error: error.message}}; - }); + return router + .tryRouteRequest(restRequest.method, routablePath, request) + .then( + response => { + return { success: response.response }; + }, + error => { + return { error: { code: error.code, error: error.message } }; + } + ); }); - return Promise.all(promises).then((results) => { - return {response: results}; + return Promise.all(promises).then(results => { + return { response: results }; }); } module.exports = { mountOnto, - makeBatchRoutingPathFunction + makeBatchRoutingPathFunction, }; diff --git a/src/cache.js b/src/cache.js index 96b00b4534..71c01f4a14 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,4 +1,4 @@ -import {InMemoryCache} from './Adapters/Cache/InMemoryCache'; +import { InMemoryCache } from './Adapters/Cache/InMemoryCache'; -export var AppCache = new InMemoryCache({ttl: NaN}); +export var AppCache = new InMemoryCache({ ttl: NaN }); export default AppCache; diff --git a/src/cli/definitions/parse-live-query-server.js b/src/cli/definitions/parse-live-query-server.js index 0fd2fca6c9..3b4ef432dd 100644 --- a/src/cli/definitions/parse-live-query-server.js +++ b/src/cli/definitions/parse-live-query-server.js @@ -1,2 +1,3 @@ -const LiveQueryServerOptions = require('../../Options/Definitions').LiveQueryServerOptions; +const LiveQueryServerOptions = require('../../Options/Definitions') + .LiveQueryServerOptions; export default LiveQueryServerOptions; diff --git a/src/cli/definitions/parse-server.js b/src/cli/definitions/parse-server.js index d19dcc5d8a..33ddc62c17 100644 --- a/src/cli/definitions/parse-server.js +++ b/src/cli/definitions/parse-server.js @@ -1,2 +1,3 @@ -const ParseServerDefinitions = require('../../Options/Definitions').ParseServerOptions; +const ParseServerDefinitions = require('../../Options/Definitions') + .ParseServerOptions; export default ParseServerDefinitions; diff --git a/src/cli/parse-live-query-server.js b/src/cli/parse-live-query-server.js index c9b00a0c77..edcdeec77f 100644 --- a/src/cli/parse-live-query-server.js +++ b/src/cli/parse-live-query-server.js @@ -7,5 +7,5 @@ runner({ start: function(program, options, logOptions) { logOptions(); ParseServer.createLiveQueryServer(undefined, options); - } -}) + }, +}); diff --git a/src/cli/parse-server.js b/src/cli/parse-server.js index 9bb5f6ce15..9dcab74eb1 100755 --- a/src/cli/parse-server.js +++ b/src/cli/parse-server.js @@ -5,7 +5,7 @@ import cluster from 'cluster'; import os from 'os'; import runner from './utils/runner'; -const help = function(){ +const help = function() { console.log(' Get Started guide:'); console.log(''); console.log(' Please have a look at the get started guide!'); @@ -15,15 +15,23 @@ const help = function(){ console.log(' Usage with npm start'); console.log(''); console.log(' $ npm start -- path/to/config.json'); - console.log(' $ npm start -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL'); - console.log(' $ npm start -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL'); + console.log( + ' $ npm start -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL' + ); + console.log( + ' $ npm start -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL' + ); console.log(''); console.log(''); console.log(' Usage:'); console.log(''); console.log(' $ parse-server path/to/config.json'); - console.log(' $ parse-server -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL'); - console.log(' $ parse-server -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL'); + console.log( + ' $ parse-server -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL' + ); + console.log( + ' $ parse-server -- --appId APP_ID --masterKey MASTER_KEY --serverURL serverURL' + ); console.log(''); }; @@ -34,47 +42,58 @@ runner({ start: function(program, options, logOptions) { if (!options.appId || !options.masterKey) { program.outputHelp(); - console.error(""); - console.error('\u001b[31mERROR: appId and masterKey are required\u001b[0m'); - console.error(""); + console.error(''); + console.error( + '\u001b[31mERROR: appId and masterKey are required\u001b[0m' + ); + console.error(''); process.exit(1); } - if (options["liveQuery.classNames"]) { + if (options['liveQuery.classNames']) { options.liveQuery = options.liveQuery || {}; - options.liveQuery.classNames = options["liveQuery.classNames"]; - delete options["liveQuery.classNames"]; + options.liveQuery.classNames = options['liveQuery.classNames']; + delete options['liveQuery.classNames']; } - if (options["liveQuery.redisURL"]) { + if (options['liveQuery.redisURL']) { options.liveQuery = options.liveQuery || {}; - options.liveQuery.redisURL = options["liveQuery.redisURL"]; - delete options["liveQuery.redisURL"]; + options.liveQuery.redisURL = options['liveQuery.redisURL']; + delete options['liveQuery.redisURL']; } if (options.cluster) { - const numCPUs = typeof options.cluster === 'number' ? options.cluster : os.cpus().length; + const numCPUs = + typeof options.cluster === 'number' + ? options.cluster + : os.cpus().length; if (cluster.isMaster) { logOptions(); - for(let i = 0; i < numCPUs; i++) { + for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code) => { - console.log(`worker ${worker.process.pid} died (${code})... Restarting`); + console.log( + `worker ${worker.process.pid} died (${code})... Restarting` + ); cluster.fork(); }); } else { ParseServer.start(options, () => { - console.log('[' + process.pid + '] parse-server running on ' + options.serverURL); + console.log( + '[' + process.pid + '] parse-server running on ' + options.serverURL + ); }); } } else { ParseServer.start(options, () => { logOptions(); console.log(''); - console.log('[' + process.pid + '] parse-server running on ' + options.serverURL); + console.log( + '[' + process.pid + '] parse-server running on ' + options.serverURL + ); }); } - } + }, }); /* eslint-enable no-console */ diff --git a/src/cli/utils/commander.js b/src/cli/utils/commander.js index 8ac47804e8..5eead78c29 100644 --- a/src/cli/utils/commander.js +++ b/src/cli/utils/commander.js @@ -9,12 +9,20 @@ Command.prototype.loadDefinitions = function(definitions) { _definitions = definitions; Object.keys(definitions).reduce((program, opt) => { - if (typeof definitions[opt] == "object") { + if (typeof definitions[opt] == 'object') { const additionalOptions = definitions[opt]; if (additionalOptions.required === true) { - return program.option(`--${opt} <${opt}>`, additionalOptions.help, additionalOptions.action); + return program.option( + `--${opt} <${opt}>`, + additionalOptions.help, + additionalOptions.action + ); } else { - return program.option(`--${opt} [${opt}]`, additionalOptions.help, additionalOptions.action); + return program.option( + `--${opt} [${opt}]`, + additionalOptions.help, + additionalOptions.action + ); } } return program.option(`--${opt} [${opt}]`); @@ -22,7 +30,7 @@ Command.prototype.loadDefinitions = function(definitions) { _reverseDefinitions = Object.keys(definitions).reduce((object, key) => { let value = definitions[key]; - if (typeof value == "object") { + if (typeof value == 'object') { value = value.env; } if (value) { @@ -32,17 +40,17 @@ Command.prototype.loadDefinitions = function(definitions) { }, {}); _defaults = Object.keys(definitions).reduce((defs, opt) => { - if(_definitions[opt].default) { + if (_definitions[opt].default) { defs[opt] = _definitions[opt].default; } return defs; }, {}); /* istanbul ignore next */ - this.on('--help', function(){ + this.on('--help', function() { console.log(' Configure From Environment:'); console.log(''); - Object.keys(_reverseDefinitions).forEach((key) => { + Object.keys(_reverseDefinitions).forEach(key => { console.log(` $ ${key}='${_reverseDefinitions[key]}'`); }); console.log(''); @@ -53,8 +61,8 @@ function parseEnvironment(env = {}) { return Object.keys(_reverseDefinitions).reduce((options, key) => { if (env[key]) { const originalKey = _reverseDefinitions[key]; - let action = (option) => (option); - if (typeof _definitions[originalKey] === "object") { + let action = option => option; + if (typeof _definitions[originalKey] === 'object') { action = _definitions[originalKey].action || action; } options[_reverseDefinitions[key]] = action(env[key]); @@ -77,7 +85,7 @@ function parseConfigFile(program) { } else { options = jsonConfig; } - Object.keys(options).forEach((key) => { + Object.keys(options).forEach(key => { const value = options[key]; if (!_definitions[key]) { throw `error: unknown option ${key}`; @@ -87,13 +95,13 @@ function parseConfigFile(program) { options[key] = action(value); } }); - console.log(`Configuration loaded from ${jsonPath}`) + console.log(`Configuration loaded from ${jsonPath}`); } return options; } Command.prototype.setValuesIfNeeded = function(options) { - Object.keys(options).forEach((key) => { + Object.keys(options).forEach(key => { if (!this.hasOwnProperty(key)) { this[key] = options[key]; } diff --git a/src/cli/utils/runner.js b/src/cli/utils/runner.js index 1c0c0af302..1f1a7c1b56 100644 --- a/src/cli/utils/runner.js +++ b/src/cli/utils/runner.js @@ -1,16 +1,15 @@ - import program from './commander'; function logStartupOptions(options) { for (const key in options) { let value = options[key]; - if (key == "masterKey") { - value = "***REDACTED***"; + if (key == 'masterKey') { + value = '***REDACTED***'; } if (typeof value === 'object') { try { - value = JSON.stringify(value) - } catch(e) { + value = JSON.stringify(value); + } catch (e) { if (value && value.constructor && value.constructor.name) { value = value.constructor.name; } @@ -22,12 +21,7 @@ function logStartupOptions(options) { } } -export default function({ - definitions, - help, - usage, - start -}) { +export default function({ definitions, help, usage, start }) { program.loadDefinitions(definitions); if (usage) { program.usage(usage); diff --git a/src/cloud-code/HTTPResponse.js b/src/cloud-code/HTTPResponse.js index aadabb1d13..1c1de93ae2 100644 --- a/src/cloud-code/HTTPResponse.js +++ b/src/cloud-code/HTTPResponse.js @@ -12,7 +12,7 @@ export default class HTTPResponse { let _text, _data; this.status = response.statusCode; this.headers = response.headers || {}; - this.cookies = this.headers["set-cookie"]; + this.cookies = this.headers['set-cookie']; if (typeof body == 'string') { _text = body; @@ -29,29 +29,33 @@ export default class HTTPResponse { _text = JSON.stringify(_data); } return _text; - } + }; const getData = () => { if (!_data) { try { _data = JSON.parse(getText()); - } catch (e) { /* */ } + } catch (e) { + /* */ + } } return _data; - } + }; Object.defineProperty(this, 'body', { - get: () => { return body } + get: () => { + return body; + }, }); Object.defineProperty(this, 'text', { enumerable: true, - get: getText + get: getText, }); Object.defineProperty(this, 'data', { enumerable: true, - get: getData + get: getData, }); } } diff --git a/src/cloud-code/Parse.Cloud.js b/src/cloud-code/Parse.Cloud.js index 142929d7d6..a24a2ab2f4 100644 --- a/src/cloud-code/Parse.Cloud.js +++ b/src/cloud-code/Parse.Cloud.js @@ -1,4 +1,4 @@ -import { Parse } from 'parse/node'; +import { Parse } from 'parse/node'; import * as triggers from '../triggers'; function getClassName(parseClass) { @@ -32,7 +32,12 @@ var ParseCloud = {}; * @param {Function} data The Cloud Function to register. This function can be an async function and should take one parameter a {@link Parse.Cloud.FunctionRequest}. */ ParseCloud.define = function(functionName, handler, validationHandler) { - triggers.addFunction(functionName, handler, validationHandler, Parse.applicationId); + triggers.addFunction( + functionName, + handler, + validationHandler, + Parse.applicationId + ); }; /** @@ -75,7 +80,12 @@ ParseCloud.job = function(functionName, handler) { */ ParseCloud.beforeSave = function(parseClass, handler) { var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.beforeSave, className, handler, Parse.applicationId); + triggers.addTrigger( + triggers.Types.beforeSave, + className, + handler, + Parse.applicationId + ); }; /** @@ -101,7 +111,12 @@ ParseCloud.beforeSave = function(parseClass, handler) { */ ParseCloud.beforeDelete = function(parseClass, handler) { var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.beforeDelete, className, handler, Parse.applicationId); + triggers.addTrigger( + triggers.Types.beforeDelete, + className, + handler, + Parse.applicationId + ); }; /** @@ -128,7 +143,12 @@ ParseCloud.beforeDelete = function(parseClass, handler) { */ ParseCloud.afterSave = function(parseClass, handler) { var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.afterSave, className, handler, Parse.applicationId); + triggers.addTrigger( + triggers.Types.afterSave, + className, + handler, + Parse.applicationId + ); }; /** @@ -154,7 +174,12 @@ ParseCloud.afterSave = function(parseClass, handler) { */ ParseCloud.afterDelete = function(parseClass, handler) { var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.afterDelete, className, handler, Parse.applicationId); + triggers.addTrigger( + triggers.Types.afterDelete, + className, + handler, + Parse.applicationId + ); }; /** @@ -180,7 +205,12 @@ ParseCloud.afterDelete = function(parseClass, handler) { */ ParseCloud.beforeFind = function(parseClass, handler) { var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.beforeFind, className, handler, Parse.applicationId); + triggers.addTrigger( + triggers.Types.beforeFind, + className, + handler, + Parse.applicationId + ); }; /** @@ -206,7 +236,12 @@ ParseCloud.beforeFind = function(parseClass, handler) { */ ParseCloud.afterFind = function(parseClass, handler) { const className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.afterFind, className, handler, Parse.applicationId); + triggers.addTrigger( + triggers.Types.afterFind, + className, + handler, + Parse.applicationId + ); }; ParseCloud.onLiveQueryEvent = function(handler) { @@ -215,14 +250,16 @@ ParseCloud.onLiveQueryEvent = function(handler) { ParseCloud._removeAllHooks = () => { triggers._unregisterAll(); -} +}; ParseCloud.useMasterKey = () => { // eslint-disable-next-line - console.warn("Parse.Cloud.useMasterKey is deprecated (and has no effect anymore) on parse-server, please refer to the cloud code migration notes: http://docs.parseplatform.org/parse-server/guide/#master-key-must-be-passed-explicitly") -} + console.warn( + 'Parse.Cloud.useMasterKey is deprecated (and has no effect anymore) on parse-server, please refer to the cloud code migration notes: http://docs.parseplatform.org/parse-server/guide/#master-key-must-be-passed-explicitly' + ); +}; -ParseCloud.httpRequest = require("./httpRequest"); +ParseCloud.httpRequest = require('./httpRequest'); module.exports = ParseCloud; diff --git a/src/cloud-code/httpRequest.js b/src/cloud-code/httpRequest.js index 35bfc3fe38..bc589d8c9a 100644 --- a/src/cloud-code/httpRequest.js +++ b/src/cloud-code/httpRequest.js @@ -3,11 +3,11 @@ import HTTPResponse from './HTTPResponse'; import querystring from 'querystring'; import log from '../logger'; -var encodeBody = function({body, headers = {}}) { +var encodeBody = function({ body, headers = {} }) { if (typeof body !== 'object') { - return {body, headers}; + return { body, headers }; } - var contentTypeKeys = Object.keys(headers).filter((key) => { + var contentTypeKeys = Object.keys(headers).filter(key => { return key.match(/content-type/i) != null; }); @@ -20,18 +20,23 @@ var encodeBody = function({body, headers = {}}) { } else { /* istanbul ignore next */ if (contentTypeKeys.length > 1) { - log.error('Parse.Cloud.httpRequest', 'multiple content-type headers are set.'); + log.error( + 'Parse.Cloud.httpRequest', + 'multiple content-type headers are set.' + ); } // There maybe many, we'll just take the 1st one var contentType = contentTypeKeys[0]; if (headers[contentType].match(/application\/json/i)) { body = JSON.stringify(body); - } else if(headers[contentType].match(/application\/x-www-form-urlencoded/i)) { + } else if ( + headers[contentType].match(/application\/x-www-form-urlencoded/i) + ) { body = querystring.stringify(body); } } - return {body, headers}; -} + return { body, headers }; +}; /** * Makes an HTTP Request. @@ -61,12 +66,12 @@ var encodeBody = function({body, headers = {}}) { module.exports = function(options) { var callbacks = { success: options.success, - error: options.error + error: options.error, }; delete options.success; delete options.error; delete options.uri; // not supported - options = Object.assign(options, encodeBody(options)); + options = Object.assign(options, encodeBody(options)); // set follow redirects to false by default options.followRedirect = options.followRedirects == true; // support params options diff --git a/src/cryptoUtils.js b/src/cryptoUtils.js index c90e74cae4..580b843ad6 100644 --- a/src/cryptoUtils.js +++ b/src/cryptoUtils.js @@ -8,7 +8,7 @@ export function randomHexString(size: number): string { throw new Error('Zero-length randomHexString is useless.'); } if (size % 2 !== 0) { - throw new Error('randomHexString size must be divisible by 2.') + throw new Error('randomHexString size must be divisible by 2.'); } return randomBytes(size / 2).toString('hex'); } @@ -23,9 +23,8 @@ export function randomString(size: number): string { if (size === 0) { throw new Error('Zero-length randomString is useless.'); } - const chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + - 'abcdefghijklmnopqrstuvwxyz' + - '0123456789'); + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789'; let objectId = ''; const bytes = randomBytes(size); for (let i = 0; i < bytes.length; ++i) { @@ -45,5 +44,7 @@ export function newToken(): string { } export function md5Hash(string: string): string { - return createHash('md5').update(string).digest('hex'); + return createHash('md5') + .update(string) + .digest('hex'); } diff --git a/src/defaults.js b/src/defaults.js index be205f4e5b..3c9d01fb95 100644 --- a/src/defaults.js +++ b/src/defaults.js @@ -3,7 +3,7 @@ const { ParseServerOptions } = require('./Options/Definitions'); const logsFolder = (() => { let folder = './logs/'; if (typeof process !== 'undefined' && process.env.TESTING === '1') { - folder = './test_logs/' + folder = './test_logs/'; } if (process.env.PARSE_SERVER_LOGS_FOLDER) { folder = nullParser(process.env.PARSE_SERVER_LOGS_FOLDER); @@ -13,24 +13,26 @@ const logsFolder = (() => { const { verbose, level } = (() => { const verbose = process.env.VERBOSE ? true : false; - return { verbose, level: verbose ? 'verbose' : undefined } + return { verbose, level: verbose ? 'verbose' : undefined }; })(); - -const DefinitionDefaults = Object.keys(ParseServerOptions).reduce((memo, key) => { - const def = ParseServerOptions[key]; - if (def.hasOwnProperty('default')) { - memo[key] = def.default; - } - return memo; -}, {}); +const DefinitionDefaults = Object.keys(ParseServerOptions).reduce( + (memo, key) => { + const def = ParseServerOptions[key]; + if (def.hasOwnProperty('default')) { + memo[key] = def.default; + } + return memo; + }, + {} +); const computedDefaults = { jsonLogs: process.env.JSON_LOGS || false, logsFolder, verbose, level, -} +}; export default Object.assign({}, DefinitionDefaults, computedDefaults); export const DefaultMongoURI = DefinitionDefaults.databaseURI; diff --git a/src/deprecated.js b/src/deprecated.js index 7192336c2c..e92c621207 100644 --- a/src/deprecated.js +++ b/src/deprecated.js @@ -1,5 +1,5 @@ export function useExternal(name, moduleName) { return function() { throw `${name} is not provided by parse-server anymore; please install ${moduleName}`; - } + }; } diff --git a/src/index.js b/src/index.js index 0cceb23489..5082eb3a26 100644 --- a/src/index.js +++ b/src/index.js @@ -1,21 +1,21 @@ -import ParseServer from './ParseServer'; -import S3Adapter from '@parse/s3-files-adapter' -import FileSystemAdapter from '@parse/fs-files-adapter' -import InMemoryCacheAdapter from './Adapters/Cache/InMemoryCacheAdapter' -import NullCacheAdapter from './Adapters/Cache/NullCacheAdapter' -import RedisCacheAdapter from './Adapters/Cache/RedisCacheAdapter' -import LRUCacheAdapter from './Adapters/Cache/LRUCache.js' -import * as TestUtils from './TestUtils'; -import { useExternal } from './deprecated'; -import { getLogger } from './logger'; -import { PushWorker } from './Push/PushWorker'; -import { ParseServerOptions } from './Options'; +import ParseServer from './ParseServer'; +import S3Adapter from '@parse/s3-files-adapter'; +import FileSystemAdapter from '@parse/fs-files-adapter'; +import InMemoryCacheAdapter from './Adapters/Cache/InMemoryCacheAdapter'; +import NullCacheAdapter from './Adapters/Cache/NullCacheAdapter'; +import RedisCacheAdapter from './Adapters/Cache/RedisCacheAdapter'; +import LRUCacheAdapter from './Adapters/Cache/LRUCache.js'; +import * as TestUtils from './TestUtils'; +import { useExternal } from './deprecated'; +import { getLogger } from './logger'; +import { PushWorker } from './Push/PushWorker'; +import { ParseServerOptions } from './Options'; // Factory function const _ParseServer = function(options: ParseServerOptions) { const server = new ParseServer(options); return server.app; -} +}; // Mount the create liveQueryServer _ParseServer.createLiveQueryServer = ParseServer.createLiveQueryServer; _ParseServer.start = ParseServer.start; @@ -23,7 +23,7 @@ _ParseServer.start = ParseServer.start; const GCSAdapter = useExternal('GCSAdapter', '@parse/gcs-files-adapter'); Object.defineProperty(module.exports, 'logger', { - get: getLogger + get: getLogger, }); export default ParseServer; @@ -37,5 +37,5 @@ export { LRUCacheAdapter, TestUtils, PushWorker, - _ParseServer as ParseServer + _ParseServer as ParseServer, }; diff --git a/src/logger.js b/src/logger.js index ee69c42cf0..e32ad0a8a8 100644 --- a/src/logger.js +++ b/src/logger.js @@ -1,14 +1,15 @@ 'use strict'; import defaults from './defaults'; import { WinstonLoggerAdapter } from './Adapters/Logger/WinstonLoggerAdapter'; -import { LoggerController } from './Controllers/LoggerController'; +import { LoggerController } from './Controllers/LoggerController'; function defaultLogger() { const options = { logsFolder: defaults.logsFolder, jsonLogs: defaults.jsonLogs, verbose: defaults.verbose, - silent: defaults.silent }; + silent: defaults.silent, + }; const adapter = new WinstonLoggerAdapter(options); return new LoggerController(adapter, null, options); } @@ -25,10 +26,10 @@ export function getLogger() { // for: `import logger from './logger'` Object.defineProperty(module.exports, 'default', { - get: getLogger + get: getLogger, }); // for: `import { logger } from './logger'` Object.defineProperty(module.exports, 'logger', { - get: getLogger + get: getLogger, }); diff --git a/src/middlewares.js b/src/middlewares.js index b85a7368be..d7c8cb41a3 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -25,7 +25,7 @@ export function handleParseHeaders(req, res, next) { javascriptKey: req.get('X-Parse-Javascript-Key'), dotNetKey: req.get('X-Parse-Windows-Key'), restAPIKey: req.get('X-Parse-REST-API-Key'), - clientVersion: req.get('X-Parse-Client-Version') + clientVersion: req.get('X-Parse-Client-Version'), }; var basicAuth = httpAuth(req); @@ -60,10 +60,12 @@ export function handleParseHeaders(req, res, next) { delete req.body._RevocableSession; } - if (req.body && + if ( + req.body && req.body._ApplicationId && AppCache.get(req.body._ApplicationId) && - (!info.masterKey || AppCache.get(req.body._ApplicationId).masterKey === info.masterKey) + (!info.masterKey || + AppCache.get(req.body._ApplicationId).masterKey === info.masterKey) ) { info.appId = req.body._ApplicationId; info.javascriptKey = req.body._JavaScriptKey || ''; @@ -114,32 +116,50 @@ export function handleParseHeaders(req, res, next) { req.config.ip = clientIp; req.info = info; - if (info.masterKey && req.config.masterKeyIps && req.config.masterKeyIps.length !== 0 && req.config.masterKeyIps.indexOf(clientIp) === -1) { + if ( + info.masterKey && + req.config.masterKeyIps && + req.config.masterKeyIps.length !== 0 && + req.config.masterKeyIps.indexOf(clientIp) === -1 + ) { return invalidRequest(req, res); } - var isMaster = (info.masterKey === req.config.masterKey); + var isMaster = info.masterKey === req.config.masterKey; if (isMaster) { - req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: true }); + req.auth = new auth.Auth({ + config: req.config, + installationId: info.installationId, + isMaster: true, + }); next(); return; } - var isReadOnlyMaster = (info.masterKey === req.config.readOnlyMasterKey); - if (typeof req.config.readOnlyMasterKey != 'undefined' && req.config.readOnlyMasterKey && isReadOnlyMaster) { - req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: true, isReadOnly: true }); + var isReadOnlyMaster = info.masterKey === req.config.readOnlyMasterKey; + if ( + typeof req.config.readOnlyMasterKey != 'undefined' && + req.config.readOnlyMasterKey && + isReadOnlyMaster + ) { + req.auth = new auth.Auth({ + config: req.config, + installationId: info.installationId, + isMaster: true, + isReadOnly: true, + }); next(); return; } // Client keys are not required in parse-server, but if any have been configured in the server, validate them // to preserve original behavior. - const keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"]; + const keys = ['clientKey', 'javascriptKey', 'dotNetKey', 'restAPIKey']; const oneKeyConfigured = keys.some(function(key) { return req.config[key] !== undefined; }); - const oneKeyMatches = keys.some(function(key){ + const oneKeyMatches = keys.some(function(key) { return req.config[key] !== undefined && info[key] === req.config[key]; }); @@ -147,45 +167,63 @@ export function handleParseHeaders(req, res, next) { return invalidRequest(req, res); } - if (req.url == "/login") { + if (req.url == '/login') { delete info.sessionToken; } if (!info.sessionToken) { - req.auth = new auth.Auth({ config: req.config, installationId: info.installationId, isMaster: false }); + req.auth = new auth.Auth({ + config: req.config, + installationId: info.installationId, + isMaster: false, + }); next(); return; } - return Promise.resolve().then(() => { - // handle the upgradeToRevocableSession path on it's own - if (info.sessionToken && + return Promise.resolve() + .then(() => { + // handle the upgradeToRevocableSession path on it's own + if ( + info.sessionToken && req.url === '/upgradeToRevocableSession' && - info.sessionToken.indexOf('r:') != 0) { - return auth.getAuthForLegacySessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }) - } else { - return auth.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }) - } - }).then((auth) => { - if (auth) { - req.auth = auth; - next(); - } - }) - .catch((error) => { - if(error instanceof Parse.Error) { + info.sessionToken.indexOf('r:') != 0 + ) { + return auth.getAuthForLegacySessionToken({ + config: req.config, + installationId: info.installationId, + sessionToken: info.sessionToken, + }); + } else { + return auth.getAuthForSessionToken({ + config: req.config, + installationId: info.installationId, + sessionToken: info.sessionToken, + }); + } + }) + .then(auth => { + if (auth) { + req.auth = auth; + next(); + } + }) + .catch(error => { + if (error instanceof Parse.Error) { next(error); return; - } - else { + } else { // TODO: Determine the correct error scenario. - req.config.loggerController.error('error getting auth for sessionToken', error); + req.config.loggerController.error( + 'error getting auth for sessionToken', + error + ); throw new Parse.Error(Parse.Error.UNKNOWN_ERROR, error); } }); } -function getClientIp(req){ +function getClientIp(req) { if (req.headers['x-forwarded-for']) { // try to get from x-forwared-for if it set (behind reverse proxy) return req.headers['x-forwarded-for'].split(',')[0]; @@ -205,8 +243,7 @@ function getClientIp(req){ } function httpAuth(req) { - if (!(req.req || req).headers.authorization) - return ; + if (!(req.req || req).headers.authorization) return; var header = (req.req || req).headers.authorization; var appId, masterKey, javascriptKey; @@ -226,33 +263,37 @@ function httpAuth(req) { var jsKeyPrefix = 'javascript-key='; - var matchKey = key.indexOf(jsKeyPrefix) + var matchKey = key.indexOf(jsKeyPrefix); if (matchKey == 0) { javascriptKey = key.substring(jsKeyPrefix.length, key.length); - } - else { + } else { masterKey = key; } } } - return {appId: appId, masterKey: masterKey, javascriptKey: javascriptKey}; + return { appId: appId, masterKey: masterKey, javascriptKey: javascriptKey }; } function decodeBase64(str) { - return new Buffer(str, 'base64').toString() + return new Buffer(str, 'base64').toString(); } export function allowCrossDomain(req, res, next) { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS'); - res.header('Access-Control-Allow-Headers', 'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, Content-Type'); - res.header('Access-Control-Expose-Headers', 'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id'); + res.header( + 'Access-Control-Allow-Headers', + 'X-Parse-Master-Key, X-Parse-REST-API-Key, X-Parse-Javascript-Key, X-Parse-Application-Id, X-Parse-Client-Version, X-Parse-Session-Token, X-Requested-With, X-Parse-Revocable-Session, Content-Type' + ); + res.header( + 'Access-Control-Expose-Headers', + 'X-Parse-Job-Status-Id, X-Parse-Push-Status-Id' + ); // intercept OPTIONS method if ('OPTIONS' == req.method) { res.sendStatus(200); - } - else { + } else { next(); } } @@ -272,14 +313,14 @@ export function handleParseErrors(err, req, res, next) { let httpStatus; // TODO: fill out this mapping switch (err.code) { - case Parse.Error.INTERNAL_SERVER_ERROR: - httpStatus = 500; - break; - case Parse.Error.OBJECT_NOT_FOUND: - httpStatus = 404; - break; - default: - httpStatus = 400; + case Parse.Error.INTERNAL_SERVER_ERROR: + httpStatus = 500; + break; + case Parse.Error.OBJECT_NOT_FOUND: + httpStatus = 404; + break; + default: + httpStatus = 400; } res.status(httpStatus); @@ -297,11 +338,10 @@ export function handleParseErrors(err, req, res, next) { res.status(500); res.json({ code: Parse.Error.INTERNAL_SERVER_ERROR, - message: 'Internal server error.' + message: 'Internal server error.', }); next(err); } - } export function enforceMasterKeyAccess(req, res, next) { @@ -317,7 +357,7 @@ export function promiseEnforceMasterKeyAccess(request) { if (!request.auth.isMaster) { const error = new Error(); error.status = 403; - error.message = "unauthorized: master key is required"; + error.message = 'unauthorized: master key is required'; throw error; } return Promise.resolve(); diff --git a/src/password.js b/src/password.js index f73a8a64c9..1e5a555eeb 100644 --- a/src/password.js +++ b/src/password.js @@ -4,7 +4,9 @@ var bcrypt = require('bcryptjs'); try { bcrypt = require('bcrypt'); -} catch(e) { /* */ } +} catch (e) { + /* */ +} // Returns a promise for a hashed password string. function hash(password) { @@ -23,5 +25,5 @@ function compare(password, hashedPassword) { module.exports = { hash: hash, - compare: compare + compare: compare, }; diff --git a/src/requiredParameter.js b/src/requiredParameter.js index f6d5dd4278..eba860dd82 100644 --- a/src/requiredParameter.js +++ b/src/requiredParameter.js @@ -1,2 +1,4 @@ /** @flow */ -export default (errorMessage: string): any => { throw errorMessage } +export default (errorMessage: string): any => { + throw errorMessage; +}; diff --git a/src/rest.js b/src/rest.js index 7ff4c852d9..e36973f8c8 100644 --- a/src/rest.js +++ b/src/rest.js @@ -14,112 +14,193 @@ var RestWrite = require('./RestWrite'); var triggers = require('./triggers'); function checkTriggers(className, config, types) { - return types.some((triggerType) => { - return triggers.getTrigger(className, triggers.Types[triggerType], config.applicationId); + return types.some(triggerType => { + return triggers.getTrigger( + className, + triggers.Types[triggerType], + config.applicationId + ); }); } function checkLiveQuery(className, config) { - return config.liveQueryController && config.liveQueryController.hasLiveQuery(className) + return ( + config.liveQueryController && + config.liveQueryController.hasLiveQuery(className) + ); } // Returns a promise for an object with optional keys 'results' and 'count'. function find(config, auth, className, restWhere, restOptions, clientSDK) { enforceRoleSecurity('find', className, auth); - return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth).then((result) => { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - const query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK); - return query.execute(); - }); + return triggers + .maybeRunQueryTrigger( + triggers.Types.beforeFind, + className, + restWhere, + restOptions, + config, + auth + ) + .then(result => { + restWhere = result.restWhere || restWhere; + restOptions = result.restOptions || restOptions; + const query = new RestQuery( + config, + auth, + className, + restWhere, + restOptions, + clientSDK + ); + return query.execute(); + }); } // get is just like find but only queries an objectId. const get = (config, auth, className, objectId, restOptions, clientSDK) => { var restWhere = { objectId }; enforceRoleSecurity('get', className, auth); - return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth, true).then((result) => { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - const query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK); - return query.execute(); - }); -} + return triggers + .maybeRunQueryTrigger( + triggers.Types.beforeFind, + className, + restWhere, + restOptions, + config, + auth, + true + ) + .then(result => { + restWhere = result.restWhere || restWhere; + restOptions = result.restOptions || restOptions; + const query = new RestQuery( + config, + auth, + className, + restWhere, + restOptions, + clientSDK + ); + return query.execute(); + }); +}; // Returns a promise that doesn't resolve to any useful value. function del(config, auth, className, objectId) { if (typeof objectId !== 'string') { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'bad objectId'); + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad objectId'); } if (className === '_User' && auth.isUnauthenticated()) { - throw new Parse.Error(Parse.Error.SESSION_MISSING, - 'Insufficient auth to delete user'); + throw new Parse.Error( + Parse.Error.SESSION_MISSING, + 'Insufficient auth to delete user' + ); } enforceRoleSecurity('delete', className, auth); var inflatedObject; - return Promise.resolve().then(() => { - const hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']); - const hasLiveQuery = checkLiveQuery(className, config); - if (hasTriggers || hasLiveQuery || className == '_Session') { - return new RestQuery(config, auth, className, { objectId }) - .forWrite() - .execute() - .then((response) => { - if (response && response.results && response.results.length) { - const firstResult = response.results[0]; - firstResult.className = className; - if (className === '_Session' && !auth.isMaster) { - if (!auth.user || firstResult.user.objectId !== auth.user.id) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Invalid session token'); + return Promise.resolve() + .then(() => { + const hasTriggers = checkTriggers(className, config, [ + 'beforeDelete', + 'afterDelete', + ]); + const hasLiveQuery = checkLiveQuery(className, config); + if (hasTriggers || hasLiveQuery || className == '_Session') { + return new RestQuery(config, auth, className, { objectId }) + .forWrite() + .execute() + .then(response => { + if (response && response.results && response.results.length) { + const firstResult = response.results[0]; + firstResult.className = className; + if (className === '_Session' && !auth.isMaster) { + if (!auth.user || firstResult.user.objectId !== auth.user.id) { + throw new Parse.Error( + Parse.Error.INVALID_SESSION_TOKEN, + 'Invalid session token' + ); + } } + var cacheAdapter = config.cacheController; + cacheAdapter.user.del(firstResult.sessionToken); + inflatedObject = Parse.Object.fromJSON(firstResult); + // Notify LiveQuery server if possible + config.liveQueryController.onAfterDelete( + inflatedObject.className, + inflatedObject + ); + return triggers.maybeRunTrigger( + triggers.Types.beforeDelete, + auth, + inflatedObject, + null, + config + ); } - var cacheAdapter = config.cacheController; - cacheAdapter.user.del(firstResult.sessionToken); - inflatedObject = Parse.Object.fromJSON(firstResult); - // Notify LiveQuery server if possible - config.liveQueryController.onAfterDelete(inflatedObject.className, inflatedObject); - return triggers.maybeRunTrigger(triggers.Types.beforeDelete, auth, inflatedObject, null, config); - } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found for delete.'); - }); - } - return Promise.resolve({}); - }).then(() => { - if (!auth.isMaster) { - return auth.getUserRoles(); - } else { - return; - } - }).then(() => { - var options = {}; - if (!auth.isMaster) { - options.acl = ['*']; - if (auth.user) { - options.acl.push(auth.user.id); - options.acl = options.acl.concat(auth.userRoles); + throw new Parse.Error( + Parse.Error.OBJECT_NOT_FOUND, + 'Object not found for delete.' + ); + }); + } + return Promise.resolve({}); + }) + .then(() => { + if (!auth.isMaster) { + return auth.getUserRoles(); + } else { + return; + } + }) + .then(() => { + var options = {}; + if (!auth.isMaster) { + options.acl = ['*']; + if (auth.user) { + options.acl.push(auth.user.id); + options.acl = options.acl.concat(auth.userRoles); + } } - } - return config.database.destroy(className, { - objectId: objectId - }, options); - }).then(() => { - return triggers.maybeRunTrigger(triggers.Types.afterDelete, auth, inflatedObject, null, config); - }).catch((error) => { - handleSessionMissingError(error, className, auth); - }); + return config.database.destroy( + className, + { + objectId: objectId, + }, + options + ); + }) + .then(() => { + return triggers.maybeRunTrigger( + triggers.Types.afterDelete, + auth, + inflatedObject, + null, + config + ); + }) + .catch(error => { + handleSessionMissingError(error, className, auth); + }); } // Returns a promise for a {response, status, location} object. function create(config, auth, className, restObject, clientSDK) { enforceRoleSecurity('create', className, auth); - var write = new RestWrite(config, auth, className, null, restObject, null, clientSDK); + var write = new RestWrite( + config, + auth, + className, + null, + restObject, + null, + clientSDK + ); return write.execute(); } @@ -129,56 +210,77 @@ function create(config, auth, className, restObject, clientSDK) { function update(config, auth, className, restWhere, restObject, clientSDK) { enforceRoleSecurity('update', className, auth); - return Promise.resolve().then(() => { - const hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']); - const hasLiveQuery = checkLiveQuery(className, config); - if (hasTriggers || hasLiveQuery) { - // Do not use find, as it runs the before finds - return new RestQuery(config, auth, className, restWhere) - .forWrite() - .execute(); - } - return Promise.resolve({}); - }).then(({ results }) => { - var originalRestObject; - if (results && results.length) { - originalRestObject = results[0]; - } - return new RestWrite(config, auth, className, restWhere, restObject, originalRestObject, clientSDK) - .execute(); - }).catch((error) => { - handleSessionMissingError(error, className, auth); - }); + return Promise.resolve() + .then(() => { + const hasTriggers = checkTriggers(className, config, [ + 'beforeSave', + 'afterSave', + ]); + const hasLiveQuery = checkLiveQuery(className, config); + if (hasTriggers || hasLiveQuery) { + // Do not use find, as it runs the before finds + return new RestQuery(config, auth, className, restWhere) + .forWrite() + .execute(); + } + return Promise.resolve({}); + }) + .then(({ results }) => { + var originalRestObject; + if (results && results.length) { + originalRestObject = results[0]; + } + return new RestWrite( + config, + auth, + className, + restWhere, + restObject, + originalRestObject, + clientSDK + ).execute(); + }) + .catch(error => { + handleSessionMissingError(error, className, auth); + }); } function handleSessionMissingError(error, className) { // If we're trying to update a user without / with bad session token - if (className === '_User' - && error.code === Parse.Error.OBJECT_NOT_FOUND) { + if (className === '_User' && error.code === Parse.Error.OBJECT_NOT_FOUND) { throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Insufficient auth.'); } throw error; } -const classesWithMasterOnlyAccess = ['_JobStatus', '_PushStatus', '_Hooks', '_GlobalConfig', '_JobSchedule']; +const classesWithMasterOnlyAccess = [ + '_JobStatus', + '_PushStatus', + '_Hooks', + '_GlobalConfig', + '_JobSchedule', +]; // Disallowing access to the _Role collection except by master key function enforceRoleSecurity(method, className, auth) { if (className === '_Installation' && !auth.isMaster) { if (method === 'delete' || method === 'find') { - const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.` + const error = `Clients aren't allowed to perform the ${method} operation on the installation collection.`; throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); } } //all volatileClasses are masterKey only - if(classesWithMasterOnlyAccess.indexOf(className) >= 0 && !auth.isMaster){ - const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.` + if (classesWithMasterOnlyAccess.indexOf(className) >= 0 && !auth.isMaster) { + const error = `Clients aren't allowed to perform the ${method} operation on the ${className} collection.`; throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); } // readOnly masterKey is not allowed - if (auth.isReadOnly && (method === 'delete' || method === 'create' || method === 'update')) { - const error = `read-only masterKey isn't allowed to perform the ${method} operation.` + if ( + auth.isReadOnly && + (method === 'delete' || method === 'create' || method === 'update') + ) { + const error = `read-only masterKey isn't allowed to perform the ${method} operation.`; throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); } } @@ -188,5 +290,5 @@ module.exports = { del, find, get, - update + update, }; diff --git a/src/triggers.js b/src/triggers.js index c8c5435e42..80c3dbfe1c 100644 --- a/src/triggers.js +++ b/src/triggers.js @@ -1,5 +1,5 @@ // triggers.js -import Parse from 'parse/node'; +import Parse from 'parse/node'; import { logger } from './logger'; export const Types = { @@ -8,7 +8,7 @@ export const Types = { beforeDelete: 'beforeDelete', afterDelete: 'afterDelete', beforeFind: 'beforeFind', - afterFind: 'afterFind' + afterFind: 'afterFind', }; const baseStore = function() { @@ -16,7 +16,7 @@ const baseStore = function() { const Functions = {}; const Jobs = {}; const LiveQuery = []; - const Triggers = Object.keys(Types).reduce(function(base, key){ + const Triggers = Object.keys(Types).reduce(function(base, key) { base[key] = {}; return base; }, {}); @@ -31,7 +31,7 @@ const baseStore = function() { }; function validateClassNameForTriggers(className, type) { - const restrictedClassNames = [ '_Session' ]; + const restrictedClassNames = ['_Session']; if (restrictedClassNames.indexOf(className) != -1) { throw `Triggers are not supported for ${className} class.`; } @@ -50,14 +50,14 @@ const Category = { Functions: 'Functions', Validators: 'Validators', Jobs: 'Jobs', - Triggers: 'Triggers' -} + Triggers: 'Triggers', +}; function getStore(category, name, applicationId) { const path = name.split('.'); path.splice(-1); // remove last component applicationId = applicationId || Parse.applicationId; - _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); + _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); let store = _triggerStore[applicationId][category]; for (const component of path) { store = store[component]; @@ -86,7 +86,12 @@ function get(category, name, applicationId) { return store[lastComponent]; } -export function addFunction(functionName, handler, validationHandler, applicationId) { +export function addFunction( + functionName, + handler, + validationHandler, + applicationId +) { add(Category.Functions, functionName, handler, applicationId); add(Category.Validators, functionName, validationHandler, applicationId); } @@ -102,7 +107,7 @@ export function addTrigger(type, className, handler, applicationId) { export function addLiveQueryEventHandler(handler, applicationId) { applicationId = applicationId || Parse.applicationId; - _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); + _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); _triggerStore[applicationId].LiveQuery.push(handler); } @@ -120,13 +125,17 @@ export function _unregisterAll() { export function getTrigger(className, triggerType, applicationId) { if (!applicationId) { - throw "Missing ApplicationID"; + throw 'Missing ApplicationID'; } return get(Category.Triggers, `${triggerType}.${className}`, applicationId); } -export function triggerExists(className: string, type: string, applicationId: string): boolean { - return (getTrigger(className, type, applicationId) != undefined); +export function triggerExists( + className: string, + type: string, + applicationId: string +): boolean { + return getTrigger(className, type, applicationId) != undefined; } export function getFunction(functionName, applicationId) { @@ -145,12 +154,18 @@ export function getJobs(applicationId) { return undefined; } - export function getValidator(functionName, applicationId) { return get(Category.Validators, functionName, applicationId); } -export function getRequestObject(triggerType, auth, parseObject, originalParseObject, config, context) { +export function getRequestObject( + triggerType, + auth, + parseObject, + originalParseObject, + config, + context +) { const request = { triggerName: triggerType, object: parseObject, @@ -184,7 +199,14 @@ export function getRequestObject(triggerType, auth, parseObject, originalParseOb return request; } -export function getRequestQueryObject(triggerType, auth, query, count, config, isGet) { +export function getRequestQueryObject( + triggerType, + auth, + query, + count, + config, + isGet +) { isGet = !!isGet; var request = { @@ -221,7 +243,7 @@ export function getResponseObject(request, resolve, reject) { return { success: function(response) { if (request.triggerName === Types.afterFind) { - if(!response){ + if (!response) { response = request.objects; } response = response.map(object => { @@ -230,8 +252,11 @@ export function getResponseObject(request, resolve, reject) { return resolve(response); } // Use the JSON response - if (response && !request.object.equals(response) - && request.triggerName === Types.beforeSave) { + if ( + response && + !request.object.equals(response) && + request.triggerName === Types.beforeSave + ) { return resolve(response); } response = {}; @@ -245,87 +270,135 @@ export function getResponseObject(request, resolve, reject) { return reject(new Parse.Error(Parse.Error.SCRIPT_FAILED, error)); } return reject(error); - } - } + }, + }; } function userIdForLog(auth) { - return (auth && auth.user) ? auth.user.id : undefined; + return auth && auth.user ? auth.user.id : undefined; } function logTriggerAfterHook(triggerType, className, input, auth) { const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); - logger.info(`${triggerType} triggered for ${className} for user ${userIdForLog(auth)}:\n Input: ${cleanInput}`, { - className, - triggerType, - user: userIdForLog(auth) - }); + logger.info( + `${triggerType} triggered for ${className} for user ${userIdForLog( + auth + )}:\n Input: ${cleanInput}`, + { + className, + triggerType, + user: userIdForLog(auth), + } + ); } -function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth) { +function logTriggerSuccessBeforeHook( + triggerType, + className, + input, + result, + auth +) { const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); const cleanResult = logger.truncateLogMessage(JSON.stringify(result)); - logger.info(`${triggerType} triggered for ${className} for user ${userIdForLog(auth)}:\n Input: ${cleanInput}\n Result: ${cleanResult}`, { - className, - triggerType, - user: userIdForLog(auth) - }); + logger.info( + `${triggerType} triggered for ${className} for user ${userIdForLog( + auth + )}:\n Input: ${cleanInput}\n Result: ${cleanResult}`, + { + className, + triggerType, + user: userIdForLog(auth), + } + ); } function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) { const cleanInput = logger.truncateLogMessage(JSON.stringify(input)); - logger.error(`${triggerType} failed for ${className} for user ${userIdForLog(auth)}:\n Input: ${cleanInput}\n Error: ${JSON.stringify(error)}`, { - className, - triggerType, - error, - user: userIdForLog(auth) - }); + logger.error( + `${triggerType} failed for ${className} for user ${userIdForLog( + auth + )}:\n Input: ${cleanInput}\n Error: ${JSON.stringify(error)}`, + { + className, + triggerType, + error, + user: userIdForLog(auth), + } + ); } -export function maybeRunAfterFindTrigger(triggerType, auth, className, objects, config) { +export function maybeRunAfterFindTrigger( + triggerType, + auth, + className, + objects, + config +) { return new Promise((resolve, reject) => { const trigger = getTrigger(className, triggerType, config.applicationId); if (!trigger) { return resolve(); } const request = getRequestObject(triggerType, auth, null, null, config); - const { success, error } = getResponseObject(request, + const { success, error } = getResponseObject( + request, object => { resolve(object); }, error => { reject(error); - }); - logTriggerSuccessBeforeHook(triggerType, className, 'AfterFind', JSON.stringify(objects), auth); + } + ); + logTriggerSuccessBeforeHook( + triggerType, + className, + 'AfterFind', + JSON.stringify(objects), + auth + ); request.objects = objects.map(object => { //setting the class name to transform into parse object object.className = className; return Parse.Object.fromJSON(object); }); - return Promise.resolve().then(() => { - const response = trigger(request); - if (response && typeof response.then === 'function') { - return response.then((results) => { - if (!results) { - throw new Parse.Error(Parse.Error.SCRIPT_FAILED, "AfterFind expect results to be returned in the promise"); - } - return results; - }); - } - return response; - }).then(success, error); - }).then((results) => { + return Promise.resolve() + .then(() => { + const response = trigger(request); + if (response && typeof response.then === 'function') { + return response.then(results => { + if (!results) { + throw new Parse.Error( + Parse.Error.SCRIPT_FAILED, + 'AfterFind expect results to be returned in the promise' + ); + } + return results; + }); + } + return response; + }) + .then(success, error); + }).then(results => { logTriggerAfterHook(triggerType, className, JSON.stringify(results), auth); return results; }); } -export function maybeRunQueryTrigger(triggerType, className, restWhere, restOptions, config, auth, isGet) { +export function maybeRunQueryTrigger( + triggerType, + className, + restWhere, + restOptions, + config, + auth, + isGet +) { const trigger = getTrigger(className, triggerType, config.applicationId); if (!trigger) { return Promise.resolve({ restWhere, - restOptions + restOptions, }); } @@ -346,61 +419,75 @@ export function maybeRunQueryTrigger(triggerType, className, restWhere, restOpti } count = !!restOptions.count; } - const requestObject = getRequestQueryObject(triggerType, auth, parseQuery, count, config, isGet); - return Promise.resolve().then(() => { - return trigger(requestObject); - }).then((result) => { - let queryResult = parseQuery; - if (result && result instanceof Parse.Query) { - queryResult = result; - } - const jsonQuery = queryResult.toJSON(); - if (jsonQuery.where) { - restWhere = jsonQuery.where; - } - if (jsonQuery.limit) { - restOptions = restOptions || {}; - restOptions.limit = jsonQuery.limit; - } - if (jsonQuery.skip) { - restOptions = restOptions || {}; - restOptions.skip = jsonQuery.skip; - } - if (jsonQuery.include) { - restOptions = restOptions || {}; - restOptions.include = jsonQuery.include; - } - if (jsonQuery.keys) { - restOptions = restOptions || {}; - restOptions.keys = jsonQuery.keys; - } - if (jsonQuery.order) { - restOptions = restOptions || {}; - restOptions.order = jsonQuery.order; - } - if (requestObject.readPreference) { - restOptions = restOptions || {}; - restOptions.readPreference = requestObject.readPreference; - } - if (requestObject.includeReadPreference) { - restOptions = restOptions || {}; - restOptions.includeReadPreference = requestObject.includeReadPreference; - } - if (requestObject.subqueryReadPreference) { - restOptions = restOptions || {}; - restOptions.subqueryReadPreference = requestObject.subqueryReadPreference; - } - return { - restWhere, - restOptions - }; - }, (err) => { - if (typeof err === 'string') { - throw new Parse.Error(1, err); - } else { - throw err; - } - }); + const requestObject = getRequestQueryObject( + triggerType, + auth, + parseQuery, + count, + config, + isGet + ); + return Promise.resolve() + .then(() => { + return trigger(requestObject); + }) + .then( + result => { + let queryResult = parseQuery; + if (result && result instanceof Parse.Query) { + queryResult = result; + } + const jsonQuery = queryResult.toJSON(); + if (jsonQuery.where) { + restWhere = jsonQuery.where; + } + if (jsonQuery.limit) { + restOptions = restOptions || {}; + restOptions.limit = jsonQuery.limit; + } + if (jsonQuery.skip) { + restOptions = restOptions || {}; + restOptions.skip = jsonQuery.skip; + } + if (jsonQuery.include) { + restOptions = restOptions || {}; + restOptions.include = jsonQuery.include; + } + if (jsonQuery.keys) { + restOptions = restOptions || {}; + restOptions.keys = jsonQuery.keys; + } + if (jsonQuery.order) { + restOptions = restOptions || {}; + restOptions.order = jsonQuery.order; + } + if (requestObject.readPreference) { + restOptions = restOptions || {}; + restOptions.readPreference = requestObject.readPreference; + } + if (requestObject.includeReadPreference) { + restOptions = restOptions || {}; + restOptions.includeReadPreference = + requestObject.includeReadPreference; + } + if (requestObject.subqueryReadPreference) { + restOptions = restOptions || {}; + restOptions.subqueryReadPreference = + requestObject.subqueryReadPreference; + } + return { + restWhere, + restOptions, + }; + }, + err => { + if (typeof err === 'string') { + throw new Parse.Error(1, err); + } else { + throw err; + } + } + ); } // To be used as part of the promise chain when saving/deleting an object @@ -408,53 +495,107 @@ export function maybeRunQueryTrigger(triggerType, className, restWhere, restOpti // Resolves to an object, empty or containing an object key. A beforeSave // trigger will set the object key to the rest format object to save. // originalParseObject is optional, we only need that for before/afterSave functions -export function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, config, context) { +export function maybeRunTrigger( + triggerType, + auth, + parseObject, + originalParseObject, + config, + context +) { if (!parseObject) { return Promise.resolve({}); } - return new Promise(function (resolve, reject) { - var trigger = getTrigger(parseObject.className, triggerType, config.applicationId); + return new Promise(function(resolve, reject) { + var trigger = getTrigger( + parseObject.className, + triggerType, + config.applicationId + ); if (!trigger) return resolve(); - var request = getRequestObject(triggerType, auth, parseObject, originalParseObject, config, context); - var { success, error } = getResponseObject(request, (object) => { - logTriggerSuccessBeforeHook( - triggerType, parseObject.className, parseObject.toJSON(), object, auth); - if (triggerType === Types.beforeSave || triggerType === Types.afterSave) { - Object.assign(context, request.context); + var request = getRequestObject( + triggerType, + auth, + parseObject, + originalParseObject, + config, + context + ); + var { success, error } = getResponseObject( + request, + object => { + logTriggerSuccessBeforeHook( + triggerType, + parseObject.className, + parseObject.toJSON(), + object, + auth + ); + if ( + triggerType === Types.beforeSave || + triggerType === Types.afterSave + ) { + Object.assign(context, request.context); + } + resolve(object); + }, + error => { + logTriggerErrorBeforeHook( + triggerType, + parseObject.className, + parseObject.toJSON(), + auth, + error + ); + reject(error); } - resolve(object); - }, (error) => { - logTriggerErrorBeforeHook( - triggerType, parseObject.className, parseObject.toJSON(), auth, error); - reject(error); - }); + ); // AfterSave and afterDelete triggers can return a promise, which if they // do, needs to be resolved before this promise is resolved, // so trigger execution is synced with RestWrite.execute() call. // If triggers do not return a promise, they can run async code parallel // to the RestWrite.execute() call. - return Promise.resolve().then(() => { - const promise = trigger(request); - if(triggerType === Types.afterSave || triggerType === Types.afterDelete) { - logTriggerAfterHook(triggerType, parseObject.className, parseObject.toJSON(), auth); - } - return promise; - }).then(success, error); + return Promise.resolve() + .then(() => { + const promise = trigger(request); + if ( + triggerType === Types.afterSave || + triggerType === Types.afterDelete + ) { + logTriggerAfterHook( + triggerType, + parseObject.className, + parseObject.toJSON(), + auth + ); + } + return promise; + }) + .then(success, error); }); } // Converts a REST-format object to a Parse.Object // data is either className or an object export function inflate(data, restObject) { - var copy = typeof data == 'object' ? data : {className: data}; + var copy = typeof data == 'object' ? data : { className: data }; for (var key in restObject) { copy[key] = restObject[key]; } return Parse.Object.fromJSON(copy); } -export function runLiveQueryEventHandlers(data, applicationId = Parse.applicationId) { - if (!_triggerStore || !_triggerStore[applicationId] || !_triggerStore[applicationId].LiveQuery) { return; } - _triggerStore[applicationId].LiveQuery.forEach((handler) => handler(data)); +export function runLiveQueryEventHandlers( + data, + applicationId = Parse.applicationId +) { + if ( + !_triggerStore || + !_triggerStore[applicationId] || + !_triggerStore[applicationId].LiveQuery + ) { + return; + } + _triggerStore[applicationId].LiveQuery.forEach(handler => handler(data)); } diff --git a/src/vendor/mongodbUrl.js b/src/vendor/mongodbUrl.js index 4e3689f0c3..ca8a1faca0 100644 --- a/src/vendor/mongodbUrl.js +++ b/src/vendor/mongodbUrl.js @@ -43,26 +43,26 @@ const simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/; const hostnameMaxLen = 255; // protocols that can allow "unsafe" and "unwise" chars. const unsafeProtocol = { - 'javascript': true, - 'javascript:': true + javascript: true, + 'javascript:': true, }; // protocols that never have a hostname. const hostlessProtocol = { - 'javascript': true, - 'javascript:': true + javascript: true, + 'javascript:': true, }; // protocols that always contain a // bit. const slashedProtocol = { - 'http': true, + http: true, 'http:': true, - 'https': true, + https: true, 'https:': true, - 'ftp': true, + ftp: true, 'ftp:': true, - 'gopher': true, + gopher: true, 'gopher:': true, - 'file': true, - 'file:': true + file: true, + 'file:': true, }; const querystring = require('querystring'); @@ -94,16 +94,16 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { const code = url.charCodeAt(i); // Find first and last non-whitespace characters for trimming - const isWs = code === 32/* */ || - code === 9/*\t*/ || - code === 13/*\r*/ || - code === 10/*\n*/ || - code === 12/*\f*/ || - code === 160/*\u00A0*/ || - code === 65279/*\uFEFF*/; + const isWs = + code === 32 /* */ || + code === 9 /*\t*/ || + code === 13 /*\r*/ || + code === 10 /*\n*/ || + code === 12 /*\f*/ || + code === 160 /*\u00A0*/ || + code === 65279 /*\uFEFF*/; if (start === -1) { - if (isWs) - continue; + if (isWs) continue; lastPos = start = i; } else { if (inWs) { @@ -120,20 +120,19 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { // Only convert backslashes while we haven't seen a split character if (!split) { switch (code) { - case 35: // '#' - hasHash = true; + case 35: // '#' + hasHash = true; // Fall through - case 63: // '?' - split = true; - break; - case 92: // '\\' - if (i - lastPos > 0) - rest += url.slice(lastPos, i); - rest += '/'; - lastPos = i + 1; - break; + case 63: // '?' + split = true; + break; + case 92: // '\\' + if (i - lastPos > 0) rest += url.slice(lastPos, i); + rest += '/'; + lastPos = i + 1; + break; } - } else if (!hasHash && code === 35/*#*/) { + } else if (!hasHash && code === 35 /*#*/) { hasHash = true; } } @@ -144,10 +143,8 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { // We didn't convert any backslashes if (end === -1) { - if (start === 0) - rest = url; - else - rest = url.slice(start); + if (start === 0) rest = url; + else rest = url.slice(start); } else { rest = url.slice(start, end); } @@ -195,17 +192,18 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { // resolution will treat //foo/bar as host=foo,path=bar because that's // how the browser resolves relative URLs. if (slashesDenoteHost || proto || /^\/\/[^@\/]+@[^@\/]+/.test(rest)) { - var slashes = rest.charCodeAt(0) === 47/*/*/ && - rest.charCodeAt(1) === 47/*/*/; + var slashes = + rest.charCodeAt(0) === 47 /*/*/ && rest.charCodeAt(1) === 47 /*/*/; if (slashes && !(proto && hostlessProtocol[proto])) { rest = rest.slice(2); this.slashes = true; } } - if (!hostlessProtocol[proto] && - (slashes || (proto && !slashedProtocol[proto]))) { - + if ( + !hostlessProtocol[proto] && + (slashes || (proto && !slashedProtocol[proto])) + ) { // there's a hostname. // the first instance of /, ?, ;, or # ends the host. // @@ -226,43 +224,40 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { var nonHost = -1; for (i = 0; i < rest.length; ++i) { switch (rest.charCodeAt(i)) { - case 9: // '\t' - case 10: // '\n' - case 13: // '\r' - case 32: // ' ' - case 34: // '"' - case 37: // '%' - case 39: // '\'' - case 59: // ';' - case 60: // '<' - case 62: // '>' - case 92: // '\\' - case 94: // '^' - case 96: // '`' - case 123: // '{' - case 124: // '|' - case 125: // '}' - // Characters that are never ever allowed in a hostname from RFC 2396 - if (nonHost === -1) - nonHost = i; - break; - case 35: // '#' - case 47: // '/' - case 63: // '?' - // Find the first instance of any host-ending characters - if (nonHost === -1) - nonHost = i; - hostEnd = i; - break; - case 64: // '@' - // At this point, either we have an explicit point where the - // auth portion cannot go past, or the last @ char is the decider. - atSign = i; - nonHost = -1; - break; + case 9: // '\t' + case 10: // '\n' + case 13: // '\r' + case 32: // ' ' + case 34: // '"' + case 37: // '%' + case 39: // '\'' + case 59: // ';' + case 60: // '<' + case 62: // '>' + case 92: // '\\' + case 94: // '^' + case 96: // '`' + case 123: // '{' + case 124: // '|' + case 125: // '}' + // Characters that are never ever allowed in a hostname from RFC 2396 + if (nonHost === -1) nonHost = i; + break; + case 35: // '#' + case 47: // '/' + case 63: // '?' + // Find the first instance of any host-ending characters + if (nonHost === -1) nonHost = i; + hostEnd = i; + break; + case 64: // '@' + // At this point, either we have an explicit point where the + // auth portion cannot go past, or the last @ char is the decider. + atSign = i; + nonHost = -1; + break; } - if (hostEnd !== -1) - break; + if (hostEnd !== -1) break; } start = 0; if (atSign !== -1) { @@ -282,21 +277,20 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { // we've indicated that there is a hostname, // so even if it's empty, it has to be present. - if (typeof this.hostname !== 'string') - this.hostname = ''; + if (typeof this.hostname !== 'string') this.hostname = ''; var hostname = this.hostname; // if hostname begins with [ and ends with ] // assume that it's an IPv6 address. - var ipv6Hostname = hostname.charCodeAt(0) === 91/*[*/ && - hostname.charCodeAt(hostname.length - 1) === 93/*]*/; + var ipv6Hostname = + hostname.charCodeAt(0) === 91 /*[*/ && + hostname.charCodeAt(hostname.length - 1) === 93 /*]*/; // validate a little. if (!ipv6Hostname) { const result = validateHostname(this, rest, hostname); - if (result !== undefined) - rest = result; + if (result !== undefined) rest = result; } if (this.hostname.length > hostnameMaxLen) { @@ -335,19 +329,18 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { // escaped, even if encodeURIComponent doesn't think they // need to be. const result = autoEscapeStr(rest); - if (result !== undefined) - rest = result; + if (result !== undefined) rest = result; } var questionIdx = -1; var hashIdx = -1; for (i = 0; i < rest.length; ++i) { const code = rest.charCodeAt(i); - if (code === 35/*#*/) { + if (code === 35 /*#*/) { this.hash = rest.slice(i); hashIdx = i; break; - } else if (code === 63/*?*/ && questionIdx === -1) { + } else if (code === 63 /*?*/ && questionIdx === -1) { questionIdx = i; } } @@ -369,18 +362,16 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { this.query = {}; } - var firstIdx = (questionIdx !== -1 && - (hashIdx === -1 || questionIdx < hashIdx) - ? questionIdx - : hashIdx); + var firstIdx = + questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx) + ? questionIdx + : hashIdx; if (firstIdx === -1) { - if (rest.length > 0) - this.pathname = rest; + if (rest.length > 0) this.pathname = rest; } else if (firstIdx > 0) { this.pathname = rest.slice(0, firstIdx); } - if (slashedProtocol[lowerProto] && - this.hostname && !this.pathname) { + if (slashedProtocol[lowerProto] && this.hostname && !this.pathname) { this.pathname = '/'; } @@ -400,9 +391,8 @@ Url.prototype.parse = function(url, parseQueryString, slashesDenoteHost) { function validateHostname(self, rest, hostname) { for (var i = 0, lastPos; i <= hostname.length; ++i) { var code; - if (i < hostname.length) - code = hostname.charCodeAt(i); - if (code === 46/*.*/ || i === hostname.length) { + if (i < hostname.length) code = hostname.charCodeAt(i); + if (code === 46 /*.*/ || i === hostname.length) { if (i - lastPos > 0) { if (i - lastPos > 63) { self.hostname = hostname.slice(0, lastPos + 63); @@ -411,23 +401,24 @@ function validateHostname(self, rest, hostname) { } lastPos = i + 1; continue; - } else if ((code >= 48/*0*/ && code <= 57/*9*/) || - (code >= 97/*a*/ && code <= 122/*z*/) || - code === 45/*-*/ || - (code >= 65/*A*/ && code <= 90/*Z*/) || - code === 43/*+*/ || - code === 95/*_*/ || - /* BEGIN MONGO URI PATCH */ - code === 44/*,*/ || - code === 58/*:*/ || - /* END MONGO URI PATCH */ - code > 127) { + } else if ( + (code >= 48 /*0*/ && code <= 57) /*9*/ || + (code >= 97 /*a*/ && code <= 122) /*z*/ || + code === 45 /*-*/ || + (code >= 65 /*A*/ && code <= 90) /*Z*/ || + code === 43 /*+*/ || + code === 95 /*_*/ || + /* BEGIN MONGO URI PATCH */ + code === 44 /*,*/ || + code === 58 /*:*/ || + /* END MONGO URI PATCH */ + code > 127 + ) { continue; } // Invalid host character self.hostname = hostname.slice(0, i); - if (i < hostname.length) - return '/' + hostname.slice(i) + rest; + if (i < hostname.length) return '/' + hostname.slice(i) + rest; break; } } @@ -440,98 +431,81 @@ function autoEscapeStr(rest) { // Automatically escape all delimiters and unwise characters from RFC 2396 // Also escape single quotes in case of an XSS attack switch (rest.charCodeAt(i)) { - case 9: // '\t' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%09'; - lastPos = i + 1; - break; - case 10: // '\n' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%0A'; - lastPos = i + 1; - break; - case 13: // '\r' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%0D'; - lastPos = i + 1; - break; - case 32: // ' ' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%20'; - lastPos = i + 1; - break; - case 34: // '"' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%22'; - lastPos = i + 1; - break; - case 39: // '\'' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%27'; - lastPos = i + 1; - break; - case 60: // '<' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%3C'; - lastPos = i + 1; - break; - case 62: // '>' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%3E'; - lastPos = i + 1; - break; - case 92: // '\\' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%5C'; - lastPos = i + 1; - break; - case 94: // '^' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%5E'; - lastPos = i + 1; - break; - case 96: // '`' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%60'; - lastPos = i + 1; - break; - case 123: // '{' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%7B'; - lastPos = i + 1; - break; - case 124: // '|' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%7C'; - lastPos = i + 1; - break; - case 125: // '}' - if (i - lastPos > 0) - newRest += rest.slice(lastPos, i); - newRest += '%7D'; - lastPos = i + 1; - break; + case 9: // '\t' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%09'; + lastPos = i + 1; + break; + case 10: // '\n' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%0A'; + lastPos = i + 1; + break; + case 13: // '\r' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%0D'; + lastPos = i + 1; + break; + case 32: // ' ' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%20'; + lastPos = i + 1; + break; + case 34: // '"' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%22'; + lastPos = i + 1; + break; + case 39: // '\'' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%27'; + lastPos = i + 1; + break; + case 60: // '<' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%3C'; + lastPos = i + 1; + break; + case 62: // '>' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%3E'; + lastPos = i + 1; + break; + case 92: // '\\' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%5C'; + lastPos = i + 1; + break; + case 94: // '^' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%5E'; + lastPos = i + 1; + break; + case 96: // '`' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%60'; + lastPos = i + 1; + break; + case 123: // '{' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%7B'; + lastPos = i + 1; + break; + case 124: // '|' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%7C'; + lastPos = i + 1; + break; + case 125: // '}' + if (i - lastPos > 0) newRest += rest.slice(lastPos, i); + newRest += '%7D'; + lastPos = i + 1; + break; } } - if (lastPos === 0) - return; - if (lastPos < rest.length) - return newRest + rest.slice(lastPos); - else - return newRest; + if (lastPos === 0) return; + if (lastPos < rest.length) return newRest + rest.slice(lastPos); + else return newRest; } // format a parsed object into a url string @@ -542,11 +516,12 @@ function urlFormat(obj) { // this way, you can call url_format() on strings // to clean up potentially wonky urls. if (typeof obj === 'string') obj = urlParse(obj); - else if (typeof obj !== 'object' || obj === null) - throw new TypeError('Parameter "urlObj" must be an object, not ' + - obj === null ? 'null' : typeof obj); - + throw new TypeError( + 'Parameter "urlObj" must be an object, not ' + obj === null + ? 'null' + : typeof obj + ); else if (!(obj instanceof Url)) return Url.prototype.format.call(obj); return obj.format(); @@ -569,9 +544,11 @@ Url.prototype.format = function() { if (this.host) { host = auth + this.host; } else if (this.hostname) { - host = auth + (this.hostname.indexOf(':') === -1 ? - this.hostname : - '[' + this.hostname + ']'); + host = + auth + + (this.hostname.indexOf(':') === -1 + ? this.hostname + : '[' + this.hostname + ']'); if (this.port) { host += ':' + this.port; } @@ -580,42 +557,41 @@ Url.prototype.format = function() { if (this.query !== null && typeof this.query === 'object') query = querystring.stringify(this.query); - var search = this.search || (query && ('?' + query)) || ''; + var search = this.search || (query && '?' + query) || ''; - if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58/*:*/) + if (protocol && protocol.charCodeAt(protocol.length - 1) !== 58 /*:*/) protocol += ':'; var newPathname = ''; var lastPos = 0; for (var i = 0; i < pathname.length; ++i) { switch (pathname.charCodeAt(i)) { - case 35: // '#' - if (i - lastPos > 0) - newPathname += pathname.slice(lastPos, i); - newPathname += '%23'; - lastPos = i + 1; - break; - case 63: // '?' - if (i - lastPos > 0) - newPathname += pathname.slice(lastPos, i); - newPathname += '%3F'; - lastPos = i + 1; - break; + case 35: // '#' + if (i - lastPos > 0) newPathname += pathname.slice(lastPos, i); + newPathname += '%23'; + lastPos = i + 1; + break; + case 63: // '?' + if (i - lastPos > 0) newPathname += pathname.slice(lastPos, i); + newPathname += '%3F'; + lastPos = i + 1; + break; } } if (lastPos > 0) { if (lastPos !== pathname.length) pathname = newPathname + pathname.slice(lastPos); - else - pathname = newPathname; + else pathname = newPathname; } // only the slashedProtocols get the //. Not mailto:, xmpp:, etc. // unless they had them to begin with. - if (this.slashes || - (!protocol || slashedProtocol[protocol]) && host !== false) { + if ( + this.slashes || + ((!protocol || slashedProtocol[protocol]) && host !== false) + ) { host = '//' + (host || ''); - if (pathname && pathname.charCodeAt(0) !== 47/*/*/) + if (pathname && pathname.charCodeAt(0) !== 47 /*/*/) pathname = '/' + pathname; } else if (!host) { host = ''; @@ -623,8 +599,8 @@ Url.prototype.format = function() { search = search.replace('#', '%23'); - if (hash && hash.charCodeAt(0) !== 35/*#*/) hash = '#' + hash; - if (search && search.charCodeAt(0) !== 63/*?*/) search = '?' + search; + if (hash && hash.charCodeAt(0) !== 35 /*#*/) hash = '#' + hash; + if (search && search.charCodeAt(0) !== 63 /*?*/) search = '?' + search; return protocol + host + pathname + search + hash; }; @@ -676,13 +652,15 @@ Url.prototype.resolveObject = function(relative) { var rkeys = Object.keys(relative); for (var rk = 0; rk < rkeys.length; rk++) { var rkey = rkeys[rk]; - if (rkey !== 'protocol') - result[rkey] = relative[rkey]; + if (rkey !== 'protocol') result[rkey] = relative[rkey]; } //urlParse appends trailing / to urls like http://www.example.com - if (slashedProtocol[result.protocol] && - result.hostname && !result.pathname) { + if ( + slashedProtocol[result.protocol] && + result.hostname && + !result.pathname + ) { result.path = result.pathname = '/'; } @@ -710,9 +688,11 @@ Url.prototype.resolveObject = function(relative) { } result.protocol = relative.protocol; - if (!relative.host && - !/^file:?$/.test(relative.protocol) && - !hostlessProtocol[relative.protocol]) { + if ( + !relative.host && + !/^file:?$/.test(relative.protocol) && + !hostlessProtocol[relative.protocol] + ) { const relPath = (relative.pathname || '').split('/'); while (relPath.length && !(relative.host = relPath.shift())); if (!relative.host) relative.host = ''; @@ -740,16 +720,14 @@ Url.prototype.resolveObject = function(relative) { return result; } - var isSourceAbs = (result.pathname && result.pathname.charAt(0) === '/'); - var isRelAbs = ( - relative.host || - relative.pathname && relative.pathname.charAt(0) === '/' - ); - var mustEndAbs = (isRelAbs || isSourceAbs || - (result.host && relative.pathname)); + var isSourceAbs = result.pathname && result.pathname.charAt(0) === '/'; + var isRelAbs = + relative.host || (relative.pathname && relative.pathname.charAt(0) === '/'); + var mustEndAbs = + isRelAbs || isSourceAbs || (result.host && relative.pathname); var removeAllDots = mustEndAbs; - var srcPath = result.pathname && result.pathname.split('/') || []; - var relPath = relative.pathname && relative.pathname.split('/') || []; + var srcPath = (result.pathname && result.pathname.split('/')) || []; + var relPath = (relative.pathname && relative.pathname.split('/')) || []; var psychotic = result.protocol && !slashedProtocol[result.protocol]; // if the url is a non-slashed url, then relative @@ -779,10 +757,12 @@ Url.prototype.resolveObject = function(relative) { if (isRelAbs) { // it's absolute. - result.host = (relative.host || relative.host === '') ? - relative.host : result.host; - result.hostname = (relative.hostname || relative.hostname === '') ? - relative.hostname : result.hostname; + result.host = + relative.host || relative.host === '' ? relative.host : result.host; + result.hostname = + relative.hostname || relative.hostname === '' + ? relative.hostname + : result.hostname; result.search = relative.search; result.query = relative.query; srcPath = relPath; @@ -804,8 +784,10 @@ Url.prototype.resolveObject = function(relative) { //occasionally the auth can get stuck only in host //this especially happens in cases like //url.resolveObject('mailto:local1@domain1', 'local2@domain2') - const authInHost = result.host && result.host.indexOf('@') > 0 ? - result.host.split('@') : false; + const authInHost = + result.host && result.host.indexOf('@') > 0 + ? result.host.split('@') + : false; if (authInHost) { result.auth = authInHost.shift(); result.host = result.hostname = authInHost.shift(); @@ -815,8 +797,9 @@ Url.prototype.resolveObject = function(relative) { result.query = relative.query; //to support http.request if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : '') + - (result.search ? result.search : ''); + result.path = + (result.pathname ? result.pathname : '') + + (result.search ? result.search : ''); } result.href = result.format(); return result; @@ -840,9 +823,10 @@ Url.prototype.resolveObject = function(relative) { // however, if it ends in anything else non-slashy, // then it must NOT get a trailing slash. var last = srcPath.slice(-1)[0]; - var hasTrailingSlash = ( - (result.host || relative.host || srcPath.length > 1) && - (last === '.' || last === '..') || last === ''); + var hasTrailingSlash = + ((result.host || relative.host || srcPath.length > 1) && + (last === '.' || last === '..')) || + last === ''; // strip single dots, resolve double dots to parent dir // if the path tries to go above the root, `up` ends up > 0 @@ -867,27 +851,35 @@ Url.prototype.resolveObject = function(relative) { } } - if (mustEndAbs && srcPath[0] !== '' && - (!srcPath[0] || srcPath[0].charAt(0) !== '/')) { + if ( + mustEndAbs && + srcPath[0] !== '' && + (!srcPath[0] || srcPath[0].charAt(0) !== '/') + ) { srcPath.unshift(''); } - if (hasTrailingSlash && (srcPath.join('/').substr(-1) !== '/')) { + if (hasTrailingSlash && srcPath.join('/').substr(-1) !== '/') { srcPath.push(''); } - var isAbsolute = srcPath[0] === '' || - (srcPath[0] && srcPath[0].charAt(0) === '/'); + var isAbsolute = + srcPath[0] === '' || (srcPath[0] && srcPath[0].charAt(0) === '/'); // put the host back if (psychotic) { - result.hostname = result.host = isAbsolute ? '' : - srcPath.length ? srcPath.shift() : ''; + result.hostname = result.host = isAbsolute + ? '' + : srcPath.length + ? srcPath.shift() + : ''; //occasionally the auth can get stuck only in host //this especially happens in cases like //url.resolveObject('mailto:local1@domain1', 'local2@domain2') - const authInHost = result.host && result.host.indexOf('@') > 0 ? - result.host.split('@') : false; + const authInHost = + result.host && result.host.indexOf('@') > 0 + ? result.host.split('@') + : false; if (authInHost) { result.auth = authInHost.shift(); result.host = result.hostname = authInHost.shift(); @@ -909,8 +901,9 @@ Url.prototype.resolveObject = function(relative) { //to support request.http if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : '') + - (result.search ? result.search : ''); + result.path = + (result.pathname ? result.pathname : '') + + (result.search ? result.search : ''); } result.auth = relative.auth || result.auth; result.slashes = result.slashes || relative.slashes; @@ -957,16 +950,21 @@ function encodeAuth(str) { // digits // alpha (uppercase) // alpha (lowercase) - if (c === 0x21 || c === 0x2D || c === 0x2E || c === 0x5F || c === 0x7E || - (c >= 0x27 && c <= 0x2A) || - (c >= 0x30 && c <= 0x3A) || - (c >= 0x41 && c <= 0x5A) || - (c >= 0x61 && c <= 0x7A)) { + if ( + c === 0x21 || + c === 0x2d || + c === 0x2e || + c === 0x5f || + c === 0x7e || + (c >= 0x27 && c <= 0x2a) || + (c >= 0x30 && c <= 0x3a) || + (c >= 0x41 && c <= 0x5a) || + (c >= 0x61 && c <= 0x7a) + ) { continue; } - if (i - lastPos > 0) - out += str.slice(lastPos, i); + if (i - lastPos > 0) out += str.slice(lastPos, i); lastPos = i + 1; @@ -978,31 +976,29 @@ function encodeAuth(str) { // Multi-byte characters ... if (c < 0x800) { - out += hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)]; + out += hexTable[0xc0 | (c >> 6)] + hexTable[0x80 | (c & 0x3f)]; continue; } - if (c < 0xD800 || c >= 0xE000) { - out += hexTable[0xE0 | (c >> 12)] + - hexTable[0x80 | ((c >> 6) & 0x3F)] + - hexTable[0x80 | (c & 0x3F)]; + if (c < 0xd800 || c >= 0xe000) { + out += + hexTable[0xe0 | (c >> 12)] + + hexTable[0x80 | ((c >> 6) & 0x3f)] + + hexTable[0x80 | (c & 0x3f)]; continue; } // Surrogate pair ++i; var c2; - if (i < str.length) - c2 = str.charCodeAt(i) & 0x3FF; - else - c2 = 0; - c = 0x10000 + (((c & 0x3FF) << 10) | c2); - out += hexTable[0xF0 | (c >> 18)] + - hexTable[0x80 | ((c >> 12) & 0x3F)] + - hexTable[0x80 | ((c >> 6) & 0x3F)] + - hexTable[0x80 | (c & 0x3F)]; - } - if (lastPos === 0) - return str; - if (lastPos < str.length) - return out + str.slice(lastPos); + if (i < str.length) c2 = str.charCodeAt(i) & 0x3ff; + else c2 = 0; + c = 0x10000 + (((c & 0x3ff) << 10) | c2); + out += + hexTable[0xf0 | (c >> 18)] + + hexTable[0x80 | ((c >> 12) & 0x3f)] + + hexTable[0x80 | ((c >> 6) & 0x3f)] + + hexTable[0x80 | (c & 0x3f)]; + } + if (lastPos === 0) return str; + if (lastPos < str.length) return out + str.slice(lastPos); return out; }