From 03790193282a4a4fde71dc89e8e313fbadc7436b Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Wed, 24 May 2017 10:11:55 +0200 Subject: [PATCH 01/29] feat: Convert $regex value to RegExp object --- src/Adapters/Storage/Mongo/MongoTransform.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 39f313240c..567aaf66a5 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -445,6 +445,8 @@ const transformInteriorAtom = atom => { return DateCoder.JSONToDatabase(atom); } else if (BytesCoder.isValidJSON(atom)) { return BytesCoder.JSONToDatabase(atom); + } else if (typeof atom === 'object' && atom && atom.$regex !== undefined) { + return new RegExp(atom.$regex); } else { return atom; } From 810b7eca1698ecfa69e21e98cfc0a415a08696c5 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Wed, 24 May 2017 11:16:02 +0200 Subject: [PATCH 02/29] feat: Add lib folder --- .gitignore | 2 +- lib/AccountLockout.js | 199 +++ lib/Adapters/AdapterLoader.js | 44 + lib/Adapters/Analytics/AnalyticsAdapter.js | 45 + lib/Adapters/Auth/AuthAdapter.js | 45 + lib/Adapters/Auth/OAuth1Client.js | 222 +++ lib/Adapters/Auth/facebook.js | 56 + lib/Adapters/Auth/github.js | 54 + lib/Adapters/Auth/google.js | 70 + lib/Adapters/Auth/index.js | 123 ++ lib/Adapters/Auth/instagram.js | 43 + lib/Adapters/Auth/janraincapture.js | 55 + lib/Adapters/Auth/janrainengage.js | 73 + lib/Adapters/Auth/linkedin.js | 60 + lib/Adapters/Auth/meetup.js | 53 + lib/Adapters/Auth/qq.js | 53 + lib/Adapters/Auth/spotify.js | 62 + lib/Adapters/Auth/twitter.js | 56 + lib/Adapters/Auth/vkontakte.js | 66 + lib/Adapters/Auth/wechat.js | 47 + lib/Adapters/Auth/weibo.js | 66 + lib/Adapters/Cache/CacheAdapter.js | 57 + lib/Adapters/Cache/InMemoryCache.js | 88 + lib/Adapters/Cache/InMemoryCacheAdapter.js | 58 + lib/Adapters/Cache/NullCacheAdapter.js | 43 + lib/Adapters/Cache/RedisCacheAdapter.js | 123 ++ lib/Adapters/Email/MailAdapter.js | 44 + lib/Adapters/Files/FilesAdapter.js | 58 + lib/Adapters/Files/GridStoreAdapter.js | 114 ++ lib/Adapters/Logger/LoggerAdapter.js | 34 + lib/Adapters/Logger/WinstonLogger.js | 131 ++ lib/Adapters/Logger/WinstonLoggerAdapter.js | 95 ++ lib/Adapters/MessageQueue/EventEmitterMQ.js | 99 ++ lib/Adapters/PubSub/EventEmitterPubSub.js | 92 ++ lib/Adapters/PubSub/RedisPubSub.js | 31 + lib/Adapters/Push/PushAdapter.js | 47 + lib/Adapters/Storage/Mongo/MongoCollection.js | 155 ++ .../Storage/Mongo/MongoSchemaCollection.js | 244 +++ .../Storage/Mongo/MongoStorageAdapter.js | 536 +++++++ lib/Adapters/Storage/Mongo/MongoTransform.js | 1022 ++++++++++++ .../Storage/Postgres/PostgresClient.js | 33 + .../Storage/Postgres/PostgresConfigParser.js | 46 + .../Postgres/PostgresStorageAdapter.js | 1426 +++++++++++++++++ .../Storage/Postgres/sql/array/add-unique.sql | 11 + .../Storage/Postgres/sql/array/add.sql | 11 + .../Postgres/sql/array/contains-all.sql | 11 + .../Storage/Postgres/sql/array/contains.sql | 11 + .../Storage/Postgres/sql/array/remove.sql | 11 + lib/Adapters/Storage/Postgres/sql/index.js | 32 + .../sql/misc/json-object-set-keys.sql | 19 + lib/Auth.js | 241 +++ lib/ClientSDK.js | 42 + lib/Config.js | 324 ++++ lib/Controllers/AdaptableController.js | 101 ++ lib/Controllers/AnalyticsController.js | 69 + lib/Controllers/CacheController.js | 129 ++ lib/Controllers/DatabaseController.js | 1167 ++++++++++++++ lib/Controllers/FilesController.js | 142 ++ lib/Controllers/HooksController.js | 268 ++++ lib/Controllers/LiveQueryController.js | 68 + lib/Controllers/LoggerController.js | 276 ++++ lib/Controllers/PushController.js | 167 ++ lib/Controllers/SchemaCache.js | 121 ++ lib/Controllers/SchemaController.js | 1114 +++++++++++++ lib/Controllers/UserController.js | 318 ++++ lib/LiveQuery/Client.js | 158 ++ lib/LiveQuery/Id.js | 34 + lib/LiveQuery/ParseCloudCodePublisher.js | 67 + lib/LiveQuery/ParseLiveQueryServer.js | 821 ++++++++++ lib/LiveQuery/ParsePubSub.js | 45 + lib/LiveQuery/ParseWebSocketServer.js | 72 + lib/LiveQuery/QueryTools.js | 298 ++++ lib/LiveQuery/RequestSchema.js | 145 ++ lib/LiveQuery/SessionTokenCache.js | 78 + lib/LiveQuery/Subscription.js | 68 + lib/LiveQuery/equalObjects.js | 52 + lib/ParseMessageQueue.js | 30 + lib/ParseServer.js | 514 ++++++ lib/ParseServerRESTController.js | 109 ++ lib/PromiseRouter.js | 302 ++++ lib/Push/PushQueue.js | 92 ++ lib/Push/PushWorker.js | 144 ++ lib/Push/utils.js | 41 + lib/RestQuery.js | 989 ++++++++++++ lib/RestWrite.js | 1104 +++++++++++++ lib/Routers/AnalyticsRouter.js | 51 + lib/Routers/ClassesRouter.js | 281 ++++ lib/Routers/CloudCodeRouter.js | 54 + lib/Routers/FeaturesRouter.js | 93 ++ lib/Routers/FilesRouter.js | 234 +++ lib/Routers/FunctionsRouter.js | 205 +++ lib/Routers/GlobalConfigRouter.js | 79 + lib/Routers/HooksRouter.js | 155 ++ lib/Routers/IAPValidationRouter.js | 147 ++ lib/Routers/InstallationsRouter.js | 113 ++ lib/Routers/LogsRouter.js | 96 ++ lib/Routers/PublicAPIRouter.js | 285 ++++ lib/Routers/PurgeRouter.js | 64 + lib/Routers/PushRouter.js | 104 ++ lib/Routers/RolesRouter.js | 89 + lib/Routers/SchemasRouter.js | 125 ++ lib/Routers/SessionsRouter.js | 167 ++ lib/Routers/UsersRouter.js | 367 +++++ lib/StatusHandler.js | 263 +++ lib/TestUtils.js | 27 + lib/batch.js | 98 ++ lib/cache.js | 11 + .../definitions/parse-live-query-server.js | 45 + lib/cli/definitions/parse-server.js | 257 +++ lib/cli/parse-live-query-server.js | 28 + lib/cli/parse-server.js | 169 ++ lib/cli/utils/commander.js | 150 ++ lib/cli/utils/parsers.js | 80 + lib/cli/utils/runner.js | 49 + lib/cloud-code/HTTPResponse.js | 65 + lib/cloud-code/Parse.Cloud.js | 76 + lib/cloud-code/httpRequest.js | 107 ++ lib/cryptoUtils.js | 59 + lib/defaults.js | 44 + lib/deprecated.js | 11 + lib/index.js | 69 + lib/logger.js | 46 + lib/middlewares.js | 310 ++++ lib/password.js | 29 + lib/requiredParameter.js | 9 + lib/rest.js | 157 ++ lib/triggers.js | 437 +++++ lib/vendor/README.md | 8 + lib/vendor/mongodbUrl.js | 928 +++++++++++ 129 files changed, 22126 insertions(+), 1 deletion(-) create mode 100644 lib/AccountLockout.js create mode 100644 lib/Adapters/AdapterLoader.js create mode 100644 lib/Adapters/Analytics/AnalyticsAdapter.js create mode 100644 lib/Adapters/Auth/AuthAdapter.js create mode 100644 lib/Adapters/Auth/OAuth1Client.js create mode 100644 lib/Adapters/Auth/facebook.js create mode 100644 lib/Adapters/Auth/github.js create mode 100644 lib/Adapters/Auth/google.js create mode 100755 lib/Adapters/Auth/index.js create mode 100644 lib/Adapters/Auth/instagram.js create mode 100644 lib/Adapters/Auth/janraincapture.js create mode 100644 lib/Adapters/Auth/janrainengage.js create mode 100644 lib/Adapters/Auth/linkedin.js create mode 100644 lib/Adapters/Auth/meetup.js create mode 100644 lib/Adapters/Auth/qq.js create mode 100644 lib/Adapters/Auth/spotify.js create mode 100644 lib/Adapters/Auth/twitter.js create mode 100644 lib/Adapters/Auth/vkontakte.js create mode 100644 lib/Adapters/Auth/wechat.js create mode 100644 lib/Adapters/Auth/weibo.js create mode 100644 lib/Adapters/Cache/CacheAdapter.js create mode 100644 lib/Adapters/Cache/InMemoryCache.js create mode 100644 lib/Adapters/Cache/InMemoryCacheAdapter.js create mode 100644 lib/Adapters/Cache/NullCacheAdapter.js create mode 100644 lib/Adapters/Cache/RedisCacheAdapter.js create mode 100644 lib/Adapters/Email/MailAdapter.js create mode 100644 lib/Adapters/Files/FilesAdapter.js create mode 100644 lib/Adapters/Files/GridStoreAdapter.js create mode 100644 lib/Adapters/Logger/LoggerAdapter.js create mode 100644 lib/Adapters/Logger/WinstonLogger.js create mode 100644 lib/Adapters/Logger/WinstonLoggerAdapter.js create mode 100644 lib/Adapters/MessageQueue/EventEmitterMQ.js create mode 100644 lib/Adapters/PubSub/EventEmitterPubSub.js create mode 100644 lib/Adapters/PubSub/RedisPubSub.js create mode 100644 lib/Adapters/Push/PushAdapter.js create mode 100644 lib/Adapters/Storage/Mongo/MongoCollection.js create mode 100644 lib/Adapters/Storage/Mongo/MongoSchemaCollection.js create mode 100644 lib/Adapters/Storage/Mongo/MongoStorageAdapter.js create mode 100644 lib/Adapters/Storage/Mongo/MongoTransform.js create mode 100644 lib/Adapters/Storage/Postgres/PostgresClient.js create mode 100644 lib/Adapters/Storage/Postgres/PostgresConfigParser.js create mode 100644 lib/Adapters/Storage/Postgres/PostgresStorageAdapter.js create mode 100644 lib/Adapters/Storage/Postgres/sql/array/add-unique.sql create mode 100644 lib/Adapters/Storage/Postgres/sql/array/add.sql create mode 100644 lib/Adapters/Storage/Postgres/sql/array/contains-all.sql create mode 100644 lib/Adapters/Storage/Postgres/sql/array/contains.sql create mode 100644 lib/Adapters/Storage/Postgres/sql/array/remove.sql create mode 100644 lib/Adapters/Storage/Postgres/sql/index.js create mode 100644 lib/Adapters/Storage/Postgres/sql/misc/json-object-set-keys.sql create mode 100644 lib/Auth.js create mode 100644 lib/ClientSDK.js create mode 100644 lib/Config.js create mode 100644 lib/Controllers/AdaptableController.js create mode 100644 lib/Controllers/AnalyticsController.js create mode 100644 lib/Controllers/CacheController.js create mode 100644 lib/Controllers/DatabaseController.js create mode 100644 lib/Controllers/FilesController.js create mode 100644 lib/Controllers/HooksController.js create mode 100644 lib/Controllers/LiveQueryController.js create mode 100644 lib/Controllers/LoggerController.js create mode 100644 lib/Controllers/PushController.js create mode 100644 lib/Controllers/SchemaCache.js create mode 100644 lib/Controllers/SchemaController.js create mode 100644 lib/Controllers/UserController.js create mode 100644 lib/LiveQuery/Client.js create mode 100644 lib/LiveQuery/Id.js create mode 100644 lib/LiveQuery/ParseCloudCodePublisher.js create mode 100644 lib/LiveQuery/ParseLiveQueryServer.js create mode 100644 lib/LiveQuery/ParsePubSub.js create mode 100644 lib/LiveQuery/ParseWebSocketServer.js create mode 100644 lib/LiveQuery/QueryTools.js create mode 100644 lib/LiveQuery/RequestSchema.js create mode 100644 lib/LiveQuery/SessionTokenCache.js create mode 100644 lib/LiveQuery/Subscription.js create mode 100644 lib/LiveQuery/equalObjects.js create mode 100644 lib/ParseMessageQueue.js create mode 100644 lib/ParseServer.js create mode 100644 lib/ParseServerRESTController.js create mode 100644 lib/PromiseRouter.js create mode 100644 lib/Push/PushQueue.js create mode 100644 lib/Push/PushWorker.js create mode 100644 lib/Push/utils.js create mode 100644 lib/RestQuery.js create mode 100644 lib/RestWrite.js create mode 100644 lib/Routers/AnalyticsRouter.js create mode 100644 lib/Routers/ClassesRouter.js create mode 100644 lib/Routers/CloudCodeRouter.js create mode 100644 lib/Routers/FeaturesRouter.js create mode 100644 lib/Routers/FilesRouter.js create mode 100644 lib/Routers/FunctionsRouter.js create mode 100644 lib/Routers/GlobalConfigRouter.js create mode 100644 lib/Routers/HooksRouter.js create mode 100644 lib/Routers/IAPValidationRouter.js create mode 100644 lib/Routers/InstallationsRouter.js create mode 100644 lib/Routers/LogsRouter.js create mode 100644 lib/Routers/PublicAPIRouter.js create mode 100644 lib/Routers/PurgeRouter.js create mode 100644 lib/Routers/PushRouter.js create mode 100644 lib/Routers/RolesRouter.js create mode 100644 lib/Routers/SchemasRouter.js create mode 100644 lib/Routers/SessionsRouter.js create mode 100644 lib/Routers/UsersRouter.js create mode 100644 lib/StatusHandler.js create mode 100644 lib/TestUtils.js create mode 100644 lib/batch.js create mode 100644 lib/cache.js create mode 100644 lib/cli/definitions/parse-live-query-server.js create mode 100644 lib/cli/definitions/parse-server.js create mode 100644 lib/cli/parse-live-query-server.js create mode 100755 lib/cli/parse-server.js create mode 100644 lib/cli/utils/commander.js create mode 100644 lib/cli/utils/parsers.js create mode 100644 lib/cli/utils/runner.js create mode 100644 lib/cloud-code/HTTPResponse.js create mode 100644 lib/cloud-code/Parse.Cloud.js create mode 100644 lib/cloud-code/httpRequest.js create mode 100644 lib/cryptoUtils.js create mode 100644 lib/defaults.js create mode 100644 lib/deprecated.js create mode 100644 lib/index.js create mode 100644 lib/logger.js create mode 100644 lib/middlewares.js create mode 100644 lib/password.js create mode 100644 lib/requiredParameter.js create mode 100644 lib/rest.js create mode 100644 lib/triggers.js create mode 100644 lib/vendor/README.md create mode 100644 lib/vendor/mongodbUrl.js diff --git a/.gitignore b/.gitignore index 4e4ee21cae..8f1edaabbf 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,7 @@ node_modules .vscode # Babel.js -lib/ +#lib/ # cache folder .cache diff --git a/lib/AccountLockout.js b/lib/AccountLockout.js new file mode 100644 index 0000000000..6dc8a469d9 --- /dev/null +++ b/lib/AccountLockout.js @@ -0,0 +1,199 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.AccountLockout = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // This class handles the Account Lockout Policy settings. + + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var AccountLockout = exports.AccountLockout = function () { + function AccountLockout(user, config) { + _classCallCheck(this, AccountLockout); + + this._user = user; + this._config = config; + } + + /** + * set _failed_login_count to value + */ + + + _createClass(AccountLockout, [{ + key: '_setFailedLoginCount', + value: function _setFailedLoginCount(value) { + var query = { + username: this._user.username + }; + + var updateFields = { + _failed_login_count: value + }; + + return this._config.database.update('_User', query, updateFields); + } + + /** + * check if the _failed_login_count field has been set + */ + + }, { + key: '_isFailedLoginCountSet', + value: function _isFailedLoginCountSet() { + var query = { + username: this._user.username, + _failed_login_count: { $exists: true } + }; + + return this._config.database.find('_User', query).then(function (users) { + if (Array.isArray(users) && users.length > 0) { + return true; + } else { + return false; + } + }); + } + + /** + * if _failed_login_count is NOT set then set it to 0 + * else do nothing + */ + + }, { + key: '_initFailedLoginCount', + value: function _initFailedLoginCount() { + var _this = this; + + return this._isFailedLoginCountSet().then(function (failedLoginCountIsSet) { + if (!failedLoginCountIsSet) { + return _this._setFailedLoginCount(0); + } + }); + } + + /** + * increment _failed_login_count by 1 + */ + + }, { + key: '_incrementFailedLoginCount', + value: function _incrementFailedLoginCount() { + var query = { + username: this._user.username + }; + + var updateFields = { _failed_login_count: { __op: 'Increment', amount: 1 } }; + + return this._config.database.update('_User', query, updateFields); + } + + /** + * if the failed login count is greater than the threshold + * then sets lockout expiration to 'currenttime + accountPolicy.duration', i.e., account is locked out for the next 'accountPolicy.duration' minutes + * else do nothing + */ + + }, { + key: '_setLockoutExpiration', + value: function _setLockoutExpiration() { + var query = { + username: this._user.username, + _failed_login_count: { $gte: this._config.accountLockout.threshold } + }; + + var now = new Date(); + + var updateFields = { + _account_lockout_expires_at: _node2.default._encode(new Date(now.getTime() + this._config.accountLockout.duration * 60 * 1000)) + }; + + return this._config.database.update('_User', query, updateFields).catch(function (err) { + 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 + } + }); + } + + /** + * if _account_lockout_expires_at > current_time and _failed_login_count > threshold + * reject with account locked error + * else + * resolve + */ + + }, { + key: '_notLocked', + value: function _notLocked() { + var _this2 = this; + + var query = { + username: this._user.username, + _account_lockout_expires_at: { $gt: _node2.default._encode(new Date()) }, + _failed_login_count: { $gte: this._config.accountLockout.threshold } + }; + + return this._config.database.find('_User', query).then(function (users) { + if (Array.isArray(users) && users.length > 0) { + throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Your account is locked due to multiple failed login attempts. Please try again after ' + _this2._config.accountLockout.duration + ' minute(s)'); + } + }); + } + + /** + * set and/or increment _failed_login_count + * if _failed_login_count > threshold + * set the _account_lockout_expires_at to current_time + accountPolicy.duration + * else + * do nothing + */ + + }, { + key: '_handleFailedLoginAttempt', + value: function _handleFailedLoginAttempt() { + var _this3 = this; + + return this._initFailedLoginCount().then(function () { + return _this3._incrementFailedLoginCount(); + }).then(function () { + return _this3._setLockoutExpiration(); + }); + } + + /** + * handle login attempt if the Account Lockout Policy is enabled + */ + + }, { + key: 'handleLoginAttempt', + value: function handleLoginAttempt(loginSuccessful) { + var _this4 = this; + + if (!this._config.accountLockout) { + return Promise.resolve(); + } + return this._notLocked().then(function () { + if (loginSuccessful) { + return _this4._setFailedLoginCount(0); + } else { + return _this4._handleFailedLoginAttempt(); + } + }); + } + }]); + + return AccountLockout; +}(); + +exports.default = AccountLockout; \ No newline at end of file diff --git a/lib/Adapters/AdapterLoader.js b/lib/Adapters/AdapterLoader.js new file mode 100644 index 0000000000..ffdf98fd95 --- /dev/null +++ b/lib/Adapters/AdapterLoader.js @@ -0,0 +1,44 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.loadAdapter = loadAdapter; +function loadAdapter(adapter, defaultAdapter, options) { + if (!adapter) { + if (!defaultAdapter) { + return options; + } + // Load from the default adapter when no adapter is set + return loadAdapter(defaultAdapter, undefined, options); + } else if (typeof adapter === "function") { + try { + return adapter(options); + } catch (e) { + if (e.name === 'TypeError') { + var Adapter = adapter; + return new Adapter(options); + } else { + throw e; + } + } + } else if (typeof adapter === "string") { + /* eslint-disable */ + adapter = require(adapter); + // If it's define as a module, get the default + if (adapter.default) { + adapter = adapter.default; + } + return loadAdapter(adapter, undefined, options); + } else if (adapter.module) { + return loadAdapter(adapter.module, undefined, adapter.options); + } else if (adapter.class) { + return loadAdapter(adapter.class, undefined, adapter.options); + } else if (adapter.adapter) { + return loadAdapter(adapter.adapter, undefined, adapter.options); + } + // return the adapter as provided + return adapter; +} + +exports.default = loadAdapter; \ No newline at end of file diff --git a/lib/Adapters/Analytics/AnalyticsAdapter.js b/lib/Adapters/Analytics/AnalyticsAdapter.js new file mode 100644 index 0000000000..b3d965a47e --- /dev/null +++ b/lib/Adapters/Analytics/AnalyticsAdapter.js @@ -0,0 +1,45 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/*eslint no-unused-vars: "off"*/ +var AnalyticsAdapter = exports.AnalyticsAdapter = function () { + function AnalyticsAdapter() { + _classCallCheck(this, AnalyticsAdapter); + } + + _createClass(AnalyticsAdapter, [{ + key: "appOpened", + + + /* + @param parameters: the analytics request body, analytics info will be in the dimensions property + @param req: the original http request + */ + value: function appOpened(parameters, req) { + return Promise.resolve({}); + } + + /* + @param eventName: the name of the custom eventName + @param parameters: the analytics request body, analytics info will be in the dimensions property + @param req: the original http request + */ + + }, { + key: "trackEvent", + value: function trackEvent(eventName, parameters, req) { + return Promise.resolve({}); + } + }]); + + return AnalyticsAdapter; +}(); + +exports.default = AnalyticsAdapter; \ No newline at end of file diff --git a/lib/Adapters/Auth/AuthAdapter.js b/lib/Adapters/Auth/AuthAdapter.js new file mode 100644 index 0000000000..5685bd02c0 --- /dev/null +++ b/lib/Adapters/Auth/AuthAdapter.js @@ -0,0 +1,45 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/*eslint no-unused-vars: "off"*/ +var AuthAdapter = exports.AuthAdapter = function () { + function AuthAdapter() { + _classCallCheck(this, AuthAdapter); + } + + _createClass(AuthAdapter, [{ + key: "validateAppId", + + + /* + @param appIds: the specified app ids in the configuration + @param authData: the client provided authData + @returns a promise that resolves if the applicationId is valid + */ + value: function validateAppId(appIds, authData) { + return Promise.resolve({}); + } + + /* + @param authData: the client provided authData + @param options: additional options + */ + + }, { + key: "validateAuthData", + value: function validateAuthData(authData, options) { + return Promise.resolve({}); + } + }]); + + return AuthAdapter; +}(); + +exports.default = AuthAdapter; \ No newline at end of file diff --git a/lib/Adapters/Auth/OAuth1Client.js b/lib/Adapters/Auth/OAuth1Client.js new file mode 100644 index 0000000000..4938a47fcb --- /dev/null +++ b/lib/Adapters/Auth/OAuth1Client.js @@ -0,0 +1,222 @@ +'use strict'; + +var https = require('https'), + crypto = require('crypto'); +var Parse = require('parse/node').Parse; + +var OAuth = function OAuth(options) { + if (!options) { + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'No options passed to OAuth'); + } + this.consumer_key = options.consumer_key; + this.consumer_secret = options.consumer_secret; + this.auth_token = options.auth_token; + this.auth_token_secret = options.auth_token_secret; + this.host = options.host; + this.oauth_params = options.oauth_params || {}; +}; + +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); + }); + }).on('error', function () { + reject('Failed to make an OAuth request'); + }); + if (request.body) { + httpRequest.write(request.body); + } + httpRequest.end(); + }); +}; + +OAuth.prototype.buildRequest = function (method, path, params, body) { + if (path.indexOf("/") != 0) { + path = "/" + path; + } + if (params && Object.keys(params).length > 0) { + path += "?" + OAuth.buildParameterString(params); + } + + var request = { + 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; + } + + 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); +}; + +OAuth.prototype.post = function (path, params, body) { + return this.send("POST", path, params, body); +}; + +/* + Proper string %escape encoding +*/ +OAuth.encode = function (str) { + // discuss at: http://phpjs.org/functions/rawurlencode/ + // original by: Brett Zamir (http://brett-zamir.me) + // input by: travc + // input by: Brett Zamir (http://brett-zamir.me) + // input by: Michael Grier + // input by: Ratheous + // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) + // bugfixed by: Brett Zamir (http://brett-zamir.me) + // bugfixed by: Joris + // reimplemented by: Brett Zamir (http://brett-zamir.me) + // reimplemented by: Brett Zamir (http://brett-zamir.me) + // note: This reflects PHP 5.3/6.0+ behavior + // note: Please be aware that this function expects to encode into UTF-8 encoded strings, as found on + // note: pages served as UTF-8 + // example 1: rawurlencode('Kevin van Zonneveld!'); + // returns 1: 'Kevin%20van%20Zonneveld%21' + // example 2: rawurlencode('http://kevin.vanzonneveld.net/'); + // returns 2: 'http%3A%2F%2Fkevin.vanzonneveld.net%2F' + // 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(); + + // 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. + return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A'); +}; + +OAuth.signatureMethod = "HMAC-SHA1"; +OAuth.version = "1.0"; + +/* + Generate a nonce +*/ +OAuth.nonce = function () { + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + for (var i = 0; i < 30; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + }return text; +}; + +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 ""; +}; + +/* + Build the signature string from the object +*/ + +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.signRequest = function (request, oauth_parameters, consumer_secret, auth_token_secret) { + oauth_parameters = oauth_parameters || {}; + + // Set default values + if (!oauth_parameters.oauth_nonce) { + oauth_parameters.oauth_nonce = OAuth.nonce(); + } + if (!oauth_parameters.oauth_timestamp) { + oauth_parameters.oauth_timestamp = Math.floor(new Date().getTime() / 1000); + } + if (!oauth_parameters.oauth_signature_method) { + oauth_parameters.oauth_signature_method = OAuth.signatureMethod; + } + if (!oauth_parameters.oauth_version) { + oauth_parameters.oauth_version = OAuth.version; + } + + if (!auth_token_secret) { + auth_token_secret = ""; + } + // Force GET method if unset + if (!request.method) { + 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) { + var parameters = parametersToMerge[i]; + for (var k in parameters) { + signatureParams[k] = parameters[k]; + } + } + + // Create a string based on the parameters + var parameterString = OAuth.buildParameterString(signatureParams); + + // Build the signature string + var url = "https://" + request.host + "" + request.path; + + 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 signature = OAuth.signature(signatureString, signatureKey); + + // Set the signature in the params + oauth_parameters.oauth_signature = signature; + 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(", "); + + request.headers.Authorization = 'OAuth ' + authHeader; + + // Set the content type header + request.headers["Content-Type"] = "application/x-www-form-urlencoded"; + return request; +}; + +module.exports = OAuth; \ No newline at end of file diff --git a/lib/Adapters/Auth/facebook.js b/lib/Adapters/Auth/facebook.js new file mode 100644 index 0000000000..d08c6bb43d --- /dev/null +++ b/lib/Adapters/Auth/facebook.js @@ -0,0 +1,56 @@ +'use strict'; + +// Helper functions for accessing the Facebook Graph API. +var https = require('https'); +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(function (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. +function validateAppId(appIds, authData) { + var access_token = authData.access_token; + if (!appIds.length) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Facebook auth is not configured.'); + } + return graphRequest('app?access_token=' + access_token).then(function (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. +function graphRequest(path) { + return new Promise(function (resolve, reject) { + https.get('https://graph.facebook.com/v2.5/' + path, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function () { + reject('Failed to validate this access token with Facebook.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/github.js b/lib/Adapters/Auth/github.js new file mode 100644 index 0000000000..495958c6ac --- /dev/null +++ b/lib/Adapters/Auth/github.js @@ -0,0 +1,54 @@ +'use strict'; + +// Helper functions for accessing the github API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + return request('user', authData.access_token).then(function (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. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for api requests +function request(path, access_token) { + return new Promise(function (resolve, reject) { + https.get({ + host: 'api.github.com', + path: '/' + path, + headers: { + 'Authorization': 'bearer ' + access_token, + 'User-Agent': 'parse-server' + } + }, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function () { + reject('Failed to validate this access token with Github.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/google.js b/lib/Adapters/Auth/google.js new file mode 100644 index 0000000000..1a43b2d355 --- /dev/null +++ b/lib/Adapters/Auth/google.js @@ -0,0 +1,70 @@ +'use strict'; + +// Helper functions for accessing the google API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +function validateIdToken(id, token) { + return request("tokeninfo?id_token=" + token).then(function (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 request("tokeninfo?access_token=" + token).then(function (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. +function validateAuthData(authData) { + if (authData.id_token) { + return validateIdToken(authData.id, authData.id_token); + } else { + return validateAuthToken(authData.id, authData.access_token).then(function () { + // Validation with auth token worked + return; + }, function () { + // Try with the id_token param + return validateIdToken(authData.id, authData.access_token); + }); + } +} + +// Returns a promise that fulfills if this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for api requests +function request(path) { + return new Promise(function (resolve, reject) { + https.get("https://www.googleapis.com/oauth2/v3/" + path, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function () { + reject('Failed to validate this access token with Google.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/index.js b/lib/Adapters/Auth/index.js new file mode 100755 index 0000000000..84d5b12bf3 --- /dev/null +++ b/lib/Adapters/Auth/index.js @@ -0,0 +1,123 @@ +'use strict'; + +var _AdapterLoader = require('../AdapterLoader'); + +var _AdapterLoader2 = _interopRequireDefault(_AdapterLoader); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var facebook = require('./facebook'); +var instagram = require("./instagram"); +var linkedin = require("./linkedin"); +var meetup = require("./meetup"); +var google = require("./google"); +var github = require("./github"); +var twitter = require("./twitter"); +var spotify = require("./spotify"); +var digits = require("./twitter"); // digits tokens are validated by twitter +var janrainengage = require("./janrainengage"); +var janraincapture = require("./janraincapture"); +var vkontakte = require("./vkontakte"); +var qq = require("./qq"); +var wechat = require("./wechat"); +var weibo = require("./weibo"); + +var anonymous = { + validateAuthData: function validateAuthData() { + return Promise.resolve(); + }, + validateAppId: function validateAppId() { + return Promise.resolve(); + } +}; + +var providers = { + facebook: facebook, + instagram: instagram, + linkedin: linkedin, + meetup: meetup, + google: google, + github: github, + twitter: twitter, + spotify: spotify, + anonymous: anonymous, + digits: digits, + janrainengage: janrainengage, + janraincapture: janraincapture, + vkontakte: vkontakte, + qq: qq, + wechat: wechat, + weibo: weibo +}; + +function authDataValidator(adapter, appIds, options) { + return function (authData) { + return adapter.validateAuthData(authData, options).then(function () { + if (appIds) { + return adapter.validateAppId(appIds, authData, options); + } + return Promise.resolve(); + }); + }; +} + +function loadAuthAdapter(provider, authOptions) { + var defaultAdapter = providers[provider]; + var adapter = Object.assign({}, defaultAdapter); + var providerOptions = authOptions[provider]; + + if (!defaultAdapter && !providerOptions) { + return; + } + + var appIds = providerOptions ? providerOptions.appIds : undefined; + + // Try the configuration methods + if (providerOptions) { + var optionalAdapter = (0, _AdapterLoader2.default)(providerOptions, undefined, providerOptions); + if (optionalAdapter) { + ['validateAuthData', 'validateAppId'].forEach(function (key) { + if (optionalAdapter[key]) { + adapter[key] = optionalAdapter[key]; + } + }); + } + } + + if (!adapter.validateAuthData || !adapter.validateAppId) { + return; + } + + return { adapter: adapter, appIds: appIds, providerOptions: providerOptions }; +} + +module.exports = function () { + var authOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var enableAnonymousUsers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; + + var _enableAnonymousUsers = enableAnonymousUsers; + var setEnableAnonymousUsers = function setEnableAnonymousUsers(enable) { + _enableAnonymousUsers = enable; + }; + // To handle the test cases on configuration + var getValidatorForProvider = function getValidatorForProvider(provider) { + + if (provider === 'anonymous' && !_enableAnonymousUsers) { + return; + } + + var _loadAuthAdapter = loadAuthAdapter(provider, authOptions), + adapter = _loadAuthAdapter.adapter, + appIds = _loadAuthAdapter.appIds, + providerOptions = _loadAuthAdapter.providerOptions; + + return authDataValidator(adapter, appIds, providerOptions); + }; + + return Object.freeze({ + getValidatorForProvider: getValidatorForProvider, + setEnableAnonymousUsers: setEnableAnonymousUsers + }); +}; + +module.exports.loadAuthAdapter = loadAuthAdapter; \ No newline at end of file diff --git a/lib/Adapters/Auth/instagram.js b/lib/Adapters/Auth/instagram.js new file mode 100644 index 0000000000..3bf9e5ff56 --- /dev/null +++ b/lib/Adapters/Auth/instagram.js @@ -0,0 +1,43 @@ +'use strict'; + +// Helper functions for accessing the instagram API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + return request("users/self/?access_token=" + authData.access_token).then(function (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.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for api requests +function request(path) { + return new Promise(function (resolve, reject) { + https.get("https://api.instagram.com/v1/" + path, 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 validate this access token with Instagram.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/janraincapture.js b/lib/Adapters/Auth/janraincapture.js new file mode 100644 index 0000000000..34828ba097 --- /dev/null +++ b/lib/Adapters/Auth/janraincapture.js @@ -0,0 +1,55 @@ +'use strict'; + +// Helper functions for accessing the Janrain Capture API. +var https = require('https'); +var Parse = require('parse/node').Parse; +var querystring = require('querystring'); + +// 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(function (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.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + //no-op + return Promise.resolve(); +} + +// 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 + }); + + return new Promise(function (resolve, reject) { + https.get({ + host: host, + path: '/entity?' + query_string_data + }, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + resolve(JSON.parse(data)); + }); + }).on('error', function () { + reject('Failed to validate this access token with Janrain capture.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/janrainengage.js b/lib/Adapters/Auth/janrainengage.js new file mode 100644 index 0000000000..494c5160fa --- /dev/null +++ b/lib/Adapters/Auth/janrainengage.js @@ -0,0 +1,73 @@ +'use strict'; + +// Helper functions for accessing the Janrain Engage API. +var https = require('https'); +var Parse = require('parse/node').Parse; +var querystring = require('querystring'); + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData, options) { + return request(options.api_key, authData.auth_token).then(function (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. +function validateAppId() { + //no-op + return Promise.resolve(); +} + +// A promisey wrapper for api requests +function request(api_key, auth_token) { + + var post_data = querystring.stringify({ + 'token': auth_token, + 'apiKey': api_key, + 'format': 'json' + }); + + var post_options = { + host: 'rpxnow.com', + path: '/api/v2/auth_info', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': post_data.length + } + }; + + return new Promise(function (resolve, reject) { + // Create the post request. + var post_req = https.request(post_options, function (res) { + var data = ''; + res.setEncoding('utf8'); + // Append data as we receive it from the Janrain engage server. + res.on('data', function (d) { + data += d; + }); + // Once we have all the data, we can parse it and return the data we want. + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }); + + post_req.write(post_data); + post_req.end(); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/linkedin.js b/lib/Adapters/Auth/linkedin.js new file mode 100644 index 0000000000..e2182baee8 --- /dev/null +++ b/lib/Adapters/Auth/linkedin.js @@ -0,0 +1,60 @@ +'use strict'; + +// Helper functions for accessing the linkedin API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// 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(function (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. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for api requests +function request(path, access_token, is_mobile_sdk) { + var headers = { + 'Authorization': 'Bearer ' + access_token, + 'x-li-format': 'json' + }; + + if (is_mobile_sdk) { + headers['x-li-src'] = 'msdk'; + } + + return new Promise(function (resolve, reject) { + https.get({ + host: 'api.linkedin.com', + path: '/v1/' + path, + headers: headers + }, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function () { + reject('Failed to validate this access token with Linkedin.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/meetup.js b/lib/Adapters/Auth/meetup.js new file mode 100644 index 0000000000..d78b974081 --- /dev/null +++ b/lib/Adapters/Auth/meetup.js @@ -0,0 +1,53 @@ +'use strict'; + +// Helper functions for accessing the meetup API. +var https = require('https'); +var Parse = require('parse/node').Parse; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData) { + return request('member/self', authData.access_token).then(function (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. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for api requests +function request(path, access_token) { + return new Promise(function (resolve, reject) { + https.get({ + host: 'api.meetup.com', + path: '/2/' + path, + headers: { + 'Authorization': 'bearer ' + access_token + } + }, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function () { + reject('Failed to validate this access token with Meetup.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/qq.js b/lib/Adapters/Auth/qq.js new file mode 100644 index 0000000000..34f0781f1f --- /dev/null +++ b/lib/Adapters/Auth/qq.js @@ -0,0 +1,53 @@ +'use strict'; + +// Helper functions for accessing the qq Graph API. +var https = require('https'); +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) { + if (data && data.openid == authData.id) { + return; + } + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills if this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for qq graph requests. +function graphRequest(path) { + return new Promise(function (resolve, reject) { + https.get('https://graph.qq.com/oauth2.0/' + path, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + var starPos = data.indexOf("("); + var endPos = data.indexOf(")"); + if (starPos == -1 || endPos == -1) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); + } + data = data.substring(starPos + 1, endPos - 1); + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function () { + reject('Failed to validate this access token with qq.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/spotify.js b/lib/Adapters/Auth/spotify.js new file mode 100644 index 0000000000..d508f39727 --- /dev/null +++ b/lib/Adapters/Auth/spotify.js @@ -0,0 +1,62 @@ +'use strict'; + +// Helper functions for accessing the Spotify API. +var https = require('https'); +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(function (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. +function validateAppId(appIds, authData) { + var access_token = authData.access_token; + if (!appIds.length) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is not configured.'); + } + return request('me', access_token).then(function (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. +function request(path, access_token) { + return new Promise(function (resolve, reject) { + https.get({ + host: 'api.spotify.com', + path: '/v1/' + path, + headers: { + 'Authorization': 'Bearer ' + access_token + } + }, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function () { + reject('Failed to validate this access token with Spotify.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/twitter.js b/lib/Adapters/Auth/twitter.js new file mode 100644 index 0000000000..47f618a905 --- /dev/null +++ b/lib/Adapters/Auth/twitter.js @@ -0,0 +1,56 @@ +'use strict'; + +// Helper functions for accessing the twitter API. +var OAuth = require('./OAuth1Client'); +var Parse = require('parse/node').Parse; +var logger = require('../../logger').default; + +// Returns a promise that fulfills iff this user id is valid. +function validateAuthData(authData, options) { + if (!options) { + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing'); + } + options = handleMultipleConfigurations(authData, options); + var client = new OAuth(options); + client.host = "api.twitter.com"; + client.auth_token = authData.auth_token; + client.auth_token_secret = authData.auth_token_secret; + + return client.get("/1.1/account/verify_credentials.json").then(function (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.'); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +function handleMultipleConfigurations(authData, options) { + if (Array.isArray(options)) { + var 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.'); + } + options = options.filter(function (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.'); + } + options = options[0]; + } + return options; +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData, + handleMultipleConfigurations: handleMultipleConfigurations +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/vkontakte.js b/lib/Adapters/Auth/vkontakte.js new file mode 100644 index 0000000000..7f91583d82 --- /dev/null +++ b/lib/Adapters/Auth/vkontakte.js @@ -0,0 +1,66 @@ +'use strict'; + +// Helper functions for accessing the vkontakte API. + +var https = require('https'); +var Parse = require('parse/node').Parse; +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) { + if (response && response && response.access_token) { + return request("api.vk.com", "method/secure.checkToken?token=" + authData.access_token + "&client_secret=" + params.appSecret + "&access_token=" + response.access_token).then(function (response) { + if (response && response.response && response.response.user_id == authData.id) { + return; + } + 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.'); + }); +} + +function vkOAuth2Request(params) { + var promise = new Parse.Promise(); + return promise.then(function () { + 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 request("oauth.vk.com", "access_token?client_id=" + params.appIds + "&client_secret=" + params.appSecret + "&v=5.59&grant_type=client_credentials"); + }); +} + +// Returns a promise that fulfills iff this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for api requests +function request(host, path) { + return new Promise(function (resolve, reject) { + https.get("https://" + host + "/" + path, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function () { + reject('Failed to validate this access token with Vk.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/wechat.js b/lib/Adapters/Auth/wechat.js new file mode 100644 index 0000000000..a414dfaf8b --- /dev/null +++ b/lib/Adapters/Auth/wechat.js @@ -0,0 +1,47 @@ +'use strict'; + +// Helper functions for accessing the WeChat Graph API. +var https = require('https'); +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) { + if (data.errcode == 0) { + return; + } + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills if this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for WeChat graph requests. +function graphRequest(path) { + return new Promise(function (resolve, reject) { + https.get('https://api.weixin.qq.com/sns/' + path, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + }).on('error', function () { + reject('Failed to validate this access token with weixin.'); + }); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Auth/weibo.js b/lib/Adapters/Auth/weibo.js new file mode 100644 index 0000000000..f4adddbfb0 --- /dev/null +++ b/lib/Adapters/Auth/weibo.js @@ -0,0 +1,66 @@ +'use strict'; + +// Helper functions for accessing the weibo Graph API. +var https = require('https'); +var Parse = require('parse/node').Parse; +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) { + if (data && data.uid == authData.id) { + return; + } + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'weibo auth is invalid for this user.'); + }); +} + +// Returns a promise that fulfills if this app id is valid. +function validateAppId() { + return Promise.resolve(); +} + +// A promisey wrapper for weibo graph requests. +function graphRequest(access_token) { + return new Promise(function (resolve, reject) { + var postData = querystring.stringify({ + "access_token": access_token + }); + var options = { + hostname: 'api.weibo.com', + path: '/oauth2/get_token_info', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(postData) + } + }; + var req = https.request(options, function (res) { + var data = ''; + res.on('data', function (chunk) { + data += chunk; + }); + res.on('end', function () { + try { + data = JSON.parse(data); + } catch (e) { + return reject(e); + } + resolve(data); + }); + res.on('error', function () { + reject('Failed to validate this access token with weibo.'); + }); + }); + req.on('error', function () { + reject('Failed to validate this access token with weibo.'); + }); + req.write(postData); + req.end(); + }); +} + +module.exports = { + validateAppId: validateAppId, + validateAuthData: validateAuthData +}; \ No newline at end of file diff --git a/lib/Adapters/Cache/CacheAdapter.js b/lib/Adapters/Cache/CacheAdapter.js new file mode 100644 index 0000000000..343697dac0 --- /dev/null +++ b/lib/Adapters/Cache/CacheAdapter.js @@ -0,0 +1,57 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/*eslint no-unused-vars: "off"*/ +var CacheAdapter = exports.CacheAdapter = function () { + function CacheAdapter() { + _classCallCheck(this, CacheAdapter); + } + + _createClass(CacheAdapter, [{ + key: "get", + + /** + * Get a value in the cache + * @param key Cache key to get + * @return Promise that will eventually resolve to the value in the cache. + */ + value: function get(key) {} + + /** + * Set a value in the cache + * @param key Cache key to set + * @param value Value to set the key + * @param ttl Optional TTL + */ + + }, { + key: "put", + value: function put(key, value, ttl) {} + + /** + * Remove a value from the cache. + * @param key Cache key to remove + */ + + }, { + key: "del", + value: function del(key) {} + + /** + * Empty a cache + */ + + }, { + key: "clear", + value: function clear() {} + }]); + + return CacheAdapter; +}(); \ No newline at end of file diff --git a/lib/Adapters/Cache/InMemoryCache.js b/lib/Adapters/Cache/InMemoryCache.js new file mode 100644 index 0000000000..6852a37aaf --- /dev/null +++ b/lib/Adapters/Cache/InMemoryCache.js @@ -0,0 +1,88 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var DEFAULT_CACHE_TTL = 5 * 1000; + +var InMemoryCache = exports.InMemoryCache = function () { + function InMemoryCache(_ref) { + var _ref$ttl = _ref.ttl, + ttl = _ref$ttl === undefined ? DEFAULT_CACHE_TTL : _ref$ttl; + + _classCallCheck(this, InMemoryCache); + + this.ttl = ttl; + this.cache = Object.create(null); + } + + _createClass(InMemoryCache, [{ + key: "get", + value: function get(key) { + var record = this.cache[key]; + if (record == null) { + return null; + } + + // Has Record and isnt expired + if (isNaN(record.expire) || record.expire >= Date.now()) { + return record.value; + } + + // Record has expired + delete this.cache[key]; + return null; + } + }, { + key: "put", + value: function put(key, value) { + var _this = this; + + var ttl = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.ttl; + + if (ttl < 0 || isNaN(ttl)) { + ttl = NaN; + } + + var record = { + value: value, + expire: ttl + Date.now() + }; + + if (!isNaN(record.expire)) { + record.timeout = setTimeout(function () { + _this.del(key); + }, ttl); + } + + this.cache[key] = record; + } + }, { + key: "del", + value: function del(key) { + var record = this.cache[key]; + if (record == null) { + return; + } + + if (record.timeout) { + clearTimeout(record.timeout); + } + delete this.cache[key]; + } + }, { + key: "clear", + value: function clear() { + this.cache = Object.create(null); + } + }]); + + return InMemoryCache; +}(); + +exports.default = InMemoryCache; \ No newline at end of file diff --git a/lib/Adapters/Cache/InMemoryCacheAdapter.js b/lib/Adapters/Cache/InMemoryCacheAdapter.js new file mode 100644 index 0000000000..3b04e8a332 --- /dev/null +++ b/lib/Adapters/Cache/InMemoryCacheAdapter.js @@ -0,0 +1,58 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.InMemoryCacheAdapter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _InMemoryCache = require('./InMemoryCache'); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var InMemoryCacheAdapter = exports.InMemoryCacheAdapter = function () { + function InMemoryCacheAdapter(ctx) { + _classCallCheck(this, InMemoryCacheAdapter); + + this.cache = new _InMemoryCache.InMemoryCache(ctx); + } + + _createClass(InMemoryCacheAdapter, [{ + key: 'get', + value: function get(key) { + var _this = this; + + return new Promise(function (resolve) { + var record = _this.cache.get(key); + if (record == null) { + return resolve(null); + } + + return resolve(JSON.parse(record)); + }); + } + }, { + key: 'put', + value: function put(key, value, ttl) { + this.cache.put(key, JSON.stringify(value), ttl); + return Promise.resolve(); + } + }, { + key: 'del', + value: function del(key) { + this.cache.del(key); + return Promise.resolve(); + } + }, { + key: 'clear', + value: function clear() { + this.cache.clear(); + return Promise.resolve(); + } + }]); + + return InMemoryCacheAdapter; +}(); + +exports.default = InMemoryCacheAdapter; \ No newline at end of file diff --git a/lib/Adapters/Cache/NullCacheAdapter.js b/lib/Adapters/Cache/NullCacheAdapter.js new file mode 100644 index 0000000000..6eca7d8001 --- /dev/null +++ b/lib/Adapters/Cache/NullCacheAdapter.js @@ -0,0 +1,43 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var NullCacheAdapter = exports.NullCacheAdapter = function () { + function NullCacheAdapter() { + _classCallCheck(this, NullCacheAdapter); + } + + _createClass(NullCacheAdapter, [{ + key: "get", + value: function get() { + return new Promise(function (resolve) { + return resolve(null); + }); + } + }, { + key: "put", + value: function put() { + return Promise.resolve(); + } + }, { + key: "del", + value: function del() { + return Promise.resolve(); + } + }, { + key: "clear", + value: function clear() { + return Promise.resolve(); + } + }]); + + return NullCacheAdapter; +}(); + +exports.default = NullCacheAdapter; \ No newline at end of file diff --git a/lib/Adapters/Cache/RedisCacheAdapter.js b/lib/Adapters/Cache/RedisCacheAdapter.js new file mode 100644 index 0000000000..a01463a03d --- /dev/null +++ b/lib/Adapters/Cache/RedisCacheAdapter.js @@ -0,0 +1,123 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RedisCacheAdapter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _redis = require('redis'); + +var _redis2 = _interopRequireDefault(_redis); + +var _logger = require('../../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var DEFAULT_REDIS_TTL = 30 * 1000; // 30 seconds in milliseconds + +function debug() { + _logger2.default.debug.apply(_logger2.default, ['RedisCacheAdapter'].concat(Array.prototype.slice.call(arguments))); +} + +var RedisCacheAdapter = exports.RedisCacheAdapter = function () { + function RedisCacheAdapter(redisCtx) { + var ttl = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_REDIS_TTL; + + _classCallCheck(this, RedisCacheAdapter); + + this.client = _redis2.default.createClient(redisCtx); + this.p = Promise.resolve(); + this.ttl = ttl; + } + + _createClass(RedisCacheAdapter, [{ + key: 'get', + value: function get(key) { + var _this = this; + + debug('get', key); + this.p = this.p.then(function () { + return new Promise(function (resolve) { + _this.client.get(key, function (err, res) { + debug('-> get', key, res); + if (!res) { + return resolve(null); + } + resolve(JSON.parse(res)); + }); + }); + }); + return this.p; + } + }, { + key: 'put', + value: function put(key, value) { + var _this2 = this; + + var ttl = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.ttl; + + value = JSON.stringify(value); + debug('put', key, value, ttl); + if (ttl === 0) { + return this.p; // ttl of zero is a logical no-op, but redis cannot set expire time of zero + } + if (ttl < 0 || isNaN(ttl)) { + ttl = DEFAULT_REDIS_TTL; + } + this.p = this.p.then(function () { + return new Promise(function (resolve) { + if (ttl === Infinity) { + _this2.client.set(key, value, function () { + resolve(); + }); + } else { + _this2.client.psetex(key, ttl, value, function () { + resolve(); + }); + } + }); + }); + return this.p; + } + }, { + key: 'del', + value: function del(key) { + var _this3 = this; + + debug('del', key); + this.p = this.p.then(function () { + return new Promise(function (resolve) { + _this3.client.del(key, function () { + resolve(); + }); + }); + }); + return this.p; + } + }, { + key: 'clear', + value: function clear() { + var _this4 = this; + + debug('clear'); + this.p = this.p.then(function () { + return new Promise(function (resolve) { + _this4.client.flushdb(function () { + resolve(); + }); + }); + }); + return this.p; + } + }]); + + return RedisCacheAdapter; +}(); + +exports.default = RedisCacheAdapter; \ No newline at end of file diff --git a/lib/Adapters/Email/MailAdapter.js b/lib/Adapters/Email/MailAdapter.js new file mode 100644 index 0000000000..589aa3564c --- /dev/null +++ b/lib/Adapters/Email/MailAdapter.js @@ -0,0 +1,44 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/*eslint no-unused-vars: "off"*/ +/* + Mail Adapter prototype + A MailAdapter should implement at least sendMail() + */ +var MailAdapter = exports.MailAdapter = function () { + function MailAdapter() { + _classCallCheck(this, MailAdapter); + } + + _createClass(MailAdapter, [{ + key: "sendMail", + + /* + * A method for sending mail + * @param options would have the parameters + * - to: the recipient + * - text: the raw text of the message + * - subject: the subject of the email + */ + value: function sendMail(options) {} + + /* You can implement those methods if you want + * to provide HTML templates etc... + */ + // sendVerificationEmail({ link, appName, user }) {} + // sendPasswordResetEmail({ link, appName, user }) {} + + }]); + + return MailAdapter; +}(); + +exports.default = MailAdapter; \ No newline at end of file diff --git a/lib/Adapters/Files/FilesAdapter.js b/lib/Adapters/Files/FilesAdapter.js new file mode 100644 index 0000000000..fc1db39af1 --- /dev/null +++ b/lib/Adapters/Files/FilesAdapter.js @@ -0,0 +1,58 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/*eslint no-unused-vars: "off"*/ +// Files Adapter +// +// Allows you to change the file storage mechanism. +// +// Adapter classes must implement the following functions: +// * createFile(config, filename, data) +// * getFileData(config, filename) +// * getFileLocation(config, request, filename) +// +// Default is GridStoreAdapter, which requires mongo +// and for the API server to be using the DatabaseController with Mongo +// database adapter. + +var FilesAdapter = exports.FilesAdapter = function () { + function FilesAdapter() { + _classCallCheck(this, FilesAdapter); + } + + _createClass(FilesAdapter, [{ + key: "createFile", + + /* This method is responsible to store the file in order to be retrieved later by its file name + * + * @param filename the filename to save + * @param data the buffer of data from the file + * @param contentType the supposed contentType + * @discussion the contentType can be undefined if the controller was not able to determine it + * + * @return a promise that should fail if the storage didn't succeed + * + */ + value: function createFile(filename, data, contentType) {} + }, { + key: "deleteFile", + value: function deleteFile(filename) {} + }, { + key: "getFileData", + value: function getFileData(filename) {} + }, { + key: "getFileLocation", + value: function getFileLocation(config, filename) {} + }]); + + return FilesAdapter; +}(); + +exports.default = FilesAdapter; \ No newline at end of file diff --git a/lib/Adapters/Files/GridStoreAdapter.js b/lib/Adapters/Files/GridStoreAdapter.js new file mode 100644 index 0000000000..478844771c --- /dev/null +++ b/lib/Adapters/Files/GridStoreAdapter.js @@ -0,0 +1,114 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.GridStoreAdapter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _mongodb = require('mongodb'); + +var _FilesAdapter2 = require('./FilesAdapter'); + +var _defaults = require('../../defaults'); + +var _defaults2 = _interopRequireDefault(_defaults); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** + GridStoreAdapter + Stores files in Mongo using GridStore + Requires the database adapter to be based on mongoclient + + weak + */ + +var GridStoreAdapter = exports.GridStoreAdapter = function (_FilesAdapter) { + _inherits(GridStoreAdapter, _FilesAdapter); + + function GridStoreAdapter() { + var mongoDatabaseURI = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _defaults2.default.DefaultMongoURI; + + _classCallCheck(this, GridStoreAdapter); + + var _this = _possibleConstructorReturn(this, (GridStoreAdapter.__proto__ || Object.getPrototypeOf(GridStoreAdapter)).call(this)); + + _this._databaseURI = mongoDatabaseURI; + return _this; + } + + _createClass(GridStoreAdapter, [{ + key: '_connect', + value: function _connect() { + if (!this._connectionPromise) { + this._connectionPromise = _mongodb.MongoClient.connect(this._databaseURI); + } + return this._connectionPromise; + } + + // For a given config object, filename, and data, store a file + // Returns a promise + + }, { + key: 'createFile', + value: function createFile(filename, data) { + return this._connect().then(function (database) { + var gridStore = new _mongodb.GridStore(database, filename, 'w'); + return gridStore.open(); + }).then(function (gridStore) { + return gridStore.write(data); + }).then(function (gridStore) { + return gridStore.close(); + }); + } + }, { + key: 'deleteFile', + value: function deleteFile(filename) { + return this._connect().then(function (database) { + var gridStore = new _mongodb.GridStore(database, filename, 'r'); + return gridStore.open(); + }).then(function (gridStore) { + return gridStore.unlink(); + }).then(function (gridStore) { + return gridStore.close(); + }); + } + }, { + key: 'getFileData', + value: function getFileData(filename) { + return this._connect().then(function (database) { + return _mongodb.GridStore.exist(database, filename).then(function () { + var gridStore = new _mongodb.GridStore(database, filename, 'r'); + return gridStore.open(); + }); + }).then(function (gridStore) { + return gridStore.read(); + }); + } + }, { + key: 'getFileLocation', + value: function getFileLocation(config, filename) { + return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename); + } + }, { + key: 'getFileStream', + value: function getFileStream(filename) { + return this._connect().then(function (database) { + return _mongodb.GridStore.exist(database, filename).then(function () { + var gridStore = new _mongodb.GridStore(database, filename, 'r'); + return gridStore.open(); + }); + }); + } + }]); + + return GridStoreAdapter; +}(_FilesAdapter2.FilesAdapter); + +exports.default = GridStoreAdapter; \ No newline at end of file diff --git a/lib/Adapters/Logger/LoggerAdapter.js b/lib/Adapters/Logger/LoggerAdapter.js new file mode 100644 index 0000000000..099b1a7510 --- /dev/null +++ b/lib/Adapters/Logger/LoggerAdapter.js @@ -0,0 +1,34 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/*eslint no-unused-vars: "off"*/ +// Logger Adapter +// +// Allows you to change the logger mechanism +// +// Adapter classes must implement the following functions: +// * log() {} +// * query(options, callback) /* optional */ +// Default is WinstonLoggerAdapter.js + +var LoggerAdapter = exports.LoggerAdapter = function () { + function LoggerAdapter(options) { + _classCallCheck(this, LoggerAdapter); + } + + _createClass(LoggerAdapter, [{ + key: "log", + value: function log(level, message) /* meta */{} + }]); + + return LoggerAdapter; +}(); + +exports.default = LoggerAdapter; \ No newline at end of file diff --git a/lib/Adapters/Logger/WinstonLogger.js b/lib/Adapters/Logger/WinstonLogger.js new file mode 100644 index 0000000000..cffce8584a --- /dev/null +++ b/lib/Adapters/Logger/WinstonLogger.js @@ -0,0 +1,131 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.logger = undefined; +exports.configureLogger = configureLogger; +exports.addTransport = addTransport; +exports.removeTransport = removeTransport; + +var _winston = require('winston'); + +var _winston2 = _interopRequireDefault(_winston); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _winstonDailyRotateFile = require('winston-daily-rotate-file'); + +var _winstonDailyRotateFile2 = _interopRequireDefault(_winstonDailyRotateFile); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _defaults = require('../../defaults'); + +var _defaults2 = _interopRequireDefault(_defaults); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var logger = new _winston2.default.Logger(); +var additionalTransports = []; + +function updateTransports(options) { + var transports = Object.assign({}, logger.transports); + if (options) { + var silent = options.silent; + delete options.silent; + if (_lodash2.default.isNull(options.dirname)) { + delete transports['parse-server']; + delete transports['parse-server-error']; + } else if (!_lodash2.default.isUndefined(options.dirname)) { + transports['parse-server'] = new _winstonDailyRotateFile2.default(Object.assign({}, { + filename: 'parse-server.info', + name: 'parse-server' + }, options, { timestamp: true })); + transports['parse-server-error'] = new _winstonDailyRotateFile2.default(Object.assign({}, { + filename: 'parse-server.err', + name: 'parse-server-error' + }, options, { level: 'error', timestamp: true })); + } + + transports.console = new _winston2.default.transports.Console(Object.assign({ + colorize: true, + name: 'console', + silent: silent + }, options)); + } + // Mount the additional transports + additionalTransports.forEach(function (transport) { + transports[transport.name] = transport; + }); + logger.configure({ + transports: _lodash2.default.values(transports) + }); +} + +function configureLogger() { + var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + _ref$logsFolder = _ref.logsFolder, + logsFolder = _ref$logsFolder === undefined ? _defaults2.default.logsFolder : _ref$logsFolder, + _ref$jsonLogs = _ref.jsonLogs, + jsonLogs = _ref$jsonLogs === undefined ? _defaults2.default.jsonLogs : _ref$jsonLogs, + _ref$logLevel = _ref.logLevel, + logLevel = _ref$logLevel === undefined ? _winston2.default.level : _ref$logLevel, + _ref$verbose = _ref.verbose, + verbose = _ref$verbose === undefined ? _defaults2.default.verbose : _ref$verbose, + _ref$silent = _ref.silent, + silent = _ref$silent === undefined ? _defaults2.default.silent : _ref$silent; + + if (verbose) { + logLevel = 'verbose'; + } + + _winston2.default.level = logLevel; + var options = {}; + + if (logsFolder) { + if (!_path2.default.isAbsolute(logsFolder)) { + logsFolder = _path2.default.resolve(process.cwd(), logsFolder); + } + try { + _fs2.default.mkdirSync(logsFolder); + } catch (e) {/* */} + } + options.dirname = logsFolder; + options.level = logLevel; + options.silent = silent; + + if (jsonLogs) { + options.json = true; + options.stringify = true; + } + updateTransports(options); +} + +function addTransport(transport) { + additionalTransports.push(transport); + updateTransports(); +} + +function removeTransport(transport) { + var transportName = typeof transport == 'string' ? transport : transport.name; + var transports = Object.assign({}, logger.transports); + delete transports[transportName]; + logger.configure({ + transports: _lodash2.default.values(transports) + }); + _lodash2.default.remove(additionalTransports, function (transport) { + return transport.name === transportName; + }); +} + +exports.logger = logger; +exports.default = logger; \ No newline at end of file diff --git a/lib/Adapters/Logger/WinstonLoggerAdapter.js b/lib/Adapters/Logger/WinstonLoggerAdapter.js new file mode 100644 index 0000000000..f01d9e5145 --- /dev/null +++ b/lib/Adapters/Logger/WinstonLoggerAdapter.js @@ -0,0 +1,95 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.WinstonLoggerAdapter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _LoggerAdapter2 = require('./LoggerAdapter'); + +var _WinstonLogger = require('./WinstonLogger'); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; + +var WinstonLoggerAdapter = exports.WinstonLoggerAdapter = function (_LoggerAdapter) { + _inherits(WinstonLoggerAdapter, _LoggerAdapter); + + function WinstonLoggerAdapter(options) { + _classCallCheck(this, WinstonLoggerAdapter); + + var _this = _possibleConstructorReturn(this, (WinstonLoggerAdapter.__proto__ || Object.getPrototypeOf(WinstonLoggerAdapter)).call(this)); + + if (options) { + (0, _WinstonLogger.configureLogger)(options); + } + return _this; + } + + _createClass(WinstonLoggerAdapter, [{ + key: 'log', + value: function log() { + return _WinstonLogger.logger.log.apply(_WinstonLogger.logger, arguments); + } + }, { + key: 'addTransport', + value: function addTransport(transport) { + // Note that this is calling addTransport + // from logger. See import - confusing. + // but this is not recursive. + (0, _WinstonLogger.addTransport)(transport); + } + + // custom query as winston is currently limited + + }, { + key: 'query', + value: function query(options) { + var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; + + if (!options) { + options = {}; + } + // defaults to 7 days prior + var from = options.from || new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); + var until = options.until || new Date(); + var limit = options.size || 10; + var order = options.order || 'desc'; + var level = options.level || 'info'; + + var queryOptions = { + from: from, + until: until, + limit: limit, + order: order + }; + + return new Promise(function (resolve, reject) { + _WinstonLogger.logger.query(queryOptions, function (err, res) { + if (err) { + callback(err); + return reject(err); + } + if (level == 'error') { + callback(res['parse-server-error']); + resolve(res['parse-server-error']); + } else { + callback(res['parse-server']); + resolve(res['parse-server']); + } + }); + }); + } + }]); + + return WinstonLoggerAdapter; +}(_LoggerAdapter2.LoggerAdapter); + +exports.default = WinstonLoggerAdapter; \ No newline at end of file diff --git a/lib/Adapters/MessageQueue/EventEmitterMQ.js b/lib/Adapters/MessageQueue/EventEmitterMQ.js new file mode 100644 index 0000000000..4a7fe3b63e --- /dev/null +++ b/lib/Adapters/MessageQueue/EventEmitterMQ.js @@ -0,0 +1,99 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.EventEmitterMQ = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _events = require('events'); + +var _events2 = _interopRequireDefault(_events); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var emitter = new _events2.default.EventEmitter(); +var subscriptions = new Map(); + +function _unsubscribe(channel) { + if (!subscriptions.has(channel)) { + //console.log('No channel to unsub from'); + return; + } + //console.log('unsub ', channel); + emitter.removeListener(channel, subscriptions.get(channel)); + subscriptions.delete(channel); +} + +var Publisher = function () { + function Publisher(emitter) { + _classCallCheck(this, Publisher); + + this.emitter = emitter; + } + + _createClass(Publisher, [{ + key: 'publish', + value: function publish(channel, message) { + this.emitter.emit(channel, message); + } + }]); + + return Publisher; +}(); + +var Consumer = function (_events$EventEmitter) { + _inherits(Consumer, _events$EventEmitter); + + function Consumer(emitter) { + _classCallCheck(this, Consumer); + + var _this = _possibleConstructorReturn(this, (Consumer.__proto__ || Object.getPrototypeOf(Consumer)).call(this)); + + _this.emitter = emitter; + return _this; + } + + _createClass(Consumer, [{ + key: 'subscribe', + value: function subscribe(channel) { + var _this2 = this; + + _unsubscribe(channel); + var handler = function handler(message) { + _this2.emit('message', channel, message); + }; + subscriptions.set(channel, handler); + this.emitter.on(channel, handler); + } + }, { + key: 'unsubscribe', + value: function unsubscribe(channel) { + _unsubscribe(channel); + } + }]); + + return Consumer; +}(_events2.default.EventEmitter); + +function createPublisher() { + return new Publisher(emitter); +} + +function createSubscriber() { + return new Consumer(emitter); +} + +var EventEmitterMQ = { + createPublisher: createPublisher, + createSubscriber: createSubscriber +}; + +exports.EventEmitterMQ = EventEmitterMQ; \ No newline at end of file diff --git a/lib/Adapters/PubSub/EventEmitterPubSub.js b/lib/Adapters/PubSub/EventEmitterPubSub.js new file mode 100644 index 0000000000..b767a0ad1b --- /dev/null +++ b/lib/Adapters/PubSub/EventEmitterPubSub.js @@ -0,0 +1,92 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.EventEmitterPubSub = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _events = require('events'); + +var _events2 = _interopRequireDefault(_events); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var emitter = new _events2.default.EventEmitter(); + +var Publisher = function () { + function Publisher(emitter) { + _classCallCheck(this, Publisher); + + this.emitter = emitter; + } + + _createClass(Publisher, [{ + key: 'publish', + value: function publish(channel, message) { + this.emitter.emit(channel, message); + } + }]); + + return Publisher; +}(); + +var Subscriber = function (_events$EventEmitter) { + _inherits(Subscriber, _events$EventEmitter); + + function Subscriber(emitter) { + _classCallCheck(this, Subscriber); + + var _this = _possibleConstructorReturn(this, (Subscriber.__proto__ || Object.getPrototypeOf(Subscriber)).call(this)); + + _this.emitter = emitter; + _this.subscriptions = new Map(); + return _this; + } + + _createClass(Subscriber, [{ + key: 'subscribe', + value: function subscribe(channel) { + var _this2 = this; + + var handler = function handler(message) { + _this2.emit('message', channel, message); + }; + this.subscriptions.set(channel, handler); + this.emitter.on(channel, handler); + } + }, { + key: 'unsubscribe', + value: function unsubscribe(channel) { + if (!this.subscriptions.has(channel)) { + return; + } + this.emitter.removeListener(channel, this.subscriptions.get(channel)); + this.subscriptions.delete(channel); + } + }]); + + return Subscriber; +}(_events2.default.EventEmitter); + +function createPublisher() { + return new Publisher(emitter); +} + +function createSubscriber() { + return new Subscriber(emitter); +} + +var EventEmitterPubSub = { + createPublisher: createPublisher, + createSubscriber: createSubscriber +}; + +exports.EventEmitterPubSub = EventEmitterPubSub; \ No newline at end of file diff --git a/lib/Adapters/PubSub/RedisPubSub.js b/lib/Adapters/PubSub/RedisPubSub.js new file mode 100644 index 0000000000..e2c8b8549c --- /dev/null +++ b/lib/Adapters/PubSub/RedisPubSub.js @@ -0,0 +1,31 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RedisPubSub = undefined; + +var _redis = require('redis'); + +var _redis2 = _interopRequireDefault(_redis); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function createPublisher(_ref) { + var redisURL = _ref.redisURL; + + return _redis2.default.createClient(redisURL, { no_ready_check: true }); +} + +function createSubscriber(_ref2) { + var redisURL = _ref2.redisURL; + + return _redis2.default.createClient(redisURL, { no_ready_check: true }); +} + +var RedisPubSub = { + createPublisher: createPublisher, + createSubscriber: createSubscriber +}; + +exports.RedisPubSub = RedisPubSub; \ No newline at end of file diff --git a/lib/Adapters/Push/PushAdapter.js b/lib/Adapters/Push/PushAdapter.js new file mode 100644 index 0000000000..2f51d31691 --- /dev/null +++ b/lib/Adapters/Push/PushAdapter.js @@ -0,0 +1,47 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/*eslint no-unused-vars: "off"*/ +// Push Adapter +// +// Allows you to change the push notification mechanism. +// +// Adapter classes must implement the following functions: +// * getValidPushTypes() +// * send(devices, installations, pushStatus) +// +// Default is ParsePushAdapter, which uses GCM for +// android push and APNS for ios push. + +var PushAdapter = exports.PushAdapter = function () { + function PushAdapter() { + _classCallCheck(this, PushAdapter); + } + + _createClass(PushAdapter, [{ + key: "send", + value: function send(body, installations, pushStatus) {} + + /** + * Get an array of valid push types. + * @returns {Array} An array of valid push types + */ + + }, { + key: "getValidPushTypes", + value: function getValidPushTypes() { + return []; + } + }]); + + return PushAdapter; +}(); + +exports.default = PushAdapter; \ No newline at end of file diff --git a/lib/Adapters/Storage/Mongo/MongoCollection.js b/lib/Adapters/Storage/Mongo/MongoCollection.js new file mode 100644 index 0000000000..a10aaceeaf --- /dev/null +++ b/lib/Adapters/Storage/Mongo/MongoCollection.js @@ -0,0 +1,155 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var mongodb = require('mongodb'); +var Collection = mongodb.Collection; + +var MongoCollection = function () { + function MongoCollection(mongoCollection) { + _classCallCheck(this, MongoCollection); + + this._mongoCollection = mongoCollection; + } + + // Does a find with "smart indexing". + // Currently this just means, if it needs a geoindex and there is + // none, then build the geoindex. + // This could be improved a lot but it's not clear if that's a good + // idea. Or even if this behavior is a good idea. + + + _createClass(MongoCollection, [{ + key: 'find', + value: function find(query) { + var _this = this; + + var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, + skip = _ref.skip, + limit = _ref.limit, + sort = _ref.sort, + keys = _ref.keys, + maxTimeMS = _ref.maxTimeMS; + + return this._rawFind(query, { skip: skip, limit: limit, sort: sort, keys: keys, maxTimeMS: maxTimeMS }).catch(function (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 + var 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(function () { + return _this._rawFind(query, { skip: skip, limit: limit, sort: sort, keys: keys, maxTimeMS: maxTimeMS }); + }); + }); + } + }, { + key: '_rawFind', + value: function _rawFind(query) { + var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, + skip = _ref2.skip, + limit = _ref2.limit, + sort = _ref2.sort, + keys = _ref2.keys, + maxTimeMS = _ref2.maxTimeMS; + + var findOperation = this._mongoCollection.find(query, { skip: skip, limit: limit, sort: sort }); + + if (keys) { + findOperation = findOperation.project(keys); + } + + if (maxTimeMS) { + findOperation = findOperation.maxTimeMS(maxTimeMS); + } + + return findOperation.toArray(); + } + }, { + key: 'count', + value: function count(query) { + var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, + skip = _ref3.skip, + limit = _ref3.limit, + sort = _ref3.sort, + maxTimeMS = _ref3.maxTimeMS; + + var countOperation = this._mongoCollection.count(query, { skip: skip, limit: limit, sort: sort, maxTimeMS: maxTimeMS }); + + return countOperation; + } + }, { + key: 'insertOne', + value: function insertOne(object) { + return this._mongoCollection.insertOne(object); + } + + // Atomically updates data in the database for a single (first) object that matched the query + // If there is nothing that matches the query - does insert + // Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5. + + }, { + key: 'upsertOne', + value: function upsertOne(query, update) { + return this._mongoCollection.update(query, update, { upsert: true }); + } + }, { + key: 'updateOne', + value: function updateOne(query, update) { + return this._mongoCollection.updateOne(query, update); + } + }, { + key: 'updateMany', + value: function updateMany(query, update) { + return this._mongoCollection.updateMany(query, update); + } + }, { + key: 'deleteOne', + value: function deleteOne(query) { + return this._mongoCollection.deleteOne(query); + } + }, { + key: 'deleteMany', + value: function deleteMany(query) { + return this._mongoCollection.deleteMany(query); + } + }, { + key: '_ensureSparseUniqueIndexInBackground', + value: function _ensureSparseUniqueIndexInBackground(indexRequest) { + var _this2 = this; + + return new Promise(function (resolve, reject) { + _this2._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, function (error) { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + }, { + key: 'drop', + value: function drop() { + return this._mongoCollection.drop(); + } + }]); + + return MongoCollection; +}(); + +exports.default = MongoCollection; \ No newline at end of file diff --git a/lib/Adapters/Storage/Mongo/MongoSchemaCollection.js b/lib/Adapters/Storage/Mongo/MongoSchemaCollection.js new file mode 100644 index 0000000000..b7565fadf9 --- /dev/null +++ b/lib/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -0,0 +1,244 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var _MongoCollection = require('./MongoCollection'); + +var _MongoCollection2 = _interopRequireDefault(_MongoCollection); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function mongoFieldToParseSchemaField(type) { + if (type[0] === '*') { + return { + type: 'Pointer', + targetClass: type.slice(1) + }; + } + if (type.startsWith('relation<')) { + return { + type: 'Relation', + targetClass: type.slice('relation<'.length, type.length - 1) + }; + } + switch (type) { + case 'number': + return { type: 'Number' }; + case 'string': + return { type: 'String' }; + case 'boolean': + return { type: 'Boolean' }; + case 'date': + return { type: 'Date' }; + case 'map': + case 'object': + return { type: 'Object' }; + case 'array': + return { type: 'Array' }; + case 'geopoint': + return { type: 'GeoPoint' }; + case 'file': + return { type: 'File' }; + case 'bytes': + return { type: 'Bytes' }; + } +} + +var nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions']; +function mongoSchemaFieldsToParseSchemaFields(schema) { + var fieldNames = Object.keys(schema).filter(function (key) { + return nonFieldSchemaKeys.indexOf(key) === -1; + }); + var response = fieldNames.reduce(function (obj, fieldName) { + obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName]); + return obj; + }, {}); + response.ACL = { type: 'ACL' }; + response.createdAt = { type: 'Date' }; + response.updatedAt = { type: 'Date' }; + response.objectId = { type: 'String' }; + return response; +} + +var emptyCLPS = Object.freeze({ + find: {}, + get: {}, + create: {}, + update: {}, + delete: {}, + addField: {} +}); + +var defaultCLPS = Object.freeze({ + find: { '*': true }, + get: { '*': true }, + create: { '*': true }, + update: { '*': true }, + delete: { '*': true }, + addField: { '*': true } +}); + +function mongoSchemaToParseSchema(mongoSchema) { + var clps = defaultCLPS; + if (mongoSchema._metadata && mongoSchema._metadata.class_permissions) { + clps = _extends({}, emptyCLPS, mongoSchema._metadata.class_permissions); + } + return { + className: mongoSchema._id, + fields: mongoSchemaFieldsToParseSchemaFields(mongoSchema), + classLevelPermissions: clps + }; +} + +function _mongoSchemaQueryFromNameQuery(name, query) { + var object = { _id: name }; + if (query) { + Object.keys(query).forEach(function (key) { + object[key] = query[key]; + }); + } + 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(_ref) { + var type = _ref.type, + targetClass = _ref.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'; + } +} + +var MongoSchemaCollection = function () { + function MongoSchemaCollection(collection) { + _classCallCheck(this, MongoSchemaCollection); + + this._collection = collection; + } + + _createClass(MongoSchemaCollection, [{ + key: '_fetchAllSchemasFrom_SCHEMA', + value: function _fetchAllSchemasFrom_SCHEMA() { + return this._collection._rawFind({}).then(function (schemas) { + return schemas.map(mongoSchemaToParseSchema); + }); + } + }, { + key: '_fechOneSchemaFrom_SCHEMA', + value: function _fechOneSchemaFrom_SCHEMA(name) { + return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(function (results) { + if (results.length === 1) { + return mongoSchemaToParseSchema(results[0]); + } else { + throw undefined; + } + }); + } + + // Atomically find and delete an object based on query. + + }, { + key: 'findAndDeleteSchema', + value: function findAndDeleteSchema(name) { + return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []); + } + }, { + key: 'updateSchema', + value: function updateSchema(name, update) { + return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update); + } + }, { + key: 'upsertSchema', + value: function upsertSchema(name, query, update) { + return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update); + } + + // Add a field to the schema. If database does not support the field + // type (e.g. mongo doesn't support more than one GeoPoint in a class) reject with an "Incorrect Type" + // Parse error with a desciptive message. If the field already exists, this function must + // not modify the schema, and must reject with DUPLICATE_VALUE error. + // If this is called for a class that doesn't exist, this function must create that class. + + // TODO: throw an error if an unsupported field type is passed. Deciding whether a type is supported + // should be the job of the adapter. Some adapters may not support GeoPoint at all. Others may + // Support additional types that Mongo doesn't, like Money, or something. + + // TODO: don't spend an extra query on finding the schema if the type we are trying to add isn't a GeoPoint. + + }, { + key: 'addFieldIfNotExists', + value: function addFieldIfNotExists(className, fieldName, type) { + var _this = this; + + return this._fechOneSchemaFrom_SCHEMA(className).then(function (schema) { + // The schema exists. Check for existing GeoPoints. + if (type.type === 'GeoPoint') { + // Make sure there are not other geopoint fields + if (Object.keys(schema.fields).some(function (existingField) { + return schema.fields[existingField].type === 'GeoPoint'; + })) { + throw new _node2.default.Error(_node2.default.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.'); + } + } + return; + }, function (error) { + // If error is undefined, the schema doesn't exist, and we can create the schema with the field. + // If some other error, reject with it. + if (error === undefined) { + return; + } + throw error; + }).then(function () { + // 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, _defineProperty({}, fieldName, { '$exists': false }), { '$set': _defineProperty({}, fieldName, parseFieldTypeToMongoFieldType(type)) }); + }); + } + }]); + + return 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; + +exports.default = MongoSchemaCollection; \ No newline at end of file diff --git a/lib/Adapters/Storage/Mongo/MongoStorageAdapter.js b/lib/Adapters/Storage/Mongo/MongoStorageAdapter.js new file mode 100644 index 0000000000..d40ad4c11a --- /dev/null +++ b/lib/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -0,0 +1,536 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.MongoStorageAdapter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _MongoCollection = require('./MongoCollection'); + +var _MongoCollection2 = _interopRequireDefault(_MongoCollection); + +var _MongoSchemaCollection = require('./MongoSchemaCollection'); + +var _MongoSchemaCollection2 = _interopRequireDefault(_MongoSchemaCollection); + +var _mongodbUrl = require('../../../vendor/mongodbUrl'); + +var _MongoTransform = require('./MongoTransform'); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _defaults = require('../../../defaults'); + +var _defaults2 = _interopRequireDefault(_defaults); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } + +var mongodb = require('mongodb'); +var MongoClient = mongodb.MongoClient; + +var MongoSchemaCollectionName = '_SCHEMA'; + +var storageAdapterAllCollections = function storageAdapterAllCollections(mongoAdapter) { + return mongoAdapter.connect().then(function () { + return mongoAdapter.database.collections(); + }).then(function (collections) { + return collections.filter(function (collection) { + if (collection.namespace.match(/\.system\./)) { + return false; + } + // TODO: If you have one app with a collection prefix that happens to be a prefix of another + // apps prefix, this will go very very badly. We should fix that somehow. + return collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0; + }); + }); +}; + +var convertParseSchemaToMongoSchema = function convertParseSchemaToMongoSchema(_ref) { + var schema = _objectWithoutProperties(_ref, []); + + delete schema.fields._rperm; + delete schema.fields._wperm; + + if (schema.className === '_User') { + // Legacy mongo adapter knows about the difference between password and _hashed_password. + // Future database adapters will only know about _hashed_password. + // Note: Parse Server will bring back password with injectDefaultSchema, so we don't need + // to add _hashed_password back ever. + delete schema.fields._hashed_password; + } + + return schema; +}; + +// Returns { code, error } if invalid, or { result }, an object +// suitable for inserting into _SCHEMA collection, otherwise. +var mongoSchemaFromFieldsAndClassNameAndCLP = function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) { + var mongoObject = { + _id: className, + objectId: 'string', + updatedAt: 'string', + createdAt: 'string' + }; + + for (var fieldName in fields) { + mongoObject[fieldName] = _MongoSchemaCollection2.default.parseFieldTypeToMongoFieldType(fields[fieldName]); + } + + if (typeof classLevelPermissions !== 'undefined') { + mongoObject._metadata = mongoObject._metadata || {}; + if (!classLevelPermissions) { + delete mongoObject._metadata.class_permissions; + } else { + mongoObject._metadata.class_permissions = classLevelPermissions; + } + } + + return mongoObject; +}; + +var MongoStorageAdapter = exports.MongoStorageAdapter = function () { + // Public + function MongoStorageAdapter(_ref2) { + var _ref2$uri = _ref2.uri, + uri = _ref2$uri === undefined ? _defaults2.default.DefaultMongoURI : _ref2$uri, + _ref2$collectionPrefi = _ref2.collectionPrefix, + collectionPrefix = _ref2$collectionPrefi === undefined ? '' : _ref2$collectionPrefi, + _ref2$mongoOptions = _ref2.mongoOptions, + mongoOptions = _ref2$mongoOptions === undefined ? {} : _ref2$mongoOptions; + + _classCallCheck(this, MongoStorageAdapter); + + this._uri = uri; + this._collectionPrefix = collectionPrefix; + this._mongoOptions = mongoOptions; + + // MaxTimeMS is not a global MongoDB client option, it is applied per operation. + this._maxTimeMS = mongoOptions.maxTimeMS; + } + // Private + + + _createClass(MongoStorageAdapter, [{ + key: 'connect', + value: function connect() { + var _this = this; + + if (this.connectionPromise) { + return this.connectionPromise; + } + + // parsing and re-formatting causes the auth value (if there) to get URI + // encoded + var encodedUri = (0, _mongodbUrl.format)((0, _mongodbUrl.parse)(this._uri)); + + this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions).then(function (database) { + if (!database) { + delete _this.connectionPromise; + return; + } + database.on('error', function () { + delete _this.connectionPromise; + }); + database.on('close', function () { + delete _this.connectionPromise; + }); + _this.database = database; + }).catch(function (err) { + delete _this.connectionPromise; + return Promise.reject(err); + }); + + return this.connectionPromise; + } + }, { + key: 'handleShutdown', + value: function handleShutdown() { + if (!this.database) { + return; + } + this.database.close(false); + } + }, { + key: '_adaptiveCollection', + value: function _adaptiveCollection(name) { + var _this2 = this; + + return this.connect().then(function () { + return _this2.database.collection(_this2._collectionPrefix + name); + }).then(function (rawCollection) { + return new _MongoCollection2.default(rawCollection); + }); + } + }, { + key: '_schemaCollection', + value: function _schemaCollection() { + var _this3 = this; + + return this.connect().then(function () { + return _this3._adaptiveCollection(MongoSchemaCollectionName); + }).then(function (collection) { + return new _MongoSchemaCollection2.default(collection); + }); + } + }, { + key: 'classExists', + value: function classExists(name) { + var _this4 = this; + + return this.connect().then(function () { + return _this4.database.listCollections({ name: _this4._collectionPrefix + name }).toArray(); + }).then(function (collections) { + return collections.length > 0; + }); + } + }, { + key: 'setClassLevelPermissions', + value: function setClassLevelPermissions(className, CLPs) { + return this._schemaCollection().then(function (schemaCollection) { + return schemaCollection.updateSchema(className, { + $set: { _metadata: { class_permissions: CLPs } } + }); + }); + } + }, { + key: 'createClass', + value: function createClass(className, schema) { + schema = convertParseSchemaToMongoSchema(schema); + var mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions); + mongoObject._id = className; + return this._schemaCollection().then(function (schemaCollection) { + return schemaCollection._collection.insertOne(mongoObject); + }).then(function (result) { + return _MongoSchemaCollection2.default._TESTmongoSchemaToParseSchema(result.ops[0]); + }).catch(function (error) { + if (error.code === 11000) { + //Mongo's duplicate key error + throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'Class already exists.'); + } else { + throw error; + } + }); + } + }, { + key: 'addFieldIfNotExists', + value: function addFieldIfNotExists(className, fieldName, type) { + return this._schemaCollection().then(function (schemaCollection) { + return schemaCollection.addFieldIfNotExists(className, fieldName, type); + }); + } + + // Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.) + // and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible. + + }, { + key: 'deleteClass', + value: function deleteClass(className) { + var _this5 = this; + + return this._adaptiveCollection(className).then(function (collection) { + return collection.drop(); + }).catch(function (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(function () { + return _this5._schemaCollection(); + }).then(function (schemaCollection) { + return schemaCollection.findAndDeleteSchema(className); + }); + } + + // Delete all data known to this adatper. Used for testing. + + }, { + key: 'deleteAllClasses', + value: function deleteAllClasses() { + return storageAdapterAllCollections(this).then(function (collections) { + return Promise.all(collections.map(function (collection) { + return collection.drop(); + })); + }); + } + + // Remove the column and all the data. For Relations, the _Join collection is handled + // specially, this function does not delete _Join columns. It should, however, indicate + // that the relation fields does not exist anymore. In mongo, this means removing it from + // the _SCHEMA collection. There should be no actual data in the collection under the same name + // as the relation column, so it's fine to attempt to delete it. If the fields listed to be + // deleted do not exist, this function should return successfully anyways. Checking for + // attempts to delete non-existent fields is the responsibility of Parse Server. + + // Pointer field names are passed for legacy reasons: the original mongo + // format stored pointer field names differently in the database, and therefore + // needed to know the type of the field before it could delete it. Future database + // adatpers should ignore the pointerFieldNames argument. All the field names are in + // fieldNames, they show up additionally in the pointerFieldNames database for use + // by the mongo adapter, which deals with the legacy mongo format. + + // This function is not obligated to delete fields atomically. It is given the field + // names in a list so that databases that are capable of deleting fields atomically + // may do so. + + // Returns a Promise. + + }, { + key: 'deleteFields', + value: function deleteFields(className, schema, fieldNames) { + var _this6 = this; + + var mongoFormatNames = fieldNames.map(function (fieldName) { + if (schema.fields[fieldName].type === 'Pointer') { + return '_p_' + fieldName; + } else { + return fieldName; + } + }); + var collectionUpdate = { '$unset': {} }; + mongoFormatNames.forEach(function (name) { + collectionUpdate['$unset'][name] = null; + }); + + var schemaUpdate = { '$unset': {} }; + fieldNames.forEach(function (name) { + schemaUpdate['$unset'][name] = null; + }); + + return this._adaptiveCollection(className).then(function (collection) { + return collection.updateMany({}, collectionUpdate); + }).then(function () { + return _this6._schemaCollection(); + }).then(function (schemaCollection) { + return schemaCollection.updateSchema(className, schemaUpdate); + }); + } + + // Return a promise for all schemas known to this adapter, in Parse format. In case the + // schemas cannot be retrieved, returns a promise that rejects. Requirements for the + // rejection reason are TBD. + + }, { + key: 'getAllClasses', + value: function getAllClasses() { + return this._schemaCollection().then(function (schemasCollection) { + return schemasCollection._fetchAllSchemasFrom_SCHEMA(); + }); + } + + // Return a promise for the schema with the given name, in Parse format. If + // this adapter doesn't know about the schema, return a promise that rejects with + // undefined as the reason. + + }, { + key: 'getClass', + value: function getClass(className) { + return this._schemaCollection().then(function (schemasCollection) { + return schemasCollection._fechOneSchemaFrom_SCHEMA(className); + }); + } + + // TODO: As yet not particularly well specified. Creates an object. Maybe shouldn't even need the schema, + // and should infer from the type. Or maybe does need the schema for validations. Or maybe needs + // the schem only for the legacy mongo format. We'll figure that out later. + + }, { + key: 'createObject', + value: function createObject(className, schema, object) { + schema = convertParseSchemaToMongoSchema(schema); + var mongoObject = (0, _MongoTransform.parseObjectToMongoObjectForCreate)(className, object, schema); + return this._adaptiveCollection(className).then(function (collection) { + return collection.insertOne(mongoObject); + }).catch(function (error) { + if (error.code === 11000) { + // Duplicate value + throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + } + throw error; + }); + } + + // Remove all objects that match the given Parse Query. + // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined. + // If there is some other error, reject with INTERNAL_SERVER_ERROR. + + }, { + key: 'deleteObjectsByQuery', + value: function deleteObjectsByQuery(className, schema, query) { + schema = convertParseSchemaToMongoSchema(schema); + return this._adaptiveCollection(className).then(function (collection) { + var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); + return collection.deleteMany(mongoWhere); + }).then(function (_ref3) { + var result = _ref3.result; + + if (result.n === 0) { + throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } + return Promise.resolve(); + }, function () { + throw new _node2.default.Error(_node2.default.Error.INTERNAL_SERVER_ERROR, 'Database adapter error'); + }); + } + + // Apply the update to all objects that match the given Parse Query. + + }, { + key: 'updateObjectsByQuery', + value: function updateObjectsByQuery(className, schema, query, update) { + schema = convertParseSchemaToMongoSchema(schema); + var mongoUpdate = (0, _MongoTransform.transformUpdate)(className, update, schema); + var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); + return this._adaptiveCollection(className).then(function (collection) { + return collection.updateMany(mongoWhere, mongoUpdate); + }); + } + + // Atomically finds and updates an object based on query. + // Return value not currently well specified. + + }, { + key: 'findOneAndUpdate', + value: function findOneAndUpdate(className, schema, query, update) { + schema = convertParseSchemaToMongoSchema(schema); + var mongoUpdate = (0, _MongoTransform.transformUpdate)(className, update, schema); + var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); + return this._adaptiveCollection(className).then(function (collection) { + return collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, { new: true }); + }).then(function (result) { + return (0, _MongoTransform.mongoObjectToParseObject)(className, result.value, schema); + }); + } + + // Hopefully we can get rid of this. It's only used for config and hooks. + + }, { + key: 'upsertOneObject', + value: function upsertOneObject(className, schema, query, update) { + schema = convertParseSchemaToMongoSchema(schema); + var mongoUpdate = (0, _MongoTransform.transformUpdate)(className, update, schema); + var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); + return this._adaptiveCollection(className).then(function (collection) { + return collection.upsertOne(mongoWhere, mongoUpdate); + }); + } + + // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. + + }, { + key: 'find', + value: function find(className, schema, query, _ref4) { + var _this7 = this; + + var skip = _ref4.skip, + limit = _ref4.limit, + sort = _ref4.sort, + keys = _ref4.keys; + + schema = convertParseSchemaToMongoSchema(schema); + var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); + var mongoSort = _lodash2.default.mapKeys(sort, function (value, fieldName) { + return (0, _MongoTransform.transformKey)(className, fieldName, schema); + }); + var mongoKeys = _lodash2.default.reduce(keys, function (memo, key) { + memo[(0, _MongoTransform.transformKey)(className, key, schema)] = 1; + return memo; + }, {}); + return this._adaptiveCollection(className).then(function (collection) { + return collection.find(mongoWhere, { + skip: skip, + limit: limit, + sort: mongoSort, + keys: mongoKeys, + maxTimeMS: _this7._maxTimeMS + }); + }).then(function (objects) { + return objects.map(function (object) { + return (0, _MongoTransform.mongoObjectToParseObject)(className, object, schema); + }); + }); + } + + // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't + // currently know which fields are nullable and which aren't, we ignore that criteria. + // As such, we shouldn't expose this function to users of parse until we have an out-of-band + // Way of determining if a field is nullable. Undefined doesn't count against uniqueness, + // which is why we use sparse indexes. + + }, { + key: 'ensureUniqueness', + value: function ensureUniqueness(className, schema, fieldNames) { + schema = convertParseSchemaToMongoSchema(schema); + var indexCreationRequest = {}; + var mongoFieldNames = fieldNames.map(function (fieldName) { + return (0, _MongoTransform.transformKey)(className, fieldName, schema); + }); + mongoFieldNames.forEach(function (fieldName) { + indexCreationRequest[fieldName] = 1; + }); + return this._adaptiveCollection(className).then(function (collection) { + return collection._ensureSparseUniqueIndexInBackground(indexCreationRequest); + }).catch(function (error) { + if (error.code === 11000) { + throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'Tried to ensure field uniqueness for a class that already has duplicates.'); + } else { + throw error; + } + }); + } + + // Used in tests + + }, { + key: '_rawFind', + value: function _rawFind(className, query) { + var _this8 = this; + + return this._adaptiveCollection(className).then(function (collection) { + return collection.find(query, { + maxTimeMS: _this8._maxTimeMS + }); + }); + } + + // Executes a count. + + }, { + key: 'count', + value: function count(className, schema, query) { + var _this9 = this; + + schema = convertParseSchemaToMongoSchema(schema); + return this._adaptiveCollection(className).then(function (collection) { + return collection.count((0, _MongoTransform.transformWhere)(className, query, schema), { + maxTimeMS: _this9._maxTimeMS + }); + }); + } + }, { + key: 'performInitialization', + value: function performInitialization() { + return Promise.resolve(); + } + }]); + + return MongoStorageAdapter; +}(); + +exports.default = MongoStorageAdapter; + +module.exports = MongoStorageAdapter; // Required for tests \ No newline at end of file diff --git a/lib/Adapters/Storage/Mongo/MongoTransform.js b/lib/Adapters/Storage/Mongo/MongoTransform.js new file mode 100644 index 0000000000..7be1f31b25 --- /dev/null +++ b/lib/Adapters/Storage/Mongo/MongoTransform.js @@ -0,0 +1,1022 @@ +'use strict'; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _logger = require('../../../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var mongodb = require('mongodb'); +var Parse = require('parse/node').Parse; + +var transformKey = function transformKey(className, fieldName, schema) { + // Check if the schema is known since it's a built-in field. + switch (fieldName) { + case 'objectId': + return '_id'; + case 'createdAt': + return '_created_at'; + case 'updatedAt': + return '_updated_at'; + case 'sessionToken': + return '_session_token'; + } + + if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') { + fieldName = '_p_' + fieldName; + } else if (schema.fields[fieldName] && schema.fields[fieldName].type == 'Pointer') { + fieldName = '_p_' + fieldName; + } + + return fieldName; +}; + +var transformKeyValueForUpdate = function 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) + }; + } + 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 }; + } + + if (parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer' || !parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer') { + key = '_p_' + key; + } + + // Handle atomic values + var value = transformTopLevelAtom(restValue); + if (value !== CannotTransform) { + if (timeField && typeof value === 'string') { + value = new Date(value); + } + if (restKey.indexOf('.') > 0) { + return { key: key, value: restValue }; + } + return { key: key, value: value }; + } + + // Handle arrays + if (restValue instanceof Array) { + value = restValue.map(transformInteriorValue); + return { key: key, value: value }; + } + + // Handle update operators + if ((typeof restValue === 'undefined' ? 'undefined' : _typeof(restValue)) === 'object' && '__op' in restValue) { + return { key: key, value: transformUpdateOperator(restValue, false) }; + } + + // Handle normal objects by recursing + value = _lodash2.default.mapValues(restValue, transformInteriorValue); + return { key: key, value: value }; +}; + +var transformInteriorValue = function transformInteriorValue(restValue) { + if (restValue !== null && (typeof restValue === 'undefined' ? 'undefined' : _typeof(restValue)) === 'object' && Object.keys(restValue).some(function (key) { + return key.includes('$') || key.includes('.'); + })) { + throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + } + // Handle atomic values + var value = transformInteriorAtom(restValue); + if (value !== CannotTransform) { + return value; + } + + // Handle arrays + if (restValue instanceof Array) { + return restValue.map(transformInteriorValue); + } + + // Handle update operators + if ((typeof restValue === 'undefined' ? 'undefined' : _typeof(restValue)) === 'object' && '__op' in restValue) { + return transformUpdateOperator(restValue, true); + } + + // Handle normal objects by recursing + return _lodash2.default.mapValues(restValue, transformInteriorValue); +}; + +var valueAsDate = function valueAsDate(value) { + if (typeof value === 'string') { + return new Date(value); + } else if (value instanceof Date) { + return value; + } + return false; +}; + +function transformQueryKeyValue(className, key, value, schema) { + switch (key) { + case 'createdAt': + if (valueAsDate(value)) { + return { key: '_created_at', value: valueAsDate(value) }; + } + key = '_created_at'; + break; + case 'updatedAt': + if (valueAsDate(value)) { + return { key: '_updated_at', value: valueAsDate(value) }; + } + key = '_updated_at'; + break; + case 'expiresAt': + if (valueAsDate(value)) { + return { key: 'expiresAt', value: valueAsDate(value) }; + } + break; + case '_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: 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: key, value: value }; + case 'sessionToken': + return { key: '_session_token', value: 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: key, value: value }; + case '$or': + return { key: '$or', value: value.map(function (subQuery) { + return transformWhere(className, subQuery, schema); + }) }; + case '$and': + return { key: '$and', value: value.map(function (subQuery) { + return transformWhere(className, subQuery, schema); + }) }; + default: + { + // Other auth data + var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); + if (authDataMatch) { + var provider = authDataMatch[1]; + // Special-case auth data. + return { key: '_auth_data_' + provider + '.id', value: value }; + } + } + } + + var expectedTypeIsArray = schema && schema.fields[key] && schema.fields[key].type === 'Array'; + + var expectedTypeIsPointer = schema && schema.fields[key] && schema.fields[key].type === 'Pointer'; + + if (expectedTypeIsPointer || !schema && value && value.__type === 'Pointer') { + key = '_p_' + key; + } + + // Handle query constraints + var transformedConstraint = transformConstraint(value, expectedTypeIsArray); + if (transformedConstraint !== CannotTransform) { + return { key: key, value: transformedConstraint }; + } + + if (expectedTypeIsArray && !(value instanceof Array)) { + return { key: key, value: { '$all': [transformInteriorAtom(value)] } }; + } + + // Handle atomic values + if (transformTopLevelAtom(value) !== CannotTransform) { + return { key: key, value: transformTopLevelAtom(value) }; + } else { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'You cannot use ' + value + ' as a query parameter.'); + } +} + +// Main exposed method to help run queries. +// restWhere is the "where" clause in REST API form. +// Returns the mongo form of the query. +function transformWhere(className, restWhere, schema) { + var mongoWhere = {}; + for (var restKey in restWhere) { + var out = transformQueryKeyValue(className, restKey, restWhere[restKey], schema); + mongoWhere[out.key] = out.value; + } + return mongoWhere; +} + +var parseObjectKeyValueToMongoObjectKeyValue = function parseObjectKeyValueToMongoObjectKeyValue(restKey, restValue, schema) { + // Check if the schema is known since it's a built-in field. + var transformedValue = void 0; + var coercedToDate = void 0; + 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') { + restKey = '_p_' + restKey; + } + } + + // Handle atomic values + var value = transformTopLevelAtom(restValue); + if (value !== CannotTransform) { + return { key: restKey, value: value }; + } + + // ACLs are handled before this method is called + // If an ACL key still exists here, something is wrong. + if (restKey === 'ACL') { + throw 'There was a problem transforming an ACL.'; + } + + // Handle arrays + if (restValue instanceof Array) { + value = restValue.map(transformInteriorValue); + return { key: restKey, value: value }; + } + + // Handle normal objects by recursing + if (Object.keys(restValue).some(function (key) { + return key.includes('$') || key.includes('.'); + })) { + throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + } + value = _lodash2.default.mapValues(restValue, transformInteriorValue); + return { key: restKey, value: value }; +}; + +var parseObjectToMongoObjectForCreate = function parseObjectToMongoObjectForCreate(className, restCreate, schema) { + restCreate = addLegacyACL(restCreate); + var mongoCreate = {}; + for (var restKey in restCreate) { + if (restCreate[restKey] && restCreate[restKey].__type === 'Relation') { + continue; + } + + var _parseObjectKeyValueT = parseObjectKeyValueToMongoObjectKeyValue(restKey, restCreate[restKey], schema), + key = _parseObjectKeyValueT.key, + value = _parseObjectKeyValueT.value; + + if (value !== undefined) { + mongoCreate[key] = value; + } + } + + // Use the legacy mongo format for createdAt and updatedAt + if (mongoCreate.createdAt) { + mongoCreate._created_at = new Date(mongoCreate.createdAt.iso || mongoCreate.createdAt); + delete mongoCreate.createdAt; + } + if (mongoCreate.updatedAt) { + mongoCreate._updated_at = new Date(mongoCreate.updatedAt.iso || mongoCreate.updatedAt); + delete mongoCreate.updatedAt; + } + + return mongoCreate; +}; + +// Main exposed method to help update old objects. +var transformUpdate = function transformUpdate(className, restUpdate, parseFormatSchema) { + var mongoUpdate = {}; + var acl = addLegacyACL(restUpdate); + if (acl._rperm || acl._wperm || acl._acl) { + mongoUpdate.$set = {}; + if (acl._rperm) { + mongoUpdate.$set._rperm = acl._rperm; + } + if (acl._wperm) { + mongoUpdate.$set._wperm = acl._wperm; + } + if (acl._acl) { + mongoUpdate.$set._acl = acl._acl; + } + } + for (var restKey in restUpdate) { + if (restUpdate[restKey] && restUpdate[restKey].__type === 'Relation') { + continue; + } + var out = transformKeyValueForUpdate(className, restKey, restUpdate[restKey], parseFormatSchema); + + // If the output value is an object with any $ keys, it's an + // operator that needs to be lifted onto the top level update + // object. + if (_typeof(out.value) === 'object' && out.value !== null && out.value.__op) { + mongoUpdate[out.value.__op] = mongoUpdate[out.value.__op] || {}; + mongoUpdate[out.value.__op][out.key] = out.value.arg; + } else { + mongoUpdate['$set'] = mongoUpdate['$set'] || {}; + mongoUpdate['$set'][out.key] = out.value; + } + } + + return mongoUpdate; +}; + +// Add the legacy _acl format. +var addLegacyACL = function addLegacyACL(restObject) { + var restObjectCopy = _extends({}, restObject); + var _acl = {}; + + if (restObject._wperm) { + restObject._wperm.forEach(function (entry) { + _acl[entry] = { w: true }; + }); + restObjectCopy._acl = _acl; + } + + if (restObject._rperm) { + restObject._rperm.forEach(function (entry) { + if (!(entry in _acl)) { + _acl[entry] = { r: true }; + } else { + _acl[entry].r = true; + } + }); + restObjectCopy._acl = _acl; + } + + return restObjectCopy; +}; + +// A sentinel value that helper transformations return when they +// cannot perform a transformation +function CannotTransform() {} + +var transformInteriorAtom = function transformInteriorAtom(atom) { + // TODO: check validity harder for the __type-defined types + if ((typeof atom === 'undefined' ? 'undefined' : _typeof(atom)) === 'object' && atom && !(atom instanceof Date) && atom.__type === 'Pointer') { + return { + __type: 'Pointer', + className: atom.className, + objectId: atom.objectId + }; + } else if (typeof atom === 'function' || (typeof atom === 'undefined' ? 'undefined' : _typeof(atom)) === 'symbol') { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot transform value: ' + atom); + } else if (DateCoder.isValidJSON(atom)) { + return DateCoder.JSONToDatabase(atom); + } else if (BytesCoder.isValidJSON(atom)) { + return BytesCoder.JSONToDatabase(atom); + } else if ((typeof atom === 'undefined' ? 'undefined' : _typeof(atom)) === 'object' && atom && atom.$regex !== undefined) { + return new RegExp(atom.$regex); + } else { + return atom; + } +}; + +// Helper function to transform an atom from REST format to Mongo format. +// An atom is anything that can't contain other expressions. So it +// includes things where objects are used to represent other +// datatypes, like pointers and dates, but it does not include objects +// or arrays with generic stuff inside. +// Raises an error if this cannot possibly be valid REST format. +// Returns CannotTransform if it's just not an atom +function transformTopLevelAtom(atom) { + switch (typeof atom === 'undefined' ? 'undefined' : _typeof(atom)) { + case 'string': + case 'number': + case 'boolean': + return atom; + case 'undefined': + return atom; + case 'symbol': + case 'function': + throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot transform value: ' + atom); + case 'object': + if (atom instanceof Date) { + // Technically dates are not rest format, but, it seems pretty + // clear what they should be transformed to, so let's just do it. + return atom; + } + + if (atom === null) { + return atom; + } + + // TODO: check validity harder for the __type-defined types + if (atom.__type == 'Pointer') { + return atom.className + '$' + atom.objectId; + } + if (DateCoder.isValidJSON(atom)) { + return DateCoder.JSONToDatabase(atom); + } + if (BytesCoder.isValidJSON(atom)) { + return BytesCoder.JSONToDatabase(atom); + } + if (GeoPointCoder.isValidJSON(atom)) { + return GeoPointCoder.JSONToDatabase(atom); + } + if (FileCoder.isValidJSON(atom)) { + return FileCoder.JSONToDatabase(atom); + } + return CannotTransform; + + default: + // I don't think typeof can ever let us get here + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'really did not expect value: ' + atom); + } +} + +// Transforms a query constraint from REST API format to Mongo format. +// A constraint is something with fields like $lt. +// If it is not a valid constraint but it could be a valid something +// else, return CannotTransform. +// inArray is whether this is an array field. +function transformConstraint(constraint, inArray) { + if ((typeof constraint === 'undefined' ? 'undefined' : _typeof(constraint)) !== 'object' || !constraint) { + return CannotTransform; + } + var transformFunction = inArray ? transformInteriorAtom : transformTopLevelAtom; + var transformer = function transformer(atom) { + var result = transformFunction(atom); + if (result === CannotTransform) { + 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 answer = {}; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = keys[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var key = _step.value; + + switch (key) { + case '$lt': + case '$lte': + case '$gt': + case '$gte': + case '$exists': + case '$ne': + case '$eq': + answer[key] = transformer(constraint[key]); + break; + + case '$in': + case '$nin': + { + var arr = constraint[key]; + if (!(arr instanceof Array)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); + } + answer[key] = _lodash2.default.flatMap(arr, function (value) { + return function (atom) { + if (Array.isArray(atom)) { + return value.map(transformer); + } else { + return transformer(atom); + } + }(value); + }); + break; + } + case '$all': + { + var _arr = constraint[key]; + if (!(_arr instanceof Array)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); + } + answer[key] = _arr.map(transformInteriorAtom); + break; + } + case '$regex': + var s = constraint[key]; + if (typeof s !== 'string') { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad regex: ' + s); + } + answer[key] = s; + break; + + case '$options': + answer[key] = constraint[key]; + break; + + case '$nearSphere': + var point = constraint[key]; + answer[key] = [point.longitude, point.latitude]; + break; + + case '$maxDistance': + answer[key] = constraint[key]; + break; + + // The SDKs don't seem to use these but they are documented in the + // REST API docs. + case '$maxDistanceInRadians': + answer['$maxDistance'] = constraint[key]; + break; + case '$maxDistanceInMiles': + answer['$maxDistance'] = constraint[key] / 3959; + break; + case '$maxDistanceInKilometers': + answer['$maxDistance'] = constraint[key] / 6371; + break; + + case '$select': + case '$dontSelect': + throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'the ' + key + ' constraint is not supported yet'); + + case '$within': + var box = constraint[key]['$box']; + if (!box || box.length != 2) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'malformatted $within arg'); + } + answer[key] = { + '$box': [[box[0].longitude, box[0].latitude], [box[1].longitude, box[1].latitude]] + }; + break; + + default: + if (key.match(/^\$+/)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad constraint: ' + key); + } + return CannotTransform; + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return answer; +} + +// Transforms an update operator from REST format to mongo format. +// To be transformed, the input should have an __op field. +// If flatten is true, this will flatten operators to their static +// data format. For example, an increment of 2 would simply become a +// 2. +// The output for a non-flattened operator is a hash with __op being +// the mongo op, and arg being the argument. +// The output for a flattened operator is just a value. +// Returns undefined if this should be a no-op. + +function transformUpdateOperator(_ref, flatten) { + var __op = _ref.__op, + amount = _ref.amount, + objects = _ref.objects; + + switch (__op) { + case 'Delete': + if (flatten) { + return undefined; + } else { + return { __op: '$unset', arg: '' }; + } + + case 'Increment': + if (typeof amount !== 'number') { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'incrementing must provide a number'); + } + if (flatten) { + return amount; + } else { + return { __op: '$inc', arg: amount }; + } + + case 'Add': + case 'AddUnique': + if (!(objects instanceof Array)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); + } + var toAdd = objects.map(transformInteriorAtom); + if (flatten) { + return toAdd; + } else { + var mongoOp = { + Add: '$push', + AddUnique: '$addToSet' + }[__op]; + return { __op: mongoOp, arg: { '$each': toAdd } }; + } + + case 'Remove': + if (!(objects instanceof Array)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to remove must be an array'); + } + var toRemove = objects.map(transformInteriorAtom); + if (flatten) { + return []; + } else { + return { __op: '$pullAll', arg: toRemove }; + } + + default: + throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'The ' + __op + ' operator is not supported yet.'); + } +} + +var nestedMongoObjectToNestedParseObject = function nestedMongoObjectToNestedParseObject(mongoObject) { + switch (typeof mongoObject === 'undefined' ? 'undefined' : _typeof(mongoObject)) { + case 'string': + case 'number': + case 'boolean': + return mongoObject; + case 'undefined': + case 'symbol': + case 'function': + throw 'bad value in mongoObjectToParseObject'; + case 'object': + if (mongoObject === null) { + return null; + } + if (mongoObject instanceof Array) { + return mongoObject.map(nestedMongoObjectToNestedParseObject); + } + + if (mongoObject instanceof Date) { + return Parse._encode(mongoObject); + } + + if (mongoObject instanceof mongodb.Long) { + return mongoObject.toNumber(); + } + + if (mongoObject instanceof mongodb.Double) { + return mongoObject.value; + } + + if (BytesCoder.isValidDatabaseObject(mongoObject)) { + return BytesCoder.databaseToJSON(mongoObject); + } + + if (mongoObject.hasOwnProperty('__type') && mongoObject.__type == 'Date' && mongoObject.iso instanceof Date) { + mongoObject.iso = mongoObject.iso.toJSON(); + return mongoObject; + } + + return _lodash2.default.mapValues(mongoObject, nestedMongoObjectToNestedParseObject); + default: + throw 'unknown js type'; + } +}; + +// Converts from a mongo-format object to a REST-format object. +// Does not strip out anything based on a lack of authentication. +var mongoObjectToParseObject = function mongoObjectToParseObject(className, mongoObject, schema) { + switch (typeof mongoObject === 'undefined' ? 'undefined' : _typeof(mongoObject)) { + case 'string': + case 'number': + case 'boolean': + return mongoObject; + case 'undefined': + case 'symbol': + case 'function': + throw 'bad value in mongoObjectToParseObject'; + case 'object': + { + if (mongoObject === null) { + return null; + } + if (mongoObject instanceof Array) { + return mongoObject.map(nestedMongoObjectToNestedParseObject); + } + + if (mongoObject instanceof Date) { + return Parse._encode(mongoObject); + } + + if (mongoObject instanceof mongodb.Long) { + return mongoObject.toNumber(); + } + + if (mongoObject instanceof mongodb.Double) { + return mongoObject.value; + } + + if (BytesCoder.isValidDatabaseObject(mongoObject)) { + return BytesCoder.databaseToJSON(mongoObject); + } + + var restObject = {}; + if (mongoObject._rperm || mongoObject._wperm) { + restObject._rperm = mongoObject._rperm || []; + restObject._wperm = mongoObject._wperm || []; + delete mongoObject._rperm; + delete mongoObject._wperm; + } + + for (var key in mongoObject) { + switch (key) { + case '_id': + restObject['objectId'] = '' + mongoObject[key]; + break; + case '_hashed_password': + restObject._hashed_password = mongoObject[key]; + break; + case '_acl': + 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; + 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]) { + _logger2.default.info('transform.js', 'Found a pointer column not in the schema, dropping it.', className, newKey); + break; + } + if (schema.fields[newKey].type !== 'Pointer') { + _logger2.default.info('transform.js', 'Found a pointer in a non-pointer column, dropping it.', className, key); + break; + } + if (mongoObject[key] === null) { + break; + } + var objData = mongoObject[key].split('$'); + if (objData[0] !== schema.fields[newKey].targetClass) { + throw 'pointer to incorrect className'; + } + restObject[newKey] = { + __type: 'Pointer', + className: objData[0], + objectId: objData[1] + }; + break; + } else if (key[0] == '_' && key != '__type') { + throw 'bad key in untransform: ' + key; + } else { + var value = mongoObject[key]; + if (schema.fields[key] && schema.fields[key].type === 'File' && FileCoder.isValidDatabaseObject(value)) { + restObject[key] = FileCoder.databaseToJSON(value); + break; + } + if (schema.fields[key] && schema.fields[key].type === 'GeoPoint' && GeoPointCoder.isValidDatabaseObject(value)) { + restObject[key] = GeoPointCoder.databaseToJSON(value); + break; + } + if (schema.fields[key] && schema.fields[key].type === 'Bytes' && BytesCoder.isValidDatabaseObject(value)) { + restObject[key] = BytesCoder.databaseToJSON(value); + break; + } + } + restObject[key] = nestedMongoObjectToNestedParseObject(mongoObject[key]); + } + } + + var relationFieldNames = Object.keys(schema.fields).filter(function (fieldName) { + return schema.fields[fieldName].type === 'Relation'; + }); + var relationFields = {}; + relationFieldNames.forEach(function (relationFieldName) { + relationFields[relationFieldName] = { + __type: 'Relation', + className: schema.fields[relationFieldName].targetClass + }; + }); + + return _extends({}, restObject, relationFields); + } + default: + throw 'unknown js type'; + } +}; + +var DateCoder = { + JSONToDatabase: function JSONToDatabase(json) { + return new Date(json.iso); + }, + isValidJSON: function isValidJSON(value) { + return (typeof value === 'undefined' ? 'undefined' : _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}=)?$"), + isBase64Value: function isBase64Value(object) { + if (typeof object !== 'string') { + return false; + } + return this.base64Pattern.test(object); + }, + databaseToJSON: function databaseToJSON(object) { + var value = void 0; + if (this.isBase64Value(object)) { + value = object; + } else { + value = object.buffer.toString('base64'); + } + return { + __type: 'Bytes', + base64: value + }; + }, + isValidDatabaseObject: function isValidDatabaseObject(object) { + return object instanceof mongodb.Binary || this.isBase64Value(object); + }, + JSONToDatabase: function JSONToDatabase(json) { + return new mongodb.Binary(new Buffer(json.base64, 'base64')); + }, + isValidJSON: function isValidJSON(value) { + return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null && value.__type === 'Bytes'; + } +}; + +var GeoPointCoder = { + databaseToJSON: function databaseToJSON(object) { + return { + __type: 'GeoPoint', + latitude: object[1], + longitude: object[0] + }; + }, + isValidDatabaseObject: function isValidDatabaseObject(object) { + return object instanceof Array && object.length == 2; + }, + JSONToDatabase: function JSONToDatabase(json) { + return [json.longitude, json.latitude]; + }, + isValidJSON: function isValidJSON(value) { + return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null && value.__type === 'GeoPoint'; + } +}; + +var FileCoder = { + databaseToJSON: function databaseToJSON(object) { + return { + __type: 'File', + name: object + }; + }, + isValidDatabaseObject: function isValidDatabaseObject(object) { + return typeof object === 'string'; + }, + JSONToDatabase: function JSONToDatabase(json) { + return json.name; + }, + isValidJSON: function isValidJSON(value) { + return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null && value.__type === 'File'; + } +}; + +module.exports = { + transformKey: transformKey, + parseObjectToMongoObjectForCreate: parseObjectToMongoObjectForCreate, + transformUpdate: transformUpdate, + transformWhere: transformWhere, + mongoObjectToParseObject: mongoObjectToParseObject +}; \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/PostgresClient.js b/lib/Adapters/Storage/Postgres/PostgresClient.js new file mode 100644 index 0000000000..33324193fc --- /dev/null +++ b/lib/Adapters/Storage/Postgres/PostgresClient.js @@ -0,0 +1,33 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.createClient = createClient; + +var parser = require('./PostgresConfigParser'); + +function createClient(uri, databaseOptions) { + var dbOptions = {}; + databaseOptions = databaseOptions || {}; + + if (uri) { + dbOptions = parser.getDatabaseOptionsFromURI(uri); + } + + for (var key in databaseOptions) { + dbOptions[key] = databaseOptions[key]; + } + + var initOptions = dbOptions.initOptions || {}; + var pgp = require('pg-promise')(initOptions); + var client = pgp(dbOptions); + + if (dbOptions.pgOptions) { + for (var _key in dbOptions.pgOptions) { + pgp.pg.defaults[_key] = dbOptions.pgOptions[_key]; + } + } + + return { client: client, pgp: pgp }; +} \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/PostgresConfigParser.js b/lib/Adapters/Storage/Postgres/PostgresConfigParser.js new file mode 100644 index 0000000000..1d4e7a46a3 --- /dev/null +++ b/lib/Adapters/Storage/Postgres/PostgresConfigParser.js @@ -0,0 +1,46 @@ +'use strict'; + +var url = require('url'); + +function getDatabaseOptionsFromURI(uri) { + var databaseOptions = {}; + + var parsedURI = url.parse(uri); + var queryParams = parseQueryParams(parsedURI.query); + var authParts = parsedURI.auth ? parsedURI.auth.split(':') : []; + + databaseOptions.host = parsedURI.hostname || 'localhost'; + databaseOptions.port = parsedURI.port ? parseInt(parsedURI.port) : 5432; + databaseOptions.database = parsedURI.pathname ? parsedURI.pathname.substr(1) : undefined; + + databaseOptions.user = authParts.length > 0 ? authParts[0] : ''; + databaseOptions.password = authParts.length > 1 ? authParts[1] : ''; + + databaseOptions.ssl = queryParams.ssl && queryParams.ssl.toLowerCase() === 'true' ? true : false; + databaseOptions.binary = 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; + + if (queryParams.poolSize) { + databaseOptions.poolSize = parseInt(queryParams.poolSize) || 10; + } + + return databaseOptions; +} + +function parseQueryParams(queryString) { + queryString = queryString || ''; + + return queryString.split('&').reduce(function (p, c) { + var parts = c.split('='); + p[decodeURIComponent(parts[0])] = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : ''; + return p; + }, {}); +} + +module.exports = { + parseQueryParams: parseQueryParams, + getDatabaseOptionsFromURI: getDatabaseOptionsFromURI +}; \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/lib/Adapters/Storage/Postgres/PostgresStorageAdapter.js new file mode 100644 index 0000000000..92e92143ba --- /dev/null +++ b/lib/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -0,0 +1,1426 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PostgresStorageAdapter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _PostgresClient = require('./PostgresClient'); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _sql = require('./sql'); + +var _sql2 = _interopRequireDefault(_sql); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +var PostgresRelationDoesNotExistError = '42P01'; +var PostgresDuplicateRelationError = '42P07'; +var PostgresDuplicateColumnError = '42701'; +var PostgresDuplicateObjectError = '42710'; +var PostgresUniqueIndexViolationError = '23505'; +var PostgresTransactionAbortedError = '25P02'; +var logger = require('../../../logger'); + +var debug = function debug() { + var args = [].concat(Array.prototype.slice.call(arguments)); + args = ['PG: ' + arguments[0]].concat(args.slice(1, args.length)); + var log = logger.getLogger(); + log.debug.apply(log, args); +}; + +var parseTypeToPostgresType = function 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 'Array': + if (type.contents && type.contents.type === 'String') { + return 'text[]'; + } else { + return 'jsonb'; + } + default: + throw 'no type for ' + JSON.stringify(type) + ' yet'; + } +}; + +var ParseToPosgresComparator = { + '$gt': '>', + '$lt': '<', + '$gte': '>=', + '$lte': '<=' +}; + +var toPostgresValue = function toPostgresValue(value) { + if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') { + if (value.__type === 'Date') { + return value.iso; + } + if (value.__type === 'File') { + return value.name; + } + } + return value; +}; + +var transformValue = function transformValue(value) { + if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) == 'object' && value.__type === 'Pointer') { + return value.objectId; + } + return value; +}; + +// Duplicate from then mongo adapter... +var emptyCLPS = Object.freeze({ + find: {}, + get: {}, + create: {}, + update: {}, + delete: {}, + addField: {} +}); + +var defaultCLPS = Object.freeze({ + find: { '*': true }, + get: { '*': true }, + create: { '*': true }, + update: { '*': true }, + delete: { '*': true }, + addField: { '*': true } +}); + +var toParseSchema = function toParseSchema(schema) { + if (schema.className === '_User') { + delete schema.fields._hashed_password; + } + if (schema.fields) { + delete schema.fields._wperm; + delete schema.fields._rperm; + } + var clps = defaultCLPS; + if (schema.classLevelPermissions) { + clps = _extends({}, emptyCLPS, schema.classLevelPermissions); + } + return { + className: schema.className, + fields: schema.fields, + classLevelPermissions: clps + }; +}; + +var toPostgresSchema = function 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' } }; + if (schema.className === '_User') { + schema.fields._hashed_password = { type: 'String' }; + schema.fields._password_history = { type: 'Array' }; + } + return schema; +}; + +var handleDotFields = function handleDotFields(object) { + Object.keys(object).forEach(function (fieldName) { + if (fieldName.indexOf('.') > -1) { + var components = fieldName.split('.'); + var first = components.shift(); + object[first] = object[first] || {}; + var currentObj = object[first]; + var next = void 0; + var value = object[fieldName]; + if (value && value.__op === 'Delete') { + value = undefined; + } + /* eslint-disable no-cond-assign */ + while (next = components.shift()) { + /* eslint-enable no-cond-assign */ + currentObj[next] = currentObj[next] || {}; + if (components.length === 0) { + currentObj[next] = value; + } + currentObj = currentObj[next]; + } + delete object[fieldName]; + } + }); + return object; +}; + +var validateKeys = function validateKeys(object) { + if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) == 'object') { + for (var key in object) { + if (_typeof(object[key]) == 'object') { + validateKeys(object[key]); + } + + if (key.includes('$') || key.includes('.')) { + throw new _node2.default.Error(_node2.default.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + } + } + } +}; + +// Returns the list of join tables on a schema +var joinTablesForSchema = function joinTablesForSchema(schema) { + var list = []; + if (schema) { + Object.keys(schema.fields).forEach(function (field) { + if (schema.fields[field].type === 'Relation') { + list.push('_Join:' + field + ':' + schema.className); + } + }); + } + return list; +}; + +var buildWhereClause = function buildWhereClause(_ref) { + var schema = _ref.schema, + query = _ref.query, + index = _ref.index; + + var patterns = []; + var values = []; + var sorts = []; + + schema = toPostgresSchema(schema); + + var _loop = function _loop(fieldName) { + var isArrayField = schema.fields && schema.fields[fieldName] && schema.fields[fieldName].type === 'Array'; + var initialPatternsLength = patterns.length; + var fieldValue = query[fieldName]; + + // nothingin the schema, it's gonna blow up + if (!schema.fields[fieldName]) { + // as it won't exist + if (fieldValue.$exists === false) { + return 'continue'; + } + } + + if (fieldName.indexOf('.') >= 0) { + var components = fieldName.split('.').map(function (cmpt, index) { + if (index === 0) { + return '"' + cmpt + '"'; + } + return '\'' + cmpt + '\''; + }); + var name = components.slice(0, components.length - 1).join('->'); + name += '->>' + components[components.length - 1]; + patterns.push(name + ' = \'' + fieldValue + '\''); + } else if (typeof fieldValue === 'string') { + patterns.push('$' + index + ':name = $' + (index + 1)); + values.push(fieldName, fieldValue); + index += 2; + } else if (typeof fieldValue === 'boolean') { + patterns.push('$' + index + ':name = $' + (index + 1)); + values.push(fieldName, fieldValue); + index += 2; + } else if (typeof fieldValue === 'number') { + patterns.push('$' + index + ':name = $' + (index + 1)); + values.push(fieldName, fieldValue); + index += 2; + } else if (fieldName === '$or' || fieldName === '$and') { + var _values; + + var clauses = []; + var clauseValues = []; + fieldValue.forEach(function (subQuery) { + var clause = buildWhereClause({ schema: schema, query: subQuery, index: index }); + if (clause.pattern.length > 0) { + clauses.push(clause.pattern); + clauseValues.push.apply(clauseValues, _toConsumableArray(clause.values)); + index += clause.values.length; + } + }); + var orOrAnd = fieldName === '$or' ? ' OR ' : ' AND '; + patterns.push('(' + clauses.join(orOrAnd) + ')'); + (_values = values).push.apply(_values, clauseValues); + } + + if (fieldValue.$ne) { + if (isArrayField) { + fieldValue.$ne = JSON.stringify([fieldValue.$ne]); + patterns.push('NOT array_contains($' + index + ':name, $' + (index + 1) + ')'); + } else { + if (fieldValue.$ne === null) { + patterns.push('$' + index + ':name <> $' + (index + 1)); + } else { + // if not null, we need to manually exclude null + patterns.push('($' + index + ':name <> $' + (index + 1) + ' OR $' + index + ':name IS NULL)'); + } + } + + // TODO: support arrays + values.push(fieldName, fieldValue.$ne); + index += 2; + } + + if (fieldValue.$eq) { + patterns.push('$' + index + ':name = $' + (index + 1)); + values.push(fieldName, fieldValue.$eq); + index += 2; + } + var 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') { + var inPatterns = []; + var allowNull = false; + values.push(fieldName); + fieldValue.$in.forEach(function (listElem, listIndex) { + if (listElem === null) { + allowNull = true; + } else { + values.push(listElem); + inPatterns.push('$' + (index + 1 + listIndex - (allowNull ? 1 : 0))); + } + }); + if (allowNull) { + patterns.push('($' + index + ':name IS NULL OR $' + index + ':name && ARRAY[' + inPatterns.join(',') + '])'); + } else { + patterns.push('$' + index + ':name && ARRAY[' + inPatterns.join(',') + ']'); + } + index = index + 1 + inPatterns.length; + } else if (isInOrNin) { + createConstraint = function createConstraint(baseArray, notIn) { + if (baseArray.length > 0) { + var not = notIn ? ' NOT ' : ''; + if (isArrayField) { + patterns.push(not + ' array_contains($' + index + ':name, $' + (index + 1) + ')'); + values.push(fieldName, JSON.stringify(baseArray)); + index += 2; + } else { + var _inPatterns = []; + values.push(fieldName); + baseArray.forEach(function (listElem, listIndex) { + values.push(listElem); + _inPatterns.push('$' + (index + 1 + listIndex)); + }); + patterns.push('$' + index + ':name ' + not + ' IN (' + _inPatterns.join(',') + ')'); + index = index + 1 + _inPatterns.length; + } + } else if (!notIn) { + values.push(fieldName); + patterns.push('$' + index + ':name IS NULL'); + index = index + 1; + } + }; + + if (fieldValue.$in) { + createConstraint(_lodash2.default.flatMap(fieldValue.$in, function (elt) { + return elt; + }), false); + } + if (fieldValue.$nin) { + createConstraint(_lodash2.default.flatMap(fieldValue.$nin, function (elt) { + return elt; + }), true); + } + } + + if (Array.isArray(fieldValue.$all) && isArrayField) { + patterns.push('array_contains_all($' + index + ':name, $' + (index + 1) + '::jsonb)'); + values.push(fieldName, JSON.stringify(fieldValue.$all)); + index += 2; + } + + if (typeof fieldValue.$exists !== 'undefined') { + if (fieldValue.$exists) { + patterns.push('$' + index + ':name IS NOT NULL'); + } else { + patterns.push('$' + index + ':name IS NULL'); + } + values.push(fieldName); + index += 1; + } + + if (fieldValue.$nearSphere) { + var point = fieldValue.$nearSphere; + var distance = fieldValue.$maxDistance; + var distanceInKM = distance * 6371 * 1000; + patterns.push('ST_distance_sphere($' + index + ':name::geometry, POINT($' + (index + 1) + ', $' + (index + 2) + ')::geometry) <= $' + (index + 3)); + sorts.push('ST_distance_sphere($' + index + ':name::geometry, POINT($' + (index + 1) + ', $' + (index + 2) + ')::geometry) ASC'); + values.push(fieldName, point.longitude, point.latitude, distanceInKM); + index += 4; + } + + if (fieldValue.$within && fieldValue.$within.$box) { + var box = fieldValue.$within.$box; + var left = box[0].longitude; + var bottom = box[0].latitude; + var right = box[1].longitude; + var top = box[1].latitude; + + patterns.push('$' + index + ':name::point <@ $' + (index + 1) + '::box'); + values.push(fieldName, '((' + left + ', ' + bottom + '), (' + right + ', ' + top + '))'); + index += 2; + } + + if (fieldValue.$regex) { + var regex = fieldValue.$regex; + var operator = '~'; + var opts = fieldValue.$options; + if (opts) { + if (opts.indexOf('i') >= 0) { + operator = '~*'; + } + if (opts.indexOf('x') >= 0) { + regex = removeWhiteSpace(regex); + } + } + + regex = processRegexPattern(regex); + + patterns.push('$' + index + ':name ' + operator + ' \'$' + (index + 1) + ':raw\''); + values.push(fieldName, regex); + index += 2; + } + + if (fieldValue.__type === 'Pointer') { + if (isArrayField) { + patterns.push('array_contains($' + index + ':name, $' + (index + 1) + ')'); + values.push(fieldName, JSON.stringify([fieldValue])); + index += 2; + } else { + patterns.push('$' + index + ':name = $' + (index + 1)); + values.push(fieldName, fieldValue.objectId); + index += 2; + } + } + + if (fieldValue.__type === 'Date') { + patterns.push('$' + index + ':name = $' + (index + 1)); + values.push(fieldName, fieldValue.iso); + index += 2; + } + + Object.keys(ParseToPosgresComparator).forEach(function (cmp) { + if (fieldValue[cmp]) { + var pgComparator = ParseToPosgresComparator[cmp]; + patterns.push('$' + index + ':name ' + pgComparator + ' $' + (index + 1)); + values.push(fieldName, toPostgresValue(fieldValue[cmp])); + index += 2; + } + }); + + if (initialPatternsLength === patterns.length) { + throw new _node2.default.Error(_node2.default.Error.OPERATION_FORBIDDEN, 'Postgres doesn\'t support this query type yet ' + JSON.stringify(fieldValue)); + } + }; + + for (var fieldName in query) { + var createConstraint; + + var _ret = _loop(fieldName); + + if (_ret === 'continue') continue; + } + values = values.map(transformValue); + return { pattern: patterns.join(' AND '), values: values, sorts: sorts }; +}; + +var PostgresStorageAdapter = exports.PostgresStorageAdapter = function () { + function PostgresStorageAdapter(_ref2) { + var uri = _ref2.uri, + _ref2$collectionPrefi = _ref2.collectionPrefix, + collectionPrefix = _ref2$collectionPrefi === undefined ? '' : _ref2$collectionPrefi, + databaseOptions = _ref2.databaseOptions; + + _classCallCheck(this, PostgresStorageAdapter); + + this._collectionPrefix = collectionPrefix; + + var _createClient = (0, _PostgresClient.createClient)(uri, databaseOptions), + client = _createClient.client, + pgp = _createClient.pgp; + + this._client = client; + this._pgp = pgp; + } + // Private + + + _createClass(PostgresStorageAdapter, [{ + key: '_ensureSchemaCollectionExists', + value: function _ensureSchemaCollectionExists(conn) { + conn = conn || this._client; + return conn.none('CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )').catch(function (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; + } + }); + } + }, { + key: 'classExists', + value: function classExists(name) { + return this._client.one('SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1)', [name]).then(function (res) { + return res.exists; + }); + } + }, { + key: 'setClassLevelPermissions', + value: function setClassLevelPermissions(className, CLPs) { + var _this = this; + + return this._ensureSchemaCollectionExists().then(function () { + var values = [className, 'schema', 'classLevelPermissions', JSON.stringify(CLPs)]; + return _this._client.none('UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 ', values); + }); + } + }, { + key: 'createClass', + value: function createClass(className, schema) { + var _this2 = this; + + return this._client.tx(function (t) { + var q1 = _this2.createTable(className, schema, t); + var q2 = t.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($, $, true)', { className: className, schema: schema }); + + return t.batch([q1, q2]); + }).then(function () { + return toParseSchema(schema); + }).catch(function (err) { + if (Array.isArray(err.data) && err.data.length > 1 && err.data[0].result.code === PostgresTransactionAbortedError) { + err = err.data[1].result; + } + + if (err.code === PostgresUniqueIndexViolationError && err.detail.includes(className)) { + throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'Class ' + className + ' already exists.'); + } + throw err; + }); + } + + // Just create a table, do not insert in schema + + }, { + key: 'createTable', + value: function createTable(className, schema, conn) { + conn = conn || this._client; + debug('createTable', className, schema); + var valuesArray = []; + var patternsArray = []; + var 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' }; + } + var index = 2; + var relations = []; + Object.keys(fields).forEach(function (fieldName) { + var parseType = fields[fieldName]; + // Skip when it's a relation + // We'll create the tables later + if (parseType.type === 'Relation') { + relations.push(fieldName); + return; + } + if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) { + parseType.contents = { type: 'String' }; + } + valuesArray.push(fieldName); + valuesArray.push(parseTypeToPostgresType(parseType)); + patternsArray.push('$' + index + ':name $' + (index + 1) + ':raw'); + if (fieldName === 'objectId') { + patternsArray.push('PRIMARY KEY ($' + index + ':name)'); + } + index = index + 2; + }); + var qs = 'CREATE TABLE IF NOT EXISTS $1:name (' + patternsArray.join(',') + ')'; + var values = [className].concat(valuesArray); + return this._ensureSchemaCollectionExists(conn).then(function () { + return conn.none(qs, values); + }).catch(function (error) { + if (error.code === PostgresDuplicateRelationError) { + // Table already exists, must have been created by a different request. Ignore error. + } else { + throw error; + } + }).then(function () { + // Create the relation tables + return Promise.all(relations.map(function (fieldName) { + return conn.none('CREATE TABLE IF NOT EXISTS $ ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', { joinTable: '_Join:' + fieldName + ':' + className }); + })); + }); + } + }, { + key: 'addFieldIfNotExists', + value: function addFieldIfNotExists(className, fieldName, type) { + var _this3 = this; + + // TODO: Must be revised for invalid logic... + debug('addFieldIfNotExists', { className: className, fieldName: fieldName, type: type }); + return this._client.tx("addFieldIfNotExists", function (t) { + var promise = Promise.resolve(); + if (type.type !== 'Relation') { + promise = t.none('ALTER TABLE $ ADD COLUMN $ $', { + className: className, + fieldName: fieldName, + postgresType: parseTypeToPostgresType(type) + }).catch(function (error) { + if (error.code === PostgresRelationDoesNotExistError) { + return _this3.createClass(className, { fields: _defineProperty({}, fieldName, type) }); + } else if (error.code === PostgresDuplicateColumnError) { + // Column already exists, created by other request. Carry on to + // See if it's the right type. + } else { + throw error; + } + }); + } else { + promise = t.none('CREATE TABLE IF NOT EXISTS $ ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', { joinTable: '_Join:' + fieldName + ':' + className }); + } + return promise.then(function () { + return t.any('SELECT "schema" FROM "_SCHEMA" WHERE "className" = $ and ("schema"::json->\'fields\'->$) is not null', { className: className, fieldName: fieldName }); + }).then(function (result) { + if (result[0]) { + throw "Attempted to add a field that already exists"; + } else { + var path = '{fields,' + fieldName + '}'; + return t.none('UPDATE "_SCHEMA" SET "schema"=jsonb_set("schema", $, $) WHERE "className"=$', { path: path, type: type, className: className }); + } + }); + }); + } + + // Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.) + // and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible. + + }, { + key: 'deleteClass', + value: function deleteClass(className) { + var _this4 = this; + + return Promise.resolve().then(function () { + var operations = [['DROP TABLE IF EXISTS $1:name', [className]], ['DELETE FROM "_SCHEMA" WHERE "className"=$1', [className]]]; + return _this4._client.tx(function (t) { + return t.batch(operations.map(function (statement) { + return t.none(statement[0], statement[1]); + })); + }); + }).then(function () { + // resolves with false when _Join table + return className.indexOf('_Join:') != 0; + }); + } + + // Delete all data known to this adapter. Used for testing. + + }, { + key: 'deleteAllClasses', + value: function deleteAllClasses() { + var _this5 = this; + + var now = new Date().getTime(); + debug('deleteAllClasses'); + return this._client.any('SELECT * FROM "_SCHEMA"').then(function (results) { + var joins = results.reduce(function (list, schema) { + return list.concat(joinTablesForSchema(schema.schema)); + }, []); + var classes = ['_SCHEMA', '_PushStatus', '_JobStatus', '_Hooks', '_GlobalConfig'].concat(_toConsumableArray(results.map(function (result) { + return result.className; + })), _toConsumableArray(joins)); + return _this5._client.tx(function (t) { + return t.batch(classes.map(function (className) { + return t.none('DROP TABLE IF EXISTS $', { className: className }); + })); + }); + }, function (error) { + if (error.code === PostgresRelationDoesNotExistError) { + // No _SCHEMA collection. Don't delete anything. + return; + } else { + throw error; + } + }).then(function () { + debug('deleteAllClasses done in ' + (new Date().getTime() - now)); + }); + } + + // Remove the column and all the data. For Relations, the _Join collection is handled + // specially, this function does not delete _Join columns. It should, however, indicate + // that the relation fields does not exist anymore. In mongo, this means removing it from + // the _SCHEMA collection. There should be no actual data in the collection under the same name + // as the relation column, so it's fine to attempt to delete it. If the fields listed to be + // deleted do not exist, this function should return successfully anyways. Checking for + // attempts to delete non-existent fields is the responsibility of Parse Server. + + // This function is not obligated to delete fields atomically. It is given the field + // names in a list so that databases that are capable of deleting fields atomically + // may do so. + + // Returns a Promise. + + }, { + key: 'deleteFields', + value: function deleteFields(className, schema, fieldNames) { + var _this6 = this; + + debug('deleteFields', className, fieldNames); + return Promise.resolve().then(function () { + fieldNames = fieldNames.reduce(function (list, fieldName) { + var field = schema.fields[fieldName]; + if (field.type !== 'Relation') { + list.push(fieldName); + } + delete schema.fields[fieldName]; + return list; + }, []); + + var values = [className].concat(_toConsumableArray(fieldNames)); + var columns = fieldNames.map(function (name, idx) { + return '$' + (idx + 2) + ':name'; + }).join(', DROP COLUMN'); + + var doBatch = function doBatch(t) { + var batch = [t.none('UPDATE "_SCHEMA" SET "schema"=$ WHERE "className"=$', { schema: schema, className: className })]; + if (values.length > 1) { + batch.push(t.none('ALTER TABLE $1:name DROP COLUMN ' + columns, values)); + } + return batch; + }; + return _this6._client.tx(function (t) { + return t.batch(doBatch(t)); + }); + }); + } + + // Return a promise for all schemas known to this adapter, in Parse format. In case the + // schemas cannot be retrieved, returns a promise that rejects. Requirements for the + // rejection reason are TBD. + + }, { + key: 'getAllClasses', + value: function getAllClasses() { + var _this7 = this; + + return this._ensureSchemaCollectionExists().then(function () { + return _this7._client.map('SELECT * FROM "_SCHEMA"', null, function (row) { + return _extends({ className: row.className }, row.schema); + }); + }).then(function (res) { + return res.map(toParseSchema); + }); + } + + // Return a promise for the schema with the given name, in Parse format. If + // this adapter doesn't know about the schema, return a promise that rejects with + // undefined as the reason. + + }, { + key: 'getClass', + value: function getClass(className) { + debug('getClass', className); + return this._client.any('SELECT * FROM "_SCHEMA" WHERE "className"=$', { className: className }).then(function (result) { + if (result.length === 1) { + return result[0].schema; + } else { + throw undefined; + } + }).then(toParseSchema); + } + + // TODO: remove the mongo format dependency in the return value + + }, { + key: 'createObject', + value: function createObject(className, schema, object) { + debug('createObject', className, object); + var columnsArray = []; + var valuesArray = []; + schema = toPostgresSchema(schema); + var geoPoints = {}; + + object = handleDotFields(object); + + validateKeys(object); + + Object.keys(object).forEach(function (fieldName) { + var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/); + if (authDataMatch) { + var provider = authDataMatch[1]; + object['authData'] = object['authData'] || {}; + object['authData'][provider] = object[fieldName]; + delete object[fieldName]; + fieldName = 'authData'; + } + + columnsArray.push(fieldName); + if (!schema.fields[fieldName] && className === '_User') { + if (fieldName === '_email_verify_token' || fieldName === '_failed_login_count' || fieldName === '_perishable_token' || fieldName === '_password_history') { + valuesArray.push(object[fieldName]); + } + + if (fieldName === '_email_verify_token_expires_at') { + if (object[fieldName]) { + valuesArray.push(object[fieldName].iso); + } else { + valuesArray.push(null); + } + } + + if (fieldName === '_account_lockout_expires_at' || fieldName === '_perishable_token_expires_at' || fieldName === '_password_changed_at') { + if (object[fieldName]) { + valuesArray.push(object[fieldName].iso); + } else { + valuesArray.push(null); + } + } + 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) { + valuesArray.push(object[fieldName]); + } else { + valuesArray.push(JSON.stringify(object[fieldName])); + } + break; + case 'Object': + case 'String': + case 'Number': + case 'Boolean': + valuesArray.push(object[fieldName]); + break; + case 'File': + valuesArray.push(object[fieldName].name); + 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'; + } + }); + + columnsArray = columnsArray.concat(Object.keys(geoPoints)); + var initialValues = valuesArray.map(function (val, index) { + var termination = ''; + var fieldName = columnsArray[index]; + if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) { + termination = '::text[]'; + } else if (schema.fields[fieldName] && schema.fields[fieldName].type === 'Array') { + termination = '::jsonb'; + } + return '$' + (index + 2 + columnsArray.length) + termination; + }); + var geoPointsInjects = Object.keys(geoPoints).map(function (key) { + var value = geoPoints[key]; + valuesArray.push(value.longitude, value.latitude); + var l = valuesArray.length + columnsArray.length; + return 'POINT($' + l + ', $' + (l + 1) + ')'; + }); + + var columnsPattern = columnsArray.map(function (col, index) { + return '$' + (index + 2) + ':name'; + }).join(','); + var valuesPattern = initialValues.concat(geoPointsInjects).join(','); + + var qs = 'INSERT INTO $1:name (' + columnsPattern + ') VALUES (' + valuesPattern + ')'; + var values = [className].concat(_toConsumableArray(columnsArray), valuesArray); + debug(qs, values); + return this._client.any(qs, values).then(function () { + return { ops: [object] }; + }).catch(function (error) { + if (error.code === PostgresUniqueIndexViolationError) { + throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + } else { + throw error; + } + }); + } + + // Remove all objects that match the given Parse Query. + // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined. + // If there is some other error, reject with INTERNAL_SERVER_ERROR. + + }, { + key: 'deleteObjectsByQuery', + value: function deleteObjectsByQuery(className, schema, query) { + debug('deleteObjectsByQuery', className, query); + var values = [className]; + var index = 2; + var where = buildWhereClause({ schema: schema, index: index, query: query }); + values.push.apply(values, _toConsumableArray(where.values)); + if (Object.keys(query).length === 0) { + where.pattern = 'TRUE'; + } + var 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, function (a) { + return +a.count; + }).then(function (count) { + if (count === 0) { + throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } else { + return count; + } + }); + } + // Return value not currently well specified. + + }, { + key: 'findOneAndUpdate', + value: function findOneAndUpdate(className, schema, query, update) { + debug('findOneAndUpdate', className, query, update); + return this.updateObjectsByQuery(className, schema, query, update).then(function (val) { + return val[0]; + }); + } + + // Apply the update to all objects that match the given Parse Query. + + }, { + key: 'updateObjectsByQuery', + value: function updateObjectsByQuery(className, schema, query, update) { + debug('updateObjectsByQuery', className, query, update); + var updatePatterns = []; + var values = [className]; + var index = 2; + schema = toPostgresSchema(schema); + + var originalUpdate = _extends({}, update); + update = handleDotFields(update); + // Resolve authData first, + // So we don't end up with multiple key updates + for (var fieldName in update) { + var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/); + if (authDataMatch) { + var provider = authDataMatch[1]; + var value = update[fieldName]; + delete update[fieldName]; + update['authData'] = update['authData'] || {}; + update['authData'][provider] = value; + } + } + + var _loop2 = function _loop2(_fieldName) { + var fieldValue = update[_fieldName]; + if (fieldValue === null) { + updatePatterns.push('$' + index + ':name = NULL'); + values.push(_fieldName); + index += 1; + } else if (_fieldName == 'authData') { + // This recursively sets the json_object + // Only 1 level deep + var generate = function generate(jsonb, key, value) { + return 'json_object_set_key(COALESCE(' + jsonb + ', \'{}\'::jsonb), ' + key + ', ' + value + ')::jsonb'; + }; + var lastKey = '$' + index + ':name'; + var fieldNameIndex = index; + index += 1; + values.push(_fieldName); + var _update = Object.keys(fieldValue).reduce(function (lastKey, key) { + var str = generate(lastKey, '$' + index + '::text', '$' + (index + 1) + '::jsonb'); + index += 2; + var value = fieldValue[key]; + if (value) { + if (value.__op === 'Delete') { + value = null; + } else { + value = JSON.stringify(value); + } + } + 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)); + 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)'); + values.push(_fieldName, JSON.stringify(fieldValue.objects)); + index += 2; + } else if (fieldValue.__op === 'Delete') { + 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)'); + 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)'); + 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)); + values.push(_fieldName, fieldValue); + index += 2; + } else if (typeof fieldValue === 'string') { + updatePatterns.push('$' + index + ':name = $' + (index + 1)); + values.push(_fieldName, fieldValue); + index += 2; + } else if (typeof fieldValue === 'boolean') { + updatePatterns.push('$' + index + ':name = $' + (index + 1)); + values.push(_fieldName, fieldValue); + index += 2; + } else if (fieldValue.__type === 'Pointer') { + updatePatterns.push('$' + index + ':name = $' + (index + 1)); + values.push(_fieldName, fieldValue.objectId); + index += 2; + } else if (fieldValue.__type === 'Date') { + updatePatterns.push('$' + index + ':name = $' + (index + 1)); + values.push(_fieldName, toPostgresValue(fieldValue)); + index += 2; + } else if (fieldValue instanceof Date) { + updatePatterns.push('$' + index + ':name = $' + (index + 1)); + values.push(_fieldName, fieldValue); + index += 2; + } else if (fieldValue.__type === 'File') { + updatePatterns.push('$' + index + ':name = $' + (index + 1)); + values.push(_fieldName, toPostgresValue(fieldValue)); + index += 2; + } else if (fieldValue.__type === 'GeoPoint') { + updatePatterns.push('$' + index + ':name = POINT($' + (index + 1) + ', $' + (index + 2) + ')'); + values.push(_fieldName, fieldValue.latitude, fieldValue.longitude); + index += 3; + } else if (fieldValue.__type === 'Relation') { + // noop + } else if (typeof fieldValue === 'number') { + updatePatterns.push('$' + index + ':name = $' + (index + 1)); + values.push(_fieldName, fieldValue); + index += 2; + } else if ((typeof fieldValue === 'undefined' ? 'undefined' : _typeof(fieldValue)) === 'object' && schema.fields[_fieldName] && schema.fields[_fieldName].type === 'Object') { + // Gather keys to increment + var keysToIncrement = Object.keys(originalUpdate).filter(function (k) { + // choose top level fields that have a delete operation set + return originalUpdate[k].__op === 'Increment' && k.split('.').length === 2 && k.split(".")[0] === _fieldName; + }).map(function (k) { + return k.split('.')[1]; + }); + + var incrementPatterns = ''; + if (keysToIncrement.length > 0) { + incrementPatterns = ' || ' + keysToIncrement.map(function (c) { + var amount = fieldValue[c].amount; + return 'CONCAT(\'{"' + c + '":\', COALESCE($' + index + ':name->>\'' + c + '\',\'0\')::int + ' + amount + ', \'}\')::jsonb'; + }).join(' || '); + // Strip the keys + keysToIncrement.forEach(function (key) { + delete fieldValue[key]; + }); + } + + var keysToDelete = Object.keys(originalUpdate).filter(function (k) { + // choose top level fields that have a delete operation set + return originalUpdate[k].__op === 'Delete' && k.split('.').length === 2 && k.split(".")[0] === _fieldName; + }).map(function (k) { + return k.split('.')[1]; + }); + + var deletePatterns = keysToDelete.reduce(function (p, c, i) { + return p + (' - \'$' + (index + 1 + i) + ':value\''); + }, ''); + + updatePatterns.push('$' + index + ':name = ( COALESCE($' + index + ':name, \'{}\'::jsonb) ' + deletePatterns + ' ' + incrementPatterns + ' || $' + (index + 1 + keysToDelete.length) + '::jsonb )'); + + values.push.apply(values, [_fieldName].concat(_toConsumableArray(keysToDelete), [JSON.stringify(fieldValue)])); + index += 2 + keysToDelete.length; + } else if (Array.isArray(fieldValue) && schema.fields[_fieldName] && schema.fields[_fieldName].type === 'Array') { + var expectedType = parseTypeToPostgresType(schema.fields[_fieldName]); + if (expectedType === 'text[]') { + updatePatterns.push('$' + index + ':name = $' + (index + 1) + '::text[]'); + } else { + var type = 'text'; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = fieldValue[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var elt = _step.value; + + if ((typeof elt === 'undefined' ? 'undefined' : _typeof(elt)) == 'object') { + type = 'json'; + break; + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + 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 { + v: Promise.reject(new _node2.default.Error(_node2.default.Error.OPERATION_FORBIDDEN, 'Postgres doesn\'t support update ' + JSON.stringify(fieldValue) + ' yet')) + }; + } + }; + + for (var _fieldName in update) { + var _ret2 = _loop2(_fieldName); + + if ((typeof _ret2 === 'undefined' ? 'undefined' : _typeof(_ret2)) === "object") return _ret2.v; + } + + var where = buildWhereClause({ schema: schema, index: index, query: query }); + values.push.apply(values, _toConsumableArray(where.values)); + + var qs = 'UPDATE $1:name SET ' + updatePatterns.join(',') + ' WHERE ' + where.pattern + ' RETURNING *'; + debug('update: ', qs, values); + return this._client.any(qs, values); // TODO: This is unsafe, verification is needed, or a different query method; + } + + // Hopefully, we can get rid of this. It's only used for config and hooks. + + }, { + key: 'upsertOneObject', + value: function upsertOneObject(className, schema, query, update) { + var _this8 = this; + + debug('upsertOneObject', { className: className, query: query, update: update }); + var createValue = Object.assign({}, query, update); + return this.createObject(className, schema, createValue).catch(function (err) { + // ignore duplicate value errors as it's upsert + if (err.code === _node2.default.Error.DUPLICATE_VALUE) { + return _this8.findOneAndUpdate(className, schema, query, update); + } + throw err; + }); + } + }, { + key: 'find', + value: function find(className, schema, query, _ref3) { + var _values2; + + var skip = _ref3.skip, + limit = _ref3.limit, + sort = _ref3.sort, + keys = _ref3.keys; + + debug('find', className, query, { skip: skip, limit: limit, sort: sort, keys: keys }); + var hasLimit = limit !== undefined; + var hasSkip = skip !== undefined; + var values = [className]; + var where = buildWhereClause({ schema: schema, query: query, index: 2 }); + (_values2 = values).push.apply(_values2, _toConsumableArray(where.values)); + + var wherePattern = where.pattern.length > 0 ? 'WHERE ' + where.pattern : ''; + var limitPattern = hasLimit ? 'LIMIT $' + (values.length + 1) : ''; + if (hasLimit) { + values.push(limit); + } + var skipPattern = hasSkip ? 'OFFSET $' + (values.length + 1) : ''; + if (hasSkip) { + values.push(skip); + } + + var sortPattern = ''; + if (sort) { + var sorting = Object.keys(sort).map(function (key) { + // Using $idx pattern gives: non-integer constant in ORDER BY + if (sort[key] === 1) { + return '"' + key + '" ASC'; + } + return '"' + key + '" DESC'; + }).join(','); + sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? 'ORDER BY ' + sorting : ''; + } + if (where.sorts && Object.keys(where.sorts).length > 0) { + sortPattern = 'ORDER BY ' + where.sorts.join(','); + } + + var columns = '*'; + if (keys) { + // Exclude empty keys + keys = keys.filter(function (key) { + return key.length > 0; + }); + columns = keys.map(function (key, index) { + return '$' + (index + values.length + 1) + ':name'; + }).join(','); + values = values.concat(keys); + } + + var qs = 'SELECT ' + columns + ' FROM $1:name ' + wherePattern + ' ' + sortPattern + ' ' + limitPattern + ' ' + skipPattern; + debug(qs, values); + return this._client.any(qs, values).catch(function (err) { + // Query on non existing table, don't crash + if (err.code === PostgresRelationDoesNotExistError) { + return []; + } + return Promise.reject(err); + }).then(function (results) { + return results.map(function (object) { + Object.keys(schema.fields).forEach(function (fieldName) { + if (schema.fields[fieldName].type === 'Pointer' && object[fieldName]) { + 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 + }; + } + if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') { + object[fieldName] = { + __type: "GeoPoint", + latitude: object[fieldName].y, + longitude: object[fieldName].x + }; + } + if (object[fieldName] && schema.fields[fieldName].type === 'File') { + object[fieldName] = { + __type: 'File', + 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. + if (object.createdAt) { + object.createdAt = object.createdAt.toISOString(); + } + if (object.updatedAt) { + object.updatedAt = object.updatedAt.toISOString(); + } + if (object.expiresAt) { + object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() }; + } + if (object._email_verify_token_expires_at) { + 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() }; + } + if (object._perishable_token_expires_at) { + 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() }; + } + + for (var fieldName in object) { + if (object[fieldName] === null) { + delete object[fieldName]; + } + if (object[fieldName] instanceof Date) { + object[fieldName] = { __type: 'Date', iso: object[fieldName].toISOString() }; + } + } + + return object; + }); + }); + } + + // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't + // currently know which fields are nullable and which aren't, we ignore that criteria. + // As such, we shouldn't expose this function to users of parse until we have an out-of-band + // Way of determining if a field is nullable. Undefined doesn't count against uniqueness, + // which is why we use sparse indexes. + + }, { + key: 'ensureUniqueness', + value: function ensureUniqueness(className, schema, fieldNames) { + // Use the same name for every ensureUniqueness attempt, because postgres + // Will happily create the same index with multiple names. + var constraintName = 'unique_' + fieldNames.sort().join('_'); + var constraintPatterns = fieldNames.map(function (fieldName, index) { + return '$' + (index + 3) + ':name'; + }); + var qs = 'ALTER TABLE $1:name ADD CONSTRAINT $2:name UNIQUE (' + constraintPatterns.join(',') + ')'; + return this._client.none(qs, [className, constraintName].concat(_toConsumableArray(fieldNames))).catch(function (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 _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + } else { + throw error; + } + }); + } + + // Executes a count. + + }, { + key: 'count', + value: function count(className, schema, query) { + debug('count', className, query); + var values = [className]; + var where = buildWhereClause({ schema: schema, query: query, index: 2 }); + values.push.apply(values, _toConsumableArray(where.values)); + + var wherePattern = where.pattern.length > 0 ? 'WHERE ' + where.pattern : ''; + var qs = 'SELECT count(*) FROM $1:name ' + wherePattern; + return this._client.one(qs, values, function (a) { + return +a.count; + }).catch(function (err) { + if (err.code === PostgresRelationDoesNotExistError) { + return 0; + } + throw err; + }); + } + }, { + key: 'performInitialization', + value: function performInitialization(_ref4) { + var _this9 = this; + + var VolatileClassesSchemas = _ref4.VolatileClassesSchemas; + + debug('performInitialization'); + var promises = VolatileClassesSchemas.map(function (schema) { + return _this9.createTable(schema.className, schema).catch(function (err) { + if (err.code === PostgresDuplicateRelationError || err.code === _node2.default.Error.INVALID_CLASS_NAME) { + return Promise.resolve(); + } + throw err; + }); + }); + return Promise.all(promises).then(function () { + return _this9._client.tx(function (t) { + return t.batch([t.none(_sql2.default.misc.jsonObjectSetKeys), t.none(_sql2.default.array.add), t.none(_sql2.default.array.addUnique), t.none(_sql2.default.array.remove), t.none(_sql2.default.array.containsAll), t.none(_sql2.default.array.contains)]); + }); + }).then(function (data) { + debug('initializationDone in ' + data.duration); + }).catch(function (error) { + /* eslint-disable no-console */ + console.error(error); + }); + } + }]); + + return PostgresStorageAdapter; +}(); + +function removeWhiteSpace(regex) { + 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(); +} + +function processRegexPattern(s) { + 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)) + '$'; + } + + // regex for contains + return literalizeRegexPart(s); +} + +function createLiteralRegex(remaining) { + return remaining.split('').map(function (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) { + var matcher1 = /\\Q((?!\\E).*)\\E$/; + var result1 = s.match(matcher1); + if (result1 && result1.length > 1 && result1.index > -1) { + // process regex that has a beginning and an end specified for the literal text + var prefix = s.substr(0, result1.index); + var remaining = result1[1]; + + return literalizeRegexPart(prefix) + createLiteralRegex(remaining); + } + + // process regex that has a beginning specified for the literal text + var matcher2 = /\\Q((?!\\E).*)$/; + var result2 = s.match(matcher2); + if (result2 && result2.length > 1 && result2.index > -1) { + var _prefix = s.substr(0, result2.index); + var _remaining = result2[1]; + + return literalizeRegexPart(_prefix) + createLiteralRegex(_remaining); + } + + // 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'); +} + +exports.default = PostgresStorageAdapter; + +module.exports = PostgresStorageAdapter; // Required for tests \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/sql/array/add-unique.sql b/lib/Adapters/Storage/Postgres/sql/array/add-unique.sql new file mode 100644 index 0000000000..aad90d45f5 --- /dev/null +++ b/lib/Adapters/Storage/Postgres/sql/array/add-unique.sql @@ -0,0 +1,11 @@ +CREATE OR REPLACE FUNCTION array_add_unique( + "array" jsonb, + "values" jsonb +) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE + STRICT +AS $function$ + SELECT array_to_json(ARRAY(SELECT DISTINCT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT DISTINCT jsonb_array_elements("values")))))::jsonb; +$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/array/add.sql b/lib/Adapters/Storage/Postgres/sql/array/add.sql new file mode 100644 index 0000000000..a0b5859908 --- /dev/null +++ b/lib/Adapters/Storage/Postgres/sql/array/add.sql @@ -0,0 +1,11 @@ +CREATE OR REPLACE FUNCTION array_add( + "array" jsonb, + "values" jsonb +) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE + STRICT +AS $function$ + SELECT array_to_json(ARRAY(SELECT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT jsonb_array_elements("values")))))::jsonb; +$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/array/contains-all.sql b/lib/Adapters/Storage/Postgres/sql/array/contains-all.sql new file mode 100644 index 0000000000..24355bc732 --- /dev/null +++ b/lib/Adapters/Storage/Postgres/sql/array/contains-all.sql @@ -0,0 +1,11 @@ +CREATE OR REPLACE FUNCTION array_contains_all( + "array" jsonb, + "values" jsonb +) + RETURNS boolean + LANGUAGE sql + IMMUTABLE + STRICT +AS $function$ + SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES; +$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/array/contains.sql b/lib/Adapters/Storage/Postgres/sql/array/contains.sql new file mode 100644 index 0000000000..f7c458782e --- /dev/null +++ b/lib/Adapters/Storage/Postgres/sql/array/contains.sql @@ -0,0 +1,11 @@ +CREATE OR REPLACE FUNCTION array_contains( + "array" jsonb, + "values" jsonb +) + RETURNS boolean + LANGUAGE sql + IMMUTABLE + STRICT +AS $function$ + SELECT RES.CNT >= 1 FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES; +$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/array/remove.sql b/lib/Adapters/Storage/Postgres/sql/array/remove.sql new file mode 100644 index 0000000000..52895d2f46 --- /dev/null +++ b/lib/Adapters/Storage/Postgres/sql/array/remove.sql @@ -0,0 +1,11 @@ +CREATE OR REPLACE FUNCTION array_remove( + "array" jsonb, + "values" jsonb +) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE + STRICT +AS $function$ + SELECT array_to_json(ARRAY(SELECT * FROM jsonb_array_elements("array") as elt WHERE elt NOT IN (SELECT * FROM (SELECT jsonb_array_elements("values")) AS sub)))::jsonb; +$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/index.js b/lib/Adapters/Storage/Postgres/sql/index.js new file mode 100644 index 0000000000..c1b31b93ac --- /dev/null +++ b/lib/Adapters/Storage/Postgres/sql/index.js @@ -0,0 +1,32 @@ +'use strict'; + +var QueryFile = require('pg-promise').QueryFile; +var path = require('path'); + +module.exports = { + array: { + add: sql('array/add.sql'), + addUnique: sql('array/add-unique.sql'), + contains: sql('array/contains.sql'), + containsAll: sql('array/contains-all.sql'), + remove: sql('array/remove.sql') + }, + misc: { + jsonObjectSetKeys: sql('misc/json-object-set-keys.sql') + } +}; + +/////////////////////////////////////////////// +// Helper for linking to external query files; +function sql(file) { + + var fullPath = path.join(__dirname, file); // generating full path; + + var qf = new QueryFile(fullPath, { minify: true }); + + if (qf.error) { + throw qf.error; + } + + return qf; +} \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/sql/misc/json-object-set-keys.sql b/lib/Adapters/Storage/Postgres/sql/misc/json-object-set-keys.sql new file mode 100644 index 0000000000..eb28b36928 --- /dev/null +++ b/lib/Adapters/Storage/Postgres/sql/misc/json-object-set-keys.sql @@ -0,0 +1,19 @@ +-- Function to set a key on a nested JSON document + +CREATE OR REPLACE FUNCTION json_object_set_key( + "json" jsonb, + key_to_set TEXT, + value_to_set anyelement +) + RETURNS jsonb + LANGUAGE sql + IMMUTABLE + STRICT +AS $function$ +SELECT concat('{', string_agg(to_json("key") || ':' || "value", ','), '}')::jsonb + FROM (SELECT * + FROM jsonb_each("json") + WHERE key <> key_to_set + UNION ALL + SELECT key_to_set, to_json("value_to_set")::jsonb) AS fields +$function$; diff --git a/lib/Auth.js b/lib/Auth.js new file mode 100644 index 0000000000..ccbd777f90 --- /dev/null +++ b/lib/Auth.js @@ -0,0 +1,241 @@ +'use strict'; + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +var Parse = require('parse/node').Parse; +var RestQuery = require('./RestQuery'); + +// 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() { + var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + config = _ref.config, + _ref$isMaster = _ref.isMaster, + isMaster = _ref$isMaster === undefined ? false : _ref$isMaster, + user = _ref.user, + installationId = _ref.installationId; + + this.config = config; + this.installationId = installationId; + this.isMaster = isMaster; + this.user = user; + + // Assuming a users roles won't change during a single request, we'll + // only load them once. + this.userRoles = []; + this.fetchedRoles = false; + this.rolePromise = null; +} + +// Whether this auth could possibly modify the given user id. +// It still could be forbidden via ACLs even if this returns true. +Auth.prototype.couldUpdateUserId = function (userId) { + if (this.isMaster) { + return true; + } + if (this.user && this.user.id === userId) { + return true; + } + return false; +}; + +// A helper to get a master-level Auth object +function master(config) { + return new Auth({ config: config, isMaster: true }); +} + +// A helper to get a nobody-level Auth object +function nobody(config) { + return new Auth({ config: config, isMaster: false }); +} + +// Returns a promise that resolves to an Auth object +var getAuthForSessionToken = function getAuthForSessionToken() { + var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + config = _ref2.config, + sessionToken = _ref2.sessionToken, + installationId = _ref2.installationId; + + return config.cacheController.user.get(sessionToken).then(function (userJSON) { + if (userJSON) { + var cachedUser = Parse.Object.fromJSON(userJSON); + return Promise.resolve(new Auth({ config: config, isMaster: false, installationId: installationId, user: cachedUser })); + } + + var restOptions = { + limit: 1, + include: 'user' + }; + + var query = new RestQuery(config, master(config), '_Session', { sessionToken: sessionToken }, restOptions); + return query.execute().then(function (response) { + var results = response.results; + if (results.length !== 1 || !results[0]['user']) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid session token'); + } + + var now = new Date(), + expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined; + if (expiresAt < now) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.'); + } + var obj = results[0]['user']; + delete obj.password; + obj['className'] = '_User'; + obj['sessionToken'] = sessionToken; + config.cacheController.user.put(sessionToken, obj); + var userObject = Parse.Object.fromJSON(obj); + return new Auth({ config: config, isMaster: false, installationId: installationId, user: userObject }); + }); + }); +}; + +var getAuthForLegacySessionToken = function getAuthForLegacySessionToken() { + var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, + config = _ref3.config, + sessionToken = _ref3.sessionToken, + installationId = _ref3.installationId; + + var restOptions = { + limit: 1 + }; + var query = new RestQuery(config, master(config), '_User', { sessionToken: sessionToken }, restOptions); + return query.execute().then(function (response) { + var results = response.results; + if (results.length !== 1) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid legacy session token'); + } + var obj = results[0]; + obj.className = '_User'; + var userObject = Parse.Object.fromJSON(obj); + return new Auth({ config: config, isMaster: false, installationId: installationId, user: userObject }); + }); +}; + +// Returns a promise that resolves to an array of role names +Auth.prototype.getUserRoles = function () { + if (this.isMaster || !this.user) { + return Promise.resolve([]); + } + if (this.fetchedRoles) { + return Promise.resolve(this.userRoles); + } + if (this.rolePromise) { + return this.rolePromise; + } + this.rolePromise = this._loadRoles(); + return this.rolePromise; +}; + +// Iterates through the role tree and compiles a users roles +Auth.prototype._loadRoles = function () { + var _this = this; + + var cacheAdapter = this.config.cacheController; + return cacheAdapter.role.get(this.user.id).then(function (cachedRoles) { + if (cachedRoles != null) { + _this.fetchedRoles = true; + _this.userRoles = cachedRoles; + return Promise.resolve(cachedRoles); + } + + var restWhere = { + 'users': { + __type: 'Pointer', + className: '_User', + objectId: _this.user.id + } + }; + // First get the role ids this user is directly a member of + var query = new RestQuery(_this.config, master(_this.config), '_Role', restWhere, {}); + return query.execute().then(function (response) { + var results = response.results; + if (!results.length) { + _this.userRoles = []; + _this.fetchedRoles = true; + _this.rolePromise = null; + + cacheAdapter.role.put(_this.user.id, _this.userRoles); + return Promise.resolve(_this.userRoles); + } + var rolesMap = results.reduce(function (m, r) { + m.names.push(r.name); + m.ids.push(r.objectId); + return m; + }, { ids: [], names: [] }); + + // run the recursive finding + return _this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names).then(function (roleNames) { + _this.userRoles = roleNames.map(function (r) { + return 'role:' + r; + }); + _this.fetchedRoles = true; + _this.rolePromise = null; + + cacheAdapter.role.put(_this.user.id, _this.userRoles); + return Promise.resolve(_this.userRoles); + }); + }); + }); +}; + +// Given a list of roleIds, find all the parent roles, returns a promise with all names +Auth.prototype._getAllRolesNamesForRoleIds = function (roleIDs) { + var _this2 = this; + + var names = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; + var queriedRoles = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + var ins = roleIDs.filter(function (roleID) { + return queriedRoles[roleID] !== true; + }).map(function (roleID) { + // mark as queried + queriedRoles[roleID] = true; + return { + __type: 'Pointer', + className: '_Role', + objectId: roleID + }; + }); + + // all roles are accounted for, return the names + if (ins.length == 0) { + return Promise.resolve([].concat(_toConsumableArray(new Set(names)))); + } + // Build an OR query across all parentRoles + var restWhere = void 0; + if (ins.length == 1) { + restWhere = { 'roles': ins[0] }; + } else { + restWhere = { 'roles': { '$in': ins } }; + } + var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); + return query.execute().then(function (response) { + var results = response.results; + // Nothing found + if (!results.length) { + return Promise.resolve(names); + } + // Map the results with all Ids and names + var resultMap = results.reduce(function (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 _this2._getAllRolesNamesForRoleIds(resultMap.ids, names, queriedRoles); + }).then(function (names) { + return Promise.resolve([].concat(_toConsumableArray(new Set(names)))); + }); +}; + +module.exports = { + Auth: Auth, + master: master, + nobody: nobody, + getAuthForSessionToken: getAuthForSessionToken, + getAuthForLegacySessionToken: getAuthForLegacySessionToken +}; \ No newline at end of file diff --git a/lib/ClientSDK.js b/lib/ClientSDK.js new file mode 100644 index 0000000000..ea73b24ccf --- /dev/null +++ b/lib/ClientSDK.js @@ -0,0 +1,42 @@ +'use strict'; + +var semver = require('semver'); + +function compatible(compatibleSDK) { + return function (clientSDK) { + if (typeof clientSDK === 'string') { + clientSDK = fromString(clientSDK); + } + // REST API, or custom SDK + if (!clientSDK) { + return true; + } + var clientVersion = clientSDK.version; + var compatiblityVersion = compatibleSDK[clientSDK.sdk]; + return semver.satisfies(clientVersion, compatiblityVersion); + }; +} + +function supportsForwardDelete(clientSDK) { + return compatible({ + js: '>=1.9.0' + })(clientSDK); +} + +function fromString(version) { + var versionRE = /([-a-zA-Z]+)([0-9\.]+)/; + var match = version.toLowerCase().match(versionRE); + if (match && match.length === 3) { + return { + sdk: match[1], + version: match[2] + }; + } + return undefined; +} + +module.exports = { + compatible: compatible, + supportsForwardDelete: supportsForwardDelete, + fromString: fromString +}; \ No newline at end of file diff --git a/lib/Config.js b/lib/Config.js new file mode 100644 index 0000000000..185a64e58b --- /dev/null +++ b/lib/Config.js @@ -0,0 +1,324 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Config = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // A Config object provides information about how a specific app is +// configured. +// mount is the URL for the root of the API; includes http, domain, etc. + +var _cache = require('./cache'); + +var _cache2 = _interopRequireDefault(_cache); + +var _SchemaCache = require('./Controllers/SchemaCache'); + +var _SchemaCache2 = _interopRequireDefault(_SchemaCache); + +var _DatabaseController = require('./Controllers/DatabaseController'); + +var _DatabaseController2 = _interopRequireDefault(_DatabaseController); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function removeTrailingSlash(str) { + if (!str) { + return str; + } + if (str.endsWith("/")) { + str = str.substr(0, str.length - 1); + } + return str; +} + +var Config = exports.Config = function () { + function Config(applicationId, mount) { + _classCallCheck(this, Config); + + var cacheInfo = _cache2.default.get(applicationId); + if (!cacheInfo) { + return; + } + + this.applicationId = applicationId; + this.jsonLogs = cacheInfo.jsonLogs; + this.masterKey = cacheInfo.masterKey; + this.clientKey = cacheInfo.clientKey; + this.javascriptKey = cacheInfo.javascriptKey; + this.dotNetKey = cacheInfo.dotNetKey; + this.restAPIKey = cacheInfo.restAPIKey; + this.webhookKey = cacheInfo.webhookKey; + this.fileKey = cacheInfo.fileKey; + this.allowClientClassCreation = cacheInfo.allowClientClassCreation; + this.userSensitiveFields = cacheInfo.userSensitiveFields; + + // Create a new DatabaseController per request + if (cacheInfo.databaseController) { + var schemaCache = new _SchemaCache2.default(cacheInfo.cacheController, cacheInfo.schemaCacheTTL, cacheInfo.enableSingleSchemaCache); + this.database = new _DatabaseController2.default(cacheInfo.databaseController.adapter, schemaCache); + } + + this.schemaCacheTTL = cacheInfo.schemaCacheTTL; + this.enableSingleSchemaCache = cacheInfo.enableSingleSchemaCache; + + this.serverURL = cacheInfo.serverURL; + this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL); + this.verifyUserEmails = cacheInfo.verifyUserEmails; + this.preventLoginWithUnverifiedEmail = cacheInfo.preventLoginWithUnverifiedEmail; + this.emailVerifyTokenValidityDuration = cacheInfo.emailVerifyTokenValidityDuration; + this.accountLockout = cacheInfo.accountLockout; + this.passwordPolicy = cacheInfo.passwordPolicy; + this.appName = cacheInfo.appName; + + this.analyticsController = cacheInfo.analyticsController; + this.cacheController = cacheInfo.cacheController; + this.hooksController = cacheInfo.hooksController; + this.filesController = cacheInfo.filesController; + this.pushController = cacheInfo.pushController; + this.pushControllerQueue = cacheInfo.pushControllerQueue; + this.pushWorker = cacheInfo.pushWorker; + this.hasPushSupport = cacheInfo.hasPushSupport; + this.hasPushScheduledSupport = cacheInfo.hasPushScheduledSupport; + this.loggerController = cacheInfo.loggerController; + this.userController = cacheInfo.userController; + this.authDataManager = cacheInfo.authDataManager; + this.customPages = cacheInfo.customPages || {}; + this.mount = removeTrailingSlash(mount); + this.liveQueryController = cacheInfo.liveQueryController; + this.sessionLength = cacheInfo.sessionLength; + this.expireInactiveSessions = cacheInfo.expireInactiveSessions; + this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this); + this.generateEmailVerifyTokenExpiresAt = this.generateEmailVerifyTokenExpiresAt.bind(this); + this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset; + } + + _createClass(Config, [{ + key: 'generateEmailVerifyTokenExpiresAt', + value: function generateEmailVerifyTokenExpiresAt() { + if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) { + return undefined; + } + var now = new Date(); + return new Date(now.getTime() + this.emailVerifyTokenValidityDuration * 1000); + } + }, { + key: 'generatePasswordResetTokenExpiresAt', + value: function generatePasswordResetTokenExpiresAt() { + if (!this.passwordPolicy || !this.passwordPolicy.resetTokenValidityDuration) { + return undefined; + } + var now = new Date(); + return new Date(now.getTime() + this.passwordPolicy.resetTokenValidityDuration * 1000); + } + }, { + key: 'generateSessionExpiresAt', + value: function generateSessionExpiresAt() { + if (!this.expireInactiveSessions) { + return undefined; + } + var now = new Date(); + return new Date(now.getTime() + this.sessionLength * 1000); + } + }, { + key: 'mount', + get: function get() { + var mount = this._mount; + if (this.publicServerURL) { + mount = this.publicServerURL; + } + return mount; + }, + set: function set(newValue) { + this._mount = newValue; + } + }, { + key: 'invalidLinkURL', + get: function get() { + return this.customPages.invalidLink || this.publicServerURL + '/apps/invalid_link.html'; + } + }, { + key: 'invalidVerificationLinkURL', + get: function get() { + return this.customPages.invalidVerificationLink || this.publicServerURL + '/apps/invalid_verification_link.html'; + } + }, { + key: 'linkSendSuccessURL', + get: function get() { + return this.customPages.linkSendSuccess || this.publicServerURL + '/apps/link_send_success.html'; + } + }, { + key: 'linkSendFailURL', + get: function get() { + return this.customPages.linkSendFail || this.publicServerURL + '/apps/link_send_fail.html'; + } + }, { + key: 'verifyEmailSuccessURL', + get: function get() { + return this.customPages.verifyEmailSuccess || this.publicServerURL + '/apps/verify_email_success.html'; + } + }, { + key: 'choosePasswordURL', + get: function get() { + return this.customPages.choosePassword || this.publicServerURL + '/apps/choose_password'; + } + }, { + key: 'requestResetPasswordURL', + get: function get() { + return this.publicServerURL + '/apps/' + this.applicationId + '/request_password_reset'; + } + }, { + key: 'passwordResetSuccessURL', + get: function get() { + return this.customPages.passwordResetSuccess || this.publicServerURL + '/apps/password_reset_success.html'; + } + }, { + key: 'parseFrameURL', + get: function get() { + return this.customPages.parseFrameURL; + } + }, { + key: 'verifyEmailURL', + get: function get() { + return this.publicServerURL + '/apps/' + this.applicationId + '/verify_email'; + } + }], [{ + key: 'validate', + value: function validate(_ref) { + var verifyUserEmails = _ref.verifyUserEmails, + userController = _ref.userController, + appName = _ref.appName, + publicServerURL = _ref.publicServerURL, + revokeSessionOnPasswordReset = _ref.revokeSessionOnPasswordReset, + expireInactiveSessions = _ref.expireInactiveSessions, + sessionLength = _ref.sessionLength, + emailVerifyTokenValidityDuration = _ref.emailVerifyTokenValidityDuration, + accountLockout = _ref.accountLockout, + passwordPolicy = _ref.passwordPolicy; + + var emailAdapter = userController.adapter; + if (verifyUserEmails) { + this.validateEmailConfiguration({ emailAdapter: emailAdapter, appName: appName, publicServerURL: publicServerURL, emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration }); + } + + this.validateAccountLockoutPolicy(accountLockout); + + this.validatePasswordPolicy(passwordPolicy); + + if (typeof revokeSessionOnPasswordReset !== 'boolean') { + throw 'revokeSessionOnPasswordReset must be a boolean value'; + } + + if (publicServerURL) { + if (!publicServerURL.startsWith("http://") && !publicServerURL.startsWith("https://")) { + throw "publicServerURL should be a valid HTTPS URL starting with https://"; + } + } + + this.validateSessionConfiguration(sessionLength, expireInactiveSessions); + } + }, { + key: 'validateAccountLockoutPolicy', + value: function validateAccountLockoutPolicy(accountLockout) { + if (accountLockout) { + 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) { + throw 'Account lockout threshold should be an integer greater than 0 and less than 1000'; + } + } + } + }, { + key: 'validatePasswordPolicy', + value: function validatePasswordPolicy(passwordPolicy) { + if (passwordPolicy) { + 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)) { + 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)) { + throw 'passwordPolicy.validatorPattern must be a regex string or RegExp object.'; + } + } + + if (passwordPolicy.validatorCallback && typeof passwordPolicy.validatorCallback !== 'function') { + throw 'passwordPolicy.validatorCallback must be a function.'; + } + + 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)) { + throw 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20'; + } + } + } + + // if the passwordPolicy.validatorPattern is configured then setup a callback to process the pattern + + }, { + key: 'setupPasswordValidator', + value: function setupPasswordValidator(passwordPolicy) { + if (passwordPolicy && passwordPolicy.validatorPattern) { + passwordPolicy.patternValidator = function (value) { + return passwordPolicy.validatorPattern.test(value); + }; + } + } + }, { + key: 'validateEmailConfiguration', + value: function validateEmailConfiguration(_ref2) { + var emailAdapter = _ref2.emailAdapter, + appName = _ref2.appName, + publicServerURL = _ref2.publicServerURL, + emailVerifyTokenValidityDuration = _ref2.emailVerifyTokenValidityDuration; + + if (!emailAdapter) { + throw 'An emailAdapter is required for e-mail verification and password resets.'; + } + if (typeof appName !== 'string') { + throw 'An app name is required for e-mail verification and password resets.'; + } + if (typeof publicServerURL !== 'string') { + throw 'A public server url is required for e-mail verification and password resets.'; + } + if (emailVerifyTokenValidityDuration) { + 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.'; + } + } + } + }, { + key: 'validateSessionConfiguration', + value: function validateSessionConfiguration(sessionLength, expireInactiveSessions) { + if (expireInactiveSessions) { + if (isNaN(sessionLength)) { + throw 'Session length must be a valid number.'; + } else if (sessionLength <= 0) { + throw 'Session length must be a value greater than 0.'; + } + } + } + }]); + + return Config; +}(); + +exports.default = Config; + +module.exports = Config; \ No newline at end of file diff --git a/lib/Controllers/AdaptableController.js b/lib/Controllers/AdaptableController.js new file mode 100644 index 0000000000..a1501dca19 --- /dev/null +++ b/lib/Controllers/AdaptableController.js @@ -0,0 +1,101 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.AdaptableController = undefined; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _Config = require("../Config"); + +var _Config2 = _interopRequireDefault(_Config); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +/* +AdaptableController.js + +AdaptableController is the base class for all controllers +that support adapter, +The super class takes care of creating the right instance for the adapter +based on the parameters passed + + */ + +// _adapter is private, use Symbol +var _adapter = Symbol(); + +var AdaptableController = exports.AdaptableController = function () { + function AdaptableController(adapter, appId, options) { + _classCallCheck(this, AdaptableController); + + this.options = options; + this.appId = appId; + this.adapter = adapter; + } + + _createClass(AdaptableController, [{ + key: "expectedAdapterType", + value: function expectedAdapterType() { + throw new Error("Subclasses should implement expectedAdapterType()"); + } + }, { + key: "validateAdapter", + value: function validateAdapter(adapter) { + AdaptableController.validateAdapter(adapter, this); + } + }, { + key: "adapter", + set: function set(adapter) { + this.validateAdapter(adapter); + this[_adapter] = adapter; + }, + get: function get() { + return this[_adapter]; + } + }, { + key: "config", + get: function get() { + return new _Config2.default(this.appId); + } + }], [{ + key: "validateAdapter", + value: function validateAdapter(adapter, self, ExpectedType) { + if (!adapter) { + throw new Error(this.constructor.name + " requires an adapter"); + } + + var Type = ExpectedType || self.expectedAdapterType(); + // Allow skipping for testing + if (!Type) { + return; + } + + // Makes sure the prototype matches + var mismatches = Object.getOwnPropertyNames(Type.prototype).reduce(function (obj, key) { + var adapterType = _typeof(adapter[key]); + var expectedType = _typeof(Type.prototype[key]); + if (adapterType !== expectedType) { + obj[key] = { + expected: expectedType, + actual: adapterType + }; + } + return obj; + }, {}); + + if (Object.keys(mismatches).length > 0) { + throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches); + } + } + }]); + + return AdaptableController; +}(); + +exports.default = AdaptableController; \ No newline at end of file diff --git a/lib/Controllers/AnalyticsController.js b/lib/Controllers/AnalyticsController.js new file mode 100644 index 0000000000..100fa67632 --- /dev/null +++ b/lib/Controllers/AnalyticsController.js @@ -0,0 +1,69 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.AnalyticsController = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _AdaptableController2 = require('./AdaptableController'); + +var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); + +var _AnalyticsAdapter = require('../Adapters/Analytics/AnalyticsAdapter'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var AnalyticsController = exports.AnalyticsController = function (_AdaptableController) { + _inherits(AnalyticsController, _AdaptableController); + + function AnalyticsController() { + _classCallCheck(this, AnalyticsController); + + return _possibleConstructorReturn(this, (AnalyticsController.__proto__ || Object.getPrototypeOf(AnalyticsController)).apply(this, arguments)); + } + + _createClass(AnalyticsController, [{ + key: 'appOpened', + value: function appOpened(req) { + var _this2 = this; + + return Promise.resolve().then(function () { + return _this2.adapter.appOpened(req.body, req); + }).then(function (response) { + return { response: response || {} }; + }).catch(function () { + return { response: {} }; + }); + } + }, { + key: 'trackEvent', + value: function trackEvent(req) { + var _this3 = this; + + return Promise.resolve().then(function () { + return _this3.adapter.trackEvent(req.params.eventName, req.body, req); + }).then(function (response) { + return { response: response || {} }; + }).catch(function () { + return { response: {} }; + }); + } + }, { + key: 'expectedAdapterType', + value: function expectedAdapterType() { + return _AnalyticsAdapter.AnalyticsAdapter; + } + }]); + + return AnalyticsController; +}(_AdaptableController3.default); + +exports.default = AnalyticsController; \ No newline at end of file diff --git a/lib/Controllers/CacheController.js b/lib/Controllers/CacheController.js new file mode 100644 index 0000000000..b2ebcb465b --- /dev/null +++ b/lib/Controllers/CacheController.js @@ -0,0 +1,129 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.CacheController = exports.SubCache = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _AdaptableController2 = require('./AdaptableController'); + +var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); + +var _CacheAdapter = require('../Adapters/Cache/CacheAdapter'); + +var _CacheAdapter2 = _interopRequireDefault(_CacheAdapter); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var KEY_SEPARATOR_CHAR = ':'; + +function joinKeys() { + for (var _len = arguments.length, keys = Array(_len), _key = 0; _key < _len; _key++) { + keys[_key] = arguments[_key]; + } + + return keys.join(KEY_SEPARATOR_CHAR); +} + +/** + * Prefix all calls to the cache via a prefix string, useful when grouping Cache by object type. + * + * eg "Role" or "Session" + */ + +var SubCache = exports.SubCache = function () { + function SubCache(prefix, cacheController, ttl) { + _classCallCheck(this, SubCache); + + this.prefix = prefix; + this.cache = cacheController; + this.ttl = ttl; + } + + _createClass(SubCache, [{ + key: 'get', + value: function get(key) { + var cacheKey = joinKeys(this.prefix, key); + return this.cache.get(cacheKey); + } + }, { + key: 'put', + value: function put(key, value, ttl) { + var cacheKey = joinKeys(this.prefix, key); + return this.cache.put(cacheKey, value, ttl); + } + }, { + key: 'del', + value: function del(key) { + var cacheKey = joinKeys(this.prefix, key); + return this.cache.del(cacheKey); + } + }, { + key: 'clear', + value: function clear() { + return this.cache.clear(); + } + }]); + + return SubCache; +}(); + +var CacheController = exports.CacheController = function (_AdaptableController) { + _inherits(CacheController, _AdaptableController); + + function CacheController(adapter, appId) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + _classCallCheck(this, CacheController); + + var _this = _possibleConstructorReturn(this, (CacheController.__proto__ || Object.getPrototypeOf(CacheController)).call(this, adapter, appId, options)); + + _this.role = new SubCache('role', _this); + _this.user = new SubCache('user', _this); + return _this; + } + + _createClass(CacheController, [{ + key: 'get', + value: function get(key) { + var cacheKey = joinKeys(this.appId, key); + return this.adapter.get(cacheKey).then(null, function () { + return Promise.resolve(null); + }); + } + }, { + key: 'put', + value: function put(key, value, ttl) { + var cacheKey = joinKeys(this.appId, key); + return this.adapter.put(cacheKey, value, ttl); + } + }, { + key: 'del', + value: function del(key) { + var cacheKey = joinKeys(this.appId, key); + return this.adapter.del(cacheKey); + } + }, { + key: 'clear', + value: function clear() { + return this.adapter.clear(); + } + }, { + key: 'expectedAdapterType', + value: function expectedAdapterType() { + return _CacheAdapter2.default; + } + }]); + + return CacheController; +}(_AdaptableController3.default); + +exports.default = CacheController; \ No newline at end of file diff --git a/lib/Controllers/DatabaseController.js b/lib/Controllers/DatabaseController.js new file mode 100644 index 0000000000..25bab4ad81 --- /dev/null +++ b/lib/Controllers/DatabaseController.js @@ -0,0 +1,1167 @@ +'use strict'; + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _node = require('parse/node'); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _intersect = require('intersect'); + +var _intersect2 = _interopRequireDefault(_intersect); + +var _deepcopy = require('deepcopy'); + +var _deepcopy2 = _interopRequireDefault(_deepcopy); + +var _logger = require('../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +var _SchemaController = require('./SchemaController'); + +var SchemaController = _interopRequireWildcard(_SchemaController); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } + +function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } // A database adapter that works with data exported from the hosted +// Parse database. + +function addWriteACL(query, acl) { + var newQuery = _lodash2.default.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].concat(_toConsumableArray(acl)) }; + return newQuery; +} + +function addReadACL(query, acl) { + var newQuery = _lodash2.default.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, "*"].concat(_toConsumableArray(acl)) }; + return newQuery; +} + +// Transforms a REST API formatted ACL object to our two-field mongo format. +var transformObjectACL = function transformObjectACL(_ref) { + var ACL = _ref.ACL, + result = _objectWithoutProperties(_ref, ['ACL']); + + if (!ACL) { + return result; + } + + result._wperm = []; + result._rperm = []; + + for (var entry in ACL) { + if (ACL[entry].read) { + result._rperm.push(entry); + } + if (ACL[entry].write) { + result._wperm.push(entry); + } + } + return result; +}; + +var specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count']; + +var isSpecialQueryKey = function isSpecialQueryKey(key) { + return specialQuerykeys.indexOf(key) >= 0; +}; + +var validateQuery = function validateQuery(query) { + if (query.ACL) { + throw new _node.Parse.Error(_node.Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); + } + + if (query.$or) { + if (query.$or instanceof Array) { + query.$or.forEach(validateQuery); + + /* In MongoDB, $or queries which are not alone at the top level of the + * query can not make efficient use of indexes due to a long standing + * bug known as SERVER-13732. + * + * This block restructures queries in which $or is not the sole top + * level element by moving all other top-level predicates inside every + * subdocument of the $or predicate, allowing MongoDB's query planner + * to make full use of the most relevant indexes. + * + * EG: {$or: [{a: 1}, {a: 2}], b: 2} + * Becomes: {$or: [{a: 1, b: 2}, {a: 2, b: 2}]} + * + * The only exceptions are $near and $nearSphere operators, which are + * constrained to only 1 operator per query. As a result, these ops + * remain at the top level + * + * https://jira.mongodb.org/browse/SERVER-13732 + * https://github.com/parse-community/parse-server/issues/3767 + */ + Object.keys(query).forEach(function (key) { + var noCollisions = !query.$or.some(function (subq) { + return subq.hasOwnProperty(key); + }); + var hasNears = false; + if (query[key] != null && _typeof(query[key]) == 'object') { + hasNears = '$near' in query[key] || '$nearSphere' in query[key]; + } + if (key != '$or' && noCollisions && !hasNears) { + query.$or.forEach(function (subquery) { + subquery[key] = query[key]; + }); + delete query[key]; + } + }); + query.$or.forEach(validateQuery); + } else { + throw new _node.Parse.Error(_node.Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); + } + } + + if (query.$and) { + if (query.$and instanceof Array) { + query.$and.forEach(validateQuery); + } else { + throw new _node.Parse.Error(_node.Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); + } + } + + Object.keys(query).forEach(function (key) { + if (query && query[key] && query[key].$regex) { + if (typeof query[key].$options === 'string') { + if (!query[key].$options.match(/^[imxs]+$/)) { + throw new _node.Parse.Error(_node.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 _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Invalid key name: ' + key); + } + }); +}; + +function DatabaseController(adapter, schemaCache) { + this.adapter = adapter; + this.schemaCache = schemaCache; + // We don't want a mutable this.schema, because then you could have + // one request that uses different schemas for different parts of + // it. Instead, use loadSchema to get a schema. + this.schemaPromise = null; +} + +DatabaseController.prototype.collectionExists = function (className) { + return this.adapter.classExists(className); +}; + +DatabaseController.prototype.purgeCollection = function (className) { + var _this = this; + + return this.loadSchema().then(function (schemaController) { + return schemaController.getOneSchema(className); + }).then(function (schema) { + return _this.adapter.deleteObjectsByQuery(className, schema, {}); + }); +}; + +DatabaseController.prototype.validateClassName = function (className) { + if (!SchemaController.classNameIsValid(className)) { + return Promise.reject(new _node.Parse.Error(_node.Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className)); + } + return Promise.resolve(); +}; + +// Returns a promise for a schemaController. +DatabaseController.prototype.loadSchema = function () { + var _this2 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { clearCache: false }; + + if (!this.schemaPromise) { + this.schemaPromise = SchemaController.load(this.adapter, this.schemaCache, options); + this.schemaPromise.then(function () { + return delete _this2.schemaPromise; + }, function () { + return delete _this2.schemaPromise; + }); + } + return this.schemaPromise; +}; + +// Returns a promise for the classname that is related to the given +// classname through the key. +// TODO: make this not in the DatabaseController interface +DatabaseController.prototype.redirectClassNameForKey = function (className, key) { + return this.loadSchema().then(function (schema) { + var t = schema.getExpectedType(className, key); + if (t && t.type == 'Relation') { + return t.targetClass; + } else { + return className; + } + }); +}; + +// Uses the schema to validate the object (REST API format). +// 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. +DatabaseController.prototype.validateObject = function (className, object, query, _ref2) { + var _this3 = this; + + var acl = _ref2.acl; + + var schema = void 0; + var isMaster = acl === undefined; + var aclGroup = acl || []; + return this.loadSchema().then(function (s) { + schema = s; + if (isMaster) { + return Promise.resolve(); + } + return _this3.canAddField(schema, className, object, aclGroup); + }).then(function () { + return schema.validateObject(className, object, query); + }); +}; + +// Filters out any data that shouldn't be on this REST-formatted object. +var filterSensitiveData = function filterSensitiveData(isMaster, aclGroup, className, object) { + if (className !== '_User') { + return object; + } + + object.password = object._hashed_password; + delete object._hashed_password; + + delete object.sessionToken; + + if (isMaster) { + return object; + } + delete object._email_verify_token; + delete object._perishable_token; + delete object._perishable_token_expires_at; + delete object._tombstone; + delete object._email_verify_token_expires_at; + delete object._failed_login_count; + delete object._account_lockout_expires_at; + delete object._password_changed_at; + + if (aclGroup.indexOf(object.objectId) > -1) { + return object; + } + delete object.authData; + return object; +}; + +// Runs an update on the database. +// Returns a promise for an object with the new values for field +// modifications that don't know their results ahead of time, like +// 'increment'. +// Options: +// 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. +var 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']; + +var isSpecialUpdateKey = function isSpecialUpdateKey(key) { + return specialKeysForUpdate.indexOf(key) >= 0; +}; + +DatabaseController.prototype.update = function (className, query, update) { + var _this4 = this; + + var _ref3 = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}, + acl = _ref3.acl, + many = _ref3.many, + upsert = _ref3.upsert; + + var skipSanitization = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; + + var originalQuery = query; + var originalUpdate = update; + // Make a copy of the object, so we don't mutate the incoming data. + update = (0, _deepcopy2.default)(update); + var relationUpdates = []; + var isMaster = acl === undefined; + var aclGroup = acl || []; + return this.loadSchema().then(function (schemaController) { + return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'update')).then(function () { + relationUpdates = _this4.collectRelationUpdates(className, originalQuery.objectId, update); + if (!isMaster) { + query = _this4.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(function (error) { + // If the schema doesn't exist, pretend it exists with no fields. This behaviour + // will likely need revisiting. + if (error === undefined) { + return { fields: {} }; + } + throw error; + }).then(function (schema) { + Object.keys(update).forEach(function (fieldName) { + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Invalid field name for update: ' + fieldName); + } + fieldName = fieldName.split('.')[0]; + if (!SchemaController.fieldNameIsValid(fieldName) && !isSpecialUpdateKey(fieldName)) { + throw new _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Invalid field name for update: ' + fieldName); + } + }); + for (var updateOperation in update) { + if (Object.keys(updateOperation).some(function (innerKey) { + return innerKey.includes('$') || innerKey.includes('.'); + })) { + throw new _node.Parse.Error(_node.Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); + } + } + update = transformObjectACL(update); + transformAuthData(className, update, schema); + if (many) { + return _this4.adapter.updateObjectsByQuery(className, schema, query, update); + } else if (upsert) { + return _this4.adapter.upsertOneObject(className, schema, query, update); + } else { + return _this4.adapter.findOneAndUpdate(className, schema, query, update); + } + }); + }).then(function (result) { + if (!result) { + return Promise.reject(new _node.Parse.Error(_node.Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); + } + return _this4.handleRelationUpdates(className, originalQuery.objectId, update, relationUpdates).then(function () { + return result; + }); + }).then(function (result) { + if (skipSanitization) { + return Promise.resolve(result); + } + return sanitizeDatabaseResult(originalUpdate, result); + }); + }); +}; + +function sanitizeDatabaseResult(originalObject, result) { + var response = {}; + if (!result) { + return Promise.resolve(response); + } + Object.keys(originalObject).forEach(function (key) { + var keyUpdate = originalObject[key]; + // determine if that was an op + if (keyUpdate && (typeof keyUpdate === 'undefined' ? 'undefined' : _typeof(keyUpdate)) === 'object' && keyUpdate.__op && ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1) { + // only valid ops that produce an actionable result + response[key] = result[key]; + } + }); + return Promise.resolve(response); +} + +// Collect all relation-updating operations from a REST-format update. +// Returns a list of all relation updates to perform +// This mutates update. +DatabaseController.prototype.collectRelationUpdates = function (className, objectId, update) { + var ops = []; + var deleteMe = []; + objectId = update.objectId || objectId; + + var process = function process(op, key) { + if (!op) { + return; + } + if (op.__op == 'AddRelation') { + ops.push({ key: key, op: op }); + deleteMe.push(key); + } + + if (op.__op == 'RemoveRelation') { + ops.push({ key: key, op: op }); + deleteMe.push(key); + } + + if (op.__op == 'Batch') { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = op.ops[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var x = _step.value; + + process(x, key); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + } + }; + + for (var key in update) { + process(update[key], key); + } + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = deleteMe[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var _key = _step2.value; + + delete update[_key]; + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + return ops; +}; + +// Processes relation-updating operations from a REST-format update. +// Returns a promise that resolves when all updates have been performed +DatabaseController.prototype.handleRelationUpdates = function (className, objectId, update, ops) { + var _this5 = this; + + var pending = []; + objectId = update.objectId || objectId; + ops.forEach(function (_ref4) { + var key = _ref4.key, + op = _ref4.op; + + if (!op) { + return; + } + if (op.__op == 'AddRelation') { + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = op.objects[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var object = _step3.value; + + pending.push(_this5.addRelation(key, className, objectId, object.objectId)); + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + } + + if (op.__op == 'RemoveRelation') { + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + for (var _iterator4 = op.objects[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var _object = _step4.value; + + pending.push(_this5.removeRelation(key, className, objectId, _object.objectId)); + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + } + }); + + return Promise.all(pending); +}; + +// Adds a relation. +// Returns a promise that resolves successfully iff the add was successful. +var relationSchema = { fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } } }; +DatabaseController.prototype.addRelation = function (key, fromClassName, fromId, toId) { + var doc = { + relatedId: toId, + owningId: fromId + }; + return this.adapter.upsertOneObject('_Join:' + key + ':' + fromClassName, relationSchema, doc, doc); +}; + +// Removes a relation. +// Returns a promise that resolves successfully iff the remove was +// successful. +DatabaseController.prototype.removeRelation = function (key, fromClassName, fromId, toId) { + var doc = { + relatedId: toId, + owningId: fromId + }; + return this.adapter.deleteObjectsByQuery('_Join:' + key + ':' + fromClassName, relationSchema, doc).catch(function (error) { + // We don't care if they try to delete a non-existent relation. + if (error.code == _node.Parse.Error.OBJECT_NOT_FOUND) { + return; + } + throw error; + }); +}; + +// Removes objects matches this query from the database. +// Returns a promise that resolves successfully iff the object was +// deleted. +// Options: +// 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. +DatabaseController.prototype.destroy = function (className, query) { + var _this6 = this; + + var _ref5 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, + acl = _ref5.acl; + + var isMaster = acl === undefined; + var aclGroup = acl || []; + + return this.loadSchema().then(function (schemaController) { + return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'delete')).then(function () { + if (!isMaster) { + query = _this6.addPointerPermissions(schemaController, className, 'delete', query, aclGroup); + if (!query) { + throw new _node.Parse.Error(_node.Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } + } + // delete by query + if (acl) { + query = addWriteACL(query, acl); + } + validateQuery(query); + return schemaController.getOneSchema(className).catch(function (error) { + // If the schema doesn't exist, pretend it exists with no fields. This behaviour + // will likely need revisiting. + if (error === undefined) { + return { fields: {} }; + } + throw error; + }).then(function (parseFormatSchema) { + return _this6.adapter.deleteObjectsByQuery(className, parseFormatSchema, query); + }).catch(function (error) { + // When deleting sessions while changing passwords, don't throw an error if they don't have any sessions. + if (className === "_Session" && error.code === _node.Parse.Error.OBJECT_NOT_FOUND) { + return Promise.resolve({}); + } + throw error; + }); + }); + }); +}; + +var flattenUpdateOperatorsForCreate = function flattenUpdateOperatorsForCreate(object) { + for (var key in object) { + if (object[key] && object[key].__op) { + switch (object[key].__op) { + case 'Increment': + if (typeof object[key].amount !== 'number') { + throw new _node.Parse.Error(_node.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 _node.Parse.Error(_node.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 _node.Parse.Error(_node.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 _node.Parse.Error(_node.Parse.Error.INVALID_JSON, 'objects to add must be an array'); + } + object[key] = []; + break; + case 'Delete': + delete object[key]; + break; + default: + throw new _node.Parse.Error(_node.Parse.Error.COMMAND_UNAVAILABLE, 'The ' + object[key].__op + ' operator is not supported yet.'); + } + } + } +}; + +var transformAuthData = function transformAuthData(className, object, schema) { + if (object.authData && className === '_User') { + Object.keys(object.authData).forEach(function (provider) { + var providerData = object.authData[provider]; + var fieldName = '_auth_data_' + provider; + if (providerData == null) { + object[fieldName] = { + __op: 'Delete' + }; + } else { + object[fieldName] = providerData; + schema.fields[fieldName] = { type: 'Object' }; + } + }); + delete object.authData; + } +}; + +// Inserts an object into the database. +// Returns a promise that resolves successfully iff the object saved. +DatabaseController.prototype.create = function (className, object) { + var _this7 = this; + + var _ref6 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, + acl = _ref6.acl; + + // Make a copy of the object, so we don't mutate the incoming data. + var originalObject = object; + object = transformObjectACL(object); + + object.createdAt = { iso: object.createdAt, __type: 'Date' }; + object.updatedAt = { iso: object.updatedAt, __type: 'Date' }; + + var isMaster = acl === undefined; + var aclGroup = acl || []; + var relationUpdates = this.collectRelationUpdates(className, null, object); + return this.validateClassName(className).then(function () { + return _this7.loadSchema(); + }).then(function (schemaController) { + return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'create')).then(function () { + return schemaController.enforceClassExists(className); + }).then(function () { + return schemaController.reloadData(); + }).then(function () { + return schemaController.getOneSchema(className, true); + }).then(function (schema) { + transformAuthData(className, object, schema); + flattenUpdateOperatorsForCreate(object); + return _this7.adapter.createObject(className, SchemaController.convertSchemaToAdapterSchema(schema), object); + }).then(function (result) { + return _this7.handleRelationUpdates(className, null, object, relationUpdates).then(function () { + return sanitizeDatabaseResult(originalObject, result.ops[0]); + }); + }); + }); +}; + +DatabaseController.prototype.canAddField = function (schema, className, object, aclGroup) { + var classSchema = schema.data[className]; + if (!classSchema) { + return Promise.resolve(); + } + var fields = Object.keys(object); + var schemaFields = Object.keys(classSchema); + var newKeys = fields.filter(function (field) { + return schemaFields.indexOf(field) < 0; + }); + if (newKeys.length > 0) { + return schema.validatePermission(className, aclGroup, 'addField'); + } + return Promise.resolve(); +}; + +// Won't delete collections in the system namespace +// Returns a promise. +DatabaseController.prototype.deleteEverything = function () { + this.schemaPromise = null; + return Promise.all([this.adapter.deleteAllClasses(), this.schemaCache.clear()]); +}; + +// Returns a promise for a list of related ids given an owning id. +// className here is the owning className. +DatabaseController.prototype.relatedIds = function (className, key, owningId) { + return this.adapter.find(joinTableName(className, key), relationSchema, { owningId: owningId }, {}).then(function (results) { + return results.map(function (result) { + return result.relatedId; + }); + }); +}; + +// Returns a promise for a list of owning ids given some related ids. +// className here is the owning className. +DatabaseController.prototype.owningIds = function (className, key, relatedIds) { + return this.adapter.find(joinTableName(className, key), relationSchema, { relatedId: { '$in': relatedIds } }, {}).then(function (results) { + return results.map(function (result) { + return result.owningId; + }); + }); +}; + +// Modifies query so that it no longer has $in on relation fields, or +// equal-to-pointer constraints on relation fields. +// Returns a promise that resolves when query is mutated +DatabaseController.prototype.reduceInRelation = function (className, query, schema) { + var _this8 = this; + + // Search for an in-relation or equal-to-relation + // Make it sequential for now, not sure of paralleization side effects + if (query['$or']) { + var ors = query['$or']; + return Promise.all(ors.map(function (aQuery, index) { + return _this8.reduceInRelation(className, aQuery, schema).then(function (aQuery) { + query['$or'][index] = aQuery; + }); + })).then(function () { + return Promise.resolve(query); + }); + } + + var promises = Object.keys(query).map(function (key) { + if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) { + var t = schema.getExpectedType(className, key); + if (!t || t.type !== 'Relation') { + return Promise.resolve(query); + } + // Build the list of queries + var queries = Object.keys(query[key]).map(function (constraintKey) { + var relatedIds = void 0; + var isNegation = false; + if (constraintKey === 'objectId') { + relatedIds = [query[key].objectId]; + } else if (constraintKey == '$in') { + relatedIds = query[key]['$in'].map(function (r) { + return r.objectId; + }); + } else if (constraintKey == '$nin') { + isNegation = true; + relatedIds = query[key]['$nin'].map(function (r) { + return r.objectId; + }); + } else if (constraintKey == '$ne') { + isNegation = true; + relatedIds = [query[key]['$ne'].objectId]; + } else { + return; + } + return { + isNegation: isNegation, + relatedIds: relatedIds + }; + }); + + // remove the current queryKey as we don,t need it anymore + delete query[key]; + // execute each query independnently to build the list of + // $in / $nin + var _promises = queries.map(function (q) { + if (!q) { + return Promise.resolve(); + } + return _this8.owningIds(className, key, q.relatedIds).then(function (ids) { + if (q.isNegation) { + _this8.addNotInObjectIdsIds(ids, query); + } else { + _this8.addInObjectIdsIds(ids, query); + } + return Promise.resolve(); + }); + }); + + return Promise.all(_promises).then(function () { + return Promise.resolve(); + }); + } + return Promise.resolve(); + }); + + return Promise.all(promises).then(function () { + return Promise.resolve(query); + }); +}; + +// Modifies query so that it no longer has $relatedTo +// Returns a promise that resolves when query is mutated +DatabaseController.prototype.reduceRelationKeys = function (className, query) { + var _this9 = this; + + if (query['$or']) { + return Promise.all(query['$or'].map(function (aQuery) { + return _this9.reduceRelationKeys(className, aQuery); + })); + } + + var relatedTo = query['$relatedTo']; + if (relatedTo) { + return this.relatedIds(relatedTo.object.className, relatedTo.key, relatedTo.object.objectId).then(function (ids) { + delete query['$relatedTo']; + _this9.addInObjectIdsIds(ids, query); + return _this9.reduceRelationKeys(className, query); + }); + } +}; + +DatabaseController.prototype.addInObjectIdsIds = function () { + var ids = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; + var query = arguments[1]; + + var idsFromString = typeof query.objectId === 'string' ? [query.objectId] : null; + var idsFromEq = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null; + var idsFromIn = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null; + + var allIds = [idsFromString, idsFromEq, idsFromIn, ids].filter(function (list) { + return list !== null; + }); + var totalLength = allIds.reduce(function (memo, list) { + return memo + list.length; + }, 0); + + var idsIntersection = []; + if (totalLength > 125) { + idsIntersection = _intersect2.default.big(allIds); + } else { + idsIntersection = (0, _intersect2.default)(allIds); + } + + // Need to make sure we don't clobber existing shorthand $eq constraints on objectId. + if (!('objectId' in query)) { + query.objectId = {}; + } else if (typeof query.objectId === 'string') { + query.objectId = { + $eq: query.objectId + }; + } + query.objectId['$in'] = idsIntersection; + + return query; +}; + +DatabaseController.prototype.addNotInObjectIdsIds = function () { + var ids = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + var query = arguments[1]; + + var idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : []; + var allIds = [].concat(_toConsumableArray(idsFromNin), _toConsumableArray(ids)).filter(function (list) { + return list !== null; + }); + + // make a set and spread to remove duplicates + allIds = [].concat(_toConsumableArray(new Set(allIds))); + + // Need to make sure we don't clobber existing shorthand $eq constraints on objectId. + if (!('objectId' in query)) { + query.objectId = {}; + } else if (typeof query.objectId === 'string') { + query.objectId = { + $eq: query.objectId + }; + } + + query.objectId['$nin'] = allIds; + return query; +}; + +// Runs a query on the database. +// Returns a promise that resolves to a list of items. +// Options: +// skip number of results to skip. +// limit limit to this number of results. +// sort an object where keys are the fields to sort by. +// the value is +1 for ascending, -1 for descending. +// count run a count instead of returning results. +// acl restrict this operation with an ACL for the provided array +// of user objectIds and roles. acl: null means no user. +// when this field is not present, don't do anything regarding ACLs. +// 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. +DatabaseController.prototype.find = function (className, query) { + var _this10 = this; + + var _ref7 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, + skip = _ref7.skip, + limit = _ref7.limit, + acl = _ref7.acl, + _ref7$sort = _ref7.sort, + sort = _ref7$sort === undefined ? {} : _ref7$sort, + count = _ref7.count, + keys = _ref7.keys, + op = _ref7.op; + + var isMaster = acl === undefined; + var aclGroup = acl || []; + op = op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find'); + // Count operation if counting + op = count === true ? 'count' : op; + + var classExists = true; + return this.loadSchema().then(function (schemaController) { + //Allow volatile classes if querying with Master (for _PushStatus) + //TODO: Move volatile classes concept into mongo adatper, postgres adapter shouldn't care + //that api.parse.com breaks when _PushStatus exists in mongo. + return schemaController.getOneSchema(className, isMaster).catch(function (error) { + // Behaviour 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(function (schema) { + // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, + // so duplicate that behaviour here. If both are specified, the corrent behaviour 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._updated_at) { + sort.updatedAt = sort._updated_at; + delete sort._updated_at; + } + Object.keys(sort).forEach(function (fieldName) { + if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { + throw new _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Cannot sort by ' + fieldName); + } + if (!SchemaController.fieldNameIsValid(fieldName)) { + throw new _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Invalid field name: ' + fieldName + '.'); + } + }); + return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)).then(function () { + return _this10.reduceRelationKeys(className, query); + }).then(function () { + return _this10.reduceInRelation(className, query, schemaController); + }).then(function () { + if (!isMaster) { + query = _this10.addPointerPermissions(schemaController, className, op, query, aclGroup); + } + if (!query) { + if (op == 'get') { + throw new _node.Parse.Error(_node.Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } else { + return []; + } + } + if (!isMaster) { + query = addReadACL(query, aclGroup); + } + validateQuery(query); + if (count) { + if (!classExists) { + return 0; + } else { + return _this10.adapter.count(className, schema, query); + } + } else { + if (!classExists) { + return []; + } else { + return _this10.adapter.find(className, schema, query, { skip: skip, limit: limit, sort: sort, keys: keys }).then(function (objects) { + return objects.map(function (object) { + object = untransformObjectACL(object); + return filterSensitiveData(isMaster, aclGroup, className, object); + }); + }); + } + } + }); + }); + }); +}; + +// Transforms a Database format ACL to a REST API format ACL +var untransformObjectACL = function untransformObjectACL(_ref8) { + var _rperm = _ref8._rperm, + _wperm = _ref8._wperm, + output = _objectWithoutProperties(_ref8, ['_rperm', '_wperm']); + + if (_rperm || _wperm) { + output.ACL = {}; + + (_rperm || []).forEach(function (entry) { + if (!output.ACL[entry]) { + output.ACL[entry] = { read: true }; + } else { + output.ACL[entry]['read'] = true; + } + }); + + (_wperm || []).forEach(function (entry) { + if (!output.ACL[entry]) { + output.ACL[entry] = { write: true }; + } else { + output.ACL[entry]['write'] = true; + } + }); + } + return output; +}; + +DatabaseController.prototype.deleteSchema = function (className) { + var _this11 = this; + + return this.loadSchema(true).then(function (schemaController) { + return schemaController.getOneSchema(className, true); + }).catch(function (error) { + if (error === undefined) { + return { fields: {} }; + } else { + throw error; + } + }).then(function (schema) { + return _this11.collectionExists(className).then(function () { + return _this11.adapter.count(className, { fields: {} }); + }).then(function (count) { + if (count > 0) { + throw new _node.Parse.Error(255, 'Class ' + className + ' is not empty, contains ' + count + ' objects, cannot drop schema.'); + } + return _this11.adapter.deleteClass(className); + }).then(function (wasParseCollection) { + if (wasParseCollection) { + var relationFieldNames = Object.keys(schema.fields).filter(function (fieldName) { + return schema.fields[fieldName].type === 'Relation'; + }); + return Promise.all(relationFieldNames.map(function (name) { + return _this11.adapter.deleteClass(joinTableName(className, name)); + })); + } else { + return Promise.resolve(); + } + }); + }); +}; + +DatabaseController.prototype.addPointerPermissions = function (schema, className, operation, query) { + var aclGroup = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; + + // Check if class has public permission for operation + // If the BaseCLP pass, let go through + if (schema.testBaseCLP(className, aclGroup, operation)) { + return query; + } + var perms = schema.perms[className]; + var field = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields'; + var userACL = aclGroup.filter(function (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 dedup users correctly + if (userACL.length != 1) { + return; + } + var userId = userACL[0]; + var userPointer = { + "__type": "Pointer", + "className": "_User", + "objectId": userId + }; + + var permFields = perms[field]; + var ors = permFields.map(function (key) { + var q = _defineProperty({}, key, userPointer); + return { '$and': [q, query] }; + }); + if (ors.length > 1) { + return { '$or': ors }; + } + return ors[0]; + } else { + return query; + } +}; + +// 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. +DatabaseController.prototype.performInitialization = function () { + var _this12 = this; + + var requiredUserFields = { fields: _extends({}, SchemaController.defaultColumns._Default, SchemaController.defaultColumns._User) }; + var requiredRoleFields = { fields: _extends({}, SchemaController.defaultColumns._Default, SchemaController.defaultColumns._Role) }; + + var userClassPromise = this.loadSchema().then(function (schema) { + return schema.enforceClassExists('_User'); + }); + var roleClassPromise = this.loadSchema().then(function (schema) { + return schema.enforceClassExists('_Role'); + }); + + var usernameUniqueness = userClassPromise.then(function () { + return _this12.adapter.ensureUniqueness('_User', requiredUserFields, ['username']); + }).catch(function (error) { + _logger2.default.warn('Unable to ensure uniqueness for usernames: ', error); + throw error; + }); + + var emailUniqueness = userClassPromise.then(function () { + return _this12.adapter.ensureUniqueness('_User', requiredUserFields, ['email']); + }).catch(function (error) { + _logger2.default.warn('Unable to ensure uniqueness for user email addresses: ', error); + throw error; + }); + + var roleUniqueness = roleClassPromise.then(function () { + return _this12.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']); + }).catch(function (error) { + _logger2.default.warn('Unable to ensure uniqueness for role name: ', error); + throw error; + }); + + // Create tables for volatile classes + var adapterInit = this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas }); + return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit]); +}; + +function joinTableName(className, key) { + return '_Join:' + key + ':' + className; +} + +// Expose validateQuery for tests +DatabaseController._validateQuery = validateQuery; +module.exports = DatabaseController; \ No newline at end of file diff --git a/lib/Controllers/FilesController.js b/lib/Controllers/FilesController.js new file mode 100644 index 0000000000..0fe78978d2 --- /dev/null +++ b/lib/Controllers/FilesController.js @@ -0,0 +1,142 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.FilesController = undefined; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _cryptoUtils = require('../cryptoUtils'); + +var _AdaptableController2 = require('./AdaptableController'); + +var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); + +var _FilesAdapter = require('../Adapters/Files/FilesAdapter'); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _mime = require('mime'); + +var _mime2 = _interopRequireDefault(_mime); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // FilesController.js + + +var 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}-.*"); + +var FilesController = exports.FilesController = function (_AdaptableController) { + _inherits(FilesController, _AdaptableController); + + function FilesController() { + _classCallCheck(this, FilesController); + + return _possibleConstructorReturn(this, (FilesController.__proto__ || Object.getPrototypeOf(FilesController)).apply(this, arguments)); + } + + _createClass(FilesController, [{ + key: 'getFileData', + value: function getFileData(config, filename) { + return this.adapter.getFileData(filename); + } + }, { + key: 'createFile', + value: function createFile(config, filename, data, contentType) { + + var extname = _path2.default.extname(filename); + + var hasExtension = extname.length > 0; + + if (!hasExtension && contentType && _mime2.default.extension(contentType)) { + filename = filename + '.' + _mime2.default.extension(contentType); + } else if (hasExtension && !contentType) { + contentType = _mime2.default.lookup(filename); + } + + filename = (0, _cryptoUtils.randomHexString)(32) + '_' + filename; + + var location = this.adapter.getFileLocation(config, filename); + return this.adapter.createFile(filename, data, contentType).then(function () { + return Promise.resolve({ + url: location, + name: filename + }); + }); + } + }, { + key: 'deleteFile', + value: function deleteFile(config, filename) { + return this.adapter.deleteFile(filename); + } + + /** + * Find file references in REST-format object and adds the url key + * with the current mount point and app id. + * Object may be a single object or list of REST-format objects. + */ + + }, { + key: 'expandFilesInObject', + value: function expandFilesInObject(config, object) { + var _this2 = this; + + if (object instanceof Array) { + object.map(function (obj) { + return _this2.expandFilesInObject(config, obj); + }); + return; + } + if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) !== 'object') { + return; + } + for (var key in object) { + var fileObject = object[key]; + if (fileObject && fileObject['__type'] === 'File') { + if (fileObject['url']) { + continue; + } + var filename = fileObject['name']; + // all filenames starting with "tfss-" should be from files.parsetfss.com + // all filenames starting with a "-" seperated UUID should be from files.parse.com + // all other filenames have been migrated or created from Parse Server + if (config.fileKey === undefined) { + fileObject['url'] = this.adapter.getFileLocation(config, filename); + } else { + if (filename.indexOf('tfss-') === 0) { + fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename); + } else if (legacyFilesRegex.test(filename)) { + fileObject['url'] = 'http://files.parse.com/' + config.fileKey + '/' + encodeURIComponent(filename); + } else { + fileObject['url'] = this.adapter.getFileLocation(config, filename); + } + } + } + } + } + }, { + key: 'expectedAdapterType', + value: function expectedAdapterType() { + return _FilesAdapter.FilesAdapter; + } + }, { + key: 'getFileStream', + value: function getFileStream(config, filename) { + return this.adapter.getFileStream(filename); + } + }]); + + return FilesController; +}(_AdaptableController3.default); + +exports.default = FilesController; \ No newline at end of file diff --git a/lib/Controllers/HooksController.js b/lib/Controllers/HooksController.js new file mode 100644 index 0000000000..76e32bf147 --- /dev/null +++ b/lib/Controllers/HooksController.js @@ -0,0 +1,268 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.HooksController = undefined; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** weak */ + +var _triggers = require("../triggers"); + +var triggers = _interopRequireWildcard(_triggers); + +var _node = require("parse/node"); + +var Parse = _interopRequireWildcard(_node); + +var _request = require("request"); + +var request = _interopRequireWildcard(_request); + +var _logger = require("../logger"); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var DefaultHooksCollectionName = "_Hooks"; + +var HooksController = exports.HooksController = function () { + function HooksController(applicationId, databaseController, webhookKey) { + _classCallCheck(this, HooksController); + + this._applicationId = applicationId; + this._webhookKey = webhookKey; + this.database = databaseController; + } + + _createClass(HooksController, [{ + key: "load", + value: function load() { + var _this = this; + + return this._getHooks().then(function (hooks) { + hooks = hooks || []; + hooks.forEach(function (hook) { + _this.addHookToTriggers(hook); + }); + }); + } + }, { + key: "getFunction", + value: function getFunction(functionName) { + return this._getHooks({ functionName: functionName }, 1).then(function (results) { + return results[0]; + }); + } + }, { + key: "getFunctions", + value: function getFunctions() { + return this._getHooks({ functionName: { $exists: true } }); + } + }, { + key: "getTrigger", + value: function getTrigger(className, triggerName) { + return this._getHooks({ className: className, triggerName: triggerName }, 1).then(function (results) { + return results[0]; + }); + } + }, { + key: "getTriggers", + value: function getTriggers() { + return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true } }); + } + }, { + key: "deleteFunction", + value: function deleteFunction(functionName) { + triggers.removeFunction(functionName, this._applicationId); + return this._removeHooks({ functionName: functionName }); + } + }, { + key: "deleteTrigger", + value: function deleteTrigger(className, triggerName) { + triggers.removeTrigger(triggerName, className, this._applicationId); + return this._removeHooks({ className: className, triggerName: triggerName }); + } + }, { + key: "_getHooks", + value: function _getHooks() { + var query = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + return this.database.find(DefaultHooksCollectionName, query).then(function (results) { + return results.map(function (result) { + delete result.objectId; + return result; + }); + }); + } + }, { + key: "_removeHooks", + value: function _removeHooks(query) { + return this.database.destroy(DefaultHooksCollectionName, query).then(function () { + return Promise.resolve({}); + }); + } + }, { + key: "saveHook", + value: function saveHook(hook) { + var query; + if (hook.functionName && hook.url) { + query = { functionName: hook.functionName }; + } else if (hook.triggerName && hook.className && hook.url) { + query = { className: hook.className, triggerName: hook.triggerName }; + } else { + throw new Parse.Error(143, "invalid hook declaration"); + } + return this.database.update(DefaultHooksCollectionName, query, hook, { upsert: true }).then(function () { + return Promise.resolve(hook); + }); + } + }, { + key: "addHookToTriggers", + value: function addHookToTriggers(hook) { + var wrappedFunction = wrapToHTTPRequest(hook, this._webhookKey); + wrappedFunction.url = hook.url; + if (hook.className) { + triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, this._applicationId); + } else { + triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId); + } + } + }, { + key: "addHook", + value: function addHook(hook) { + this.addHookToTriggers(hook); + return this.saveHook(hook); + } + }, { + key: "createOrUpdateHook", + value: function createOrUpdateHook(aHook) { + var hook; + if (aHook && aHook.functionName && aHook.url) { + hook = {}; + hook.functionName = aHook.functionName; + hook.url = aHook.url; + } 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"); + } + + return this.addHook(hook); + } + }, { + key: "createHook", + value: function createHook(aHook) { + var _this2 = this; + + if (aHook.functionName) { + return this.getFunction(aHook.functionName).then(function (result) { + if (result) { + throw new Parse.Error(143, "function name: " + aHook.functionName + " already exits"); + } else { + return _this2.createOrUpdateHook(aHook); + } + }); + } else if (aHook.className && aHook.triggerName) { + return this.getTrigger(aHook.className, aHook.triggerName).then(function (result) { + if (result) { + throw new Parse.Error(143, "class " + aHook.className + " already has trigger " + aHook.triggerName); + } + return _this2.createOrUpdateHook(aHook); + }); + } + + throw new Parse.Error(143, "invalid hook declaration"); + } + }, { + key: "updateHook", + value: function updateHook(aHook) { + var _this3 = this; + + if (aHook.functionName) { + return this.getFunction(aHook.functionName).then(function (result) { + if (result) { + return _this3.createOrUpdateHook(aHook); + } + 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(function (result) { + if (result) { + return _this3.createOrUpdateHook(aHook); + } + throw new Parse.Error(143, "class " + aHook.className + " does not exist"); + }); + } + throw new Parse.Error(143, "invalid hook declaration"); + } + }]); + + return HooksController; +}(); + +function wrapToHTTPRequest(hook, key) { + return function (req, res) { + var jsonBody = {}; + for (var i in req) { + jsonBody[i] = req[i]; + } + if (req.object) { + jsonBody.object = req.object.toJSON(); + jsonBody.object.className = req.object.className; + } + if (req.original) { + jsonBody.original = req.original.toJSON(); + jsonBody.original.className = req.original.className; + } + var jsonRequest = { + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(jsonBody) + }; + + if (key) { + jsonRequest.headers['X-Parse-Webhook-Key'] = key; + } else { + _logger.logger.warn('Making outgoing webhook request without webhookKey being set!'); + } + + request.post(hook.url, jsonRequest, function (err, httpResponse, body) { + var result; + if (body) { + if (typeof body === "string") { + try { + body = JSON.parse(body); + } catch (e) { + err = { error: "Malformed response", code: -1 }; + } + } + if (!err) { + result = body.success; + err = body.error; + } + } + + if (err) { + return res.error(err); + } else if (hook.triggerName === 'beforeSave') { + if ((typeof result === "undefined" ? "undefined" : _typeof(result)) === 'object') { + delete result.createdAt; + delete result.updatedAt; + } + return res.success({ object: result }); + } else { + return res.success(result); + } + }); + }; +} + +exports.default = HooksController; \ No newline at end of file diff --git a/lib/Controllers/LiveQueryController.js b/lib/Controllers/LiveQueryController.js new file mode 100644 index 0000000000..062d443f3c --- /dev/null +++ b/lib/Controllers/LiveQueryController.js @@ -0,0 +1,68 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.LiveQueryController = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _ParseCloudCodePublisher = require('../LiveQuery/ParseCloudCodePublisher'); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var LiveQueryController = exports.LiveQueryController = function () { + function LiveQueryController(config) { + _classCallCheck(this, LiveQueryController); + + // If config is empty, we just assume no classs needs to be registered as LiveQuery + if (!config || !config.classNames) { + this.classNames = new Set(); + } else if (config.classNames instanceof Array) { + this.classNames = new Set(config.classNames); + } else { + throw 'liveQuery.classes should be an array of string'; + } + this.liveQueryPublisher = new _ParseCloudCodePublisher.ParseCloudCodePublisher(config); + } + + _createClass(LiveQueryController, [{ + key: 'onAfterSave', + value: function onAfterSave(className, currentObject, originalObject) { + if (!this.hasLiveQuery(className)) { + return; + } + var req = this._makePublisherRequest(currentObject, originalObject); + this.liveQueryPublisher.onCloudCodeAfterSave(req); + } + }, { + key: 'onAfterDelete', + value: function onAfterDelete(className, currentObject, originalObject) { + if (!this.hasLiveQuery(className)) { + return; + } + var req = this._makePublisherRequest(currentObject, originalObject); + this.liveQueryPublisher.onCloudCodeAfterDelete(req); + } + }, { + key: 'hasLiveQuery', + value: function hasLiveQuery(className) { + return this.classNames.has(className); + } + }, { + key: '_makePublisherRequest', + value: function _makePublisherRequest(currentObject, originalObject) { + var req = { + object: currentObject + }; + if (currentObject) { + req.original = originalObject; + } + return req; + } + }]); + + return LiveQueryController; +}(); + +exports.default = LiveQueryController; \ No newline at end of file diff --git a/lib/Controllers/LoggerController.js b/lib/Controllers/LoggerController.js new file mode 100644 index 0000000000..841eb4b597 --- /dev/null +++ b/lib/Controllers/LoggerController.js @@ -0,0 +1,276 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.LoggerController = exports.LogOrder = exports.LogLevel = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _node = require('parse/node'); + +var _AdaptableController2 = require('./AdaptableController'); + +var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); + +var _LoggerAdapter = require('../Adapters/Logger/LoggerAdapter'); + +var _url = require('url'); + +var _url2 = _interopRequireDefault(_url); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; +var LOG_STRING_TRUNCATE_LENGTH = 1000; +var truncationMarker = '... (truncated)'; + +var LogLevel = exports.LogLevel = { + INFO: 'info', + ERROR: 'error' +}; + +var LogOrder = exports.LogOrder = { + DESCENDING: 'desc', + ASCENDING: 'asc' +}; + +var LoggerController = exports.LoggerController = function (_AdaptableController) { + _inherits(LoggerController, _AdaptableController); + + function LoggerController() { + _classCallCheck(this, LoggerController); + + return _possibleConstructorReturn(this, (LoggerController.__proto__ || Object.getPrototypeOf(LoggerController)).apply(this, arguments)); + } + + _createClass(LoggerController, [{ + key: 'maskSensitiveUrl', + value: function maskSensitiveUrl(urlString) { + var password = _url2.default.parse(urlString, true).query.password; + + if (password) { + urlString = urlString.replace('password=' + password, 'password=********'); + } + return urlString; + } + }, { + key: 'maskSensitive', + value: function maskSensitive(argArray) { + var _this2 = this; + + return argArray.map(function (e) { + if (!e) { + return e; + } + + if (typeof e === 'string') { + return e.replace(/(password".?:.?")[^"]*"/g, '$1********"'); + } + // else it is an object... + + // check the url + if (e.url) { + // for strings + if (typeof e.url === 'string') { + e.url = _this2.maskSensitiveUrl(e.url); + } else if (Array.isArray(e.url)) { + // for strings in array + e.url = e.url.map(function (item) { + if (typeof item === 'string') { + return _this2.maskSensitiveUrl(item); + } + + return item; + }); + } + } + + if (e.body) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = Object.keys(e.body)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var key = _step.value; + + if (key === 'password') { + e.body[key] = '********'; + break; + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + } + + if (e.params) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = Object.keys(e.params)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var _key = _step2.value; + + if (_key === 'password') { + e.params[_key] = '********'; + break; + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + } + + return e; + }); + } + }, { + key: 'log', + value: function log(level, args) { + // make the passed in arguments object an array with the spread operator + args = this.maskSensitive([].concat(_toConsumableArray(args))); + args = [].concat(level, args); + this.adapter.log.apply(this.adapter, args); + } + }, { + key: 'info', + value: function info() { + return this.log('info', arguments); + } + }, { + key: 'error', + value: function error() { + return this.log('error', arguments); + } + }, { + key: 'warn', + value: function warn() { + return this.log('warn', arguments); + } + }, { + key: 'verbose', + value: function verbose() { + return this.log('verbose', arguments); + } + }, { + key: 'debug', + value: function debug() { + return this.log('debug', arguments); + } + }, { + key: 'silly', + value: function silly() { + return this.log('silly', arguments); + } + // check that date input is valid + + }, { + key: 'truncateLogMessage', + value: function truncateLogMessage(string) { + if (string && string.length > LOG_STRING_TRUNCATE_LENGTH) { + var truncated = string.substring(0, LOG_STRING_TRUNCATE_LENGTH) + truncationMarker; + return truncated; + } + + return string; + } + }, { + key: 'getLogs', + + + // Returns a promise for a {response} object. + // query params: + // level (optional) Level of logging you want to query for (info || error) + // from (optional) Start time for the search. Defaults to 1 week ago. + // until (optional) End time for the search. Defaults to current time. + // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”. + // size (optional) Number of rows returned by search. Defaults to 10 + value: function getLogs() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + if (!this.adapter) { + throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Logger adapter is not available'); + } + if (typeof this.adapter.query !== 'function') { + throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Querying logs is not supported with this adapter'); + } + options = LoggerController.parseOptions(options); + return this.adapter.query(options); + } + }, { + key: 'expectedAdapterType', + value: function expectedAdapterType() { + return _LoggerAdapter.LoggerAdapter; + } + }], [{ + key: 'validDateTime', + value: function validDateTime(date) { + if (!date) { + return null; + } + date = new Date(date); + + if (!isNaN(date.getTime())) { + return date; + } + + return null; + } + }, { + key: 'parseOptions', + value: function parseOptions() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var from = LoggerController.validDateTime(options.from) || new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); + var until = LoggerController.validDateTime(options.until) || new Date(); + var size = Number(options.size) || 10; + var order = options.order || LogOrder.DESCENDING; + var level = options.level || LogLevel.INFO; + + return { + from: from, + until: until, + size: size, + order: order, + level: level + }; + } + }]); + + return LoggerController; +}(_AdaptableController3.default); + +exports.default = LoggerController; \ No newline at end of file diff --git a/lib/Controllers/PushController.js b/lib/Controllers/PushController.js new file mode 100644 index 0000000000..b1f27a77a8 --- /dev/null +++ b/lib/Controllers/PushController.js @@ -0,0 +1,167 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PushController = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _node = require('parse/node'); + +var _deepcopy = require('deepcopy'); + +var _deepcopy2 = _interopRequireDefault(_deepcopy); + +var _RestQuery = require('../RestQuery'); + +var _RestQuery2 = _interopRequireDefault(_RestQuery); + +var _RestWrite = require('../RestWrite'); + +var _RestWrite2 = _interopRequireDefault(_RestWrite); + +var _Auth = require('../Auth'); + +var _StatusHandler = require('../StatusHandler'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var PushController = exports.PushController = function () { + function PushController() { + _classCallCheck(this, PushController); + } + + _createClass(PushController, [{ + key: 'sendPush', + value: function sendPush() { + var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var where = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var config = arguments[2]; + var auth = arguments[3]; + var onPushStatusSaved = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : function () {}; + + if (!config.hasPushSupport) { + throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Missing push configuration'); + } + // Replace the expiration_time and push_time with a valid Unix epoch milliseconds time + body.expiration_time = PushController.getExpirationTime(body); + var push_time = PushController.getPushTime(body); + if (typeof push_time !== 'undefined') { + body['push_time'] = push_time; + } + // TODO: If the req can pass the checking, we return immediately instead of waiting + // pushes to be sent. We probably change this behaviour in the future. + var badgeUpdate = function badgeUpdate() { + return Promise.resolve(); + }; + if (body.data && body.data.badge) { + var badge = body.data.badge; + var restUpdate = {}; + if (typeof badge == 'string' && badge.toLowerCase() === 'increment') { + restUpdate = { badge: { __op: 'Increment', amount: 1 } }; + } else if (Number(badge)) { + restUpdate = { badge: badge }; + } else { + throw "Invalid value for badge, expected number or 'Increment'"; + } + var updateWhere = (0, _deepcopy2.default)(where); + + badgeUpdate = function badgeUpdate() { + updateWhere.deviceType = 'ios'; + // Build a real RestQuery so we can use it in RestWrite + var restQuery = new _RestQuery2.default(config, (0, _Auth.master)(config), '_Installation', updateWhere); + return restQuery.buildRestWhere().then(function () { + var write = new _RestWrite2.default(config, (0, _Auth.master)(config), '_Installation', restQuery.restWhere, restUpdate); + write.runOptions.many = true; + return write.execute(); + }); + }; + } + var pushStatus = (0, _StatusHandler.pushStatusHandler)(config); + return Promise.resolve().then(function () { + return pushStatus.setInitial(body, where); + }).then(function () { + onPushStatusSaved(pushStatus.objectId); + return badgeUpdate(); + }).then(function () { + if (body.hasOwnProperty('push_time') && config.hasPushScheduledSupport) { + return Promise.resolve(); + } + return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus); + }).catch(function (err) { + return pushStatus.fail(err).then(function () { + throw err; + }); + }); + } + + /** + * Get expiration time from the request body. + * @param {Object} request A request object + * @returns {Number|undefined} The expiration time if it exists in the request + */ + + }], [{ + key: 'getExpirationTime', + value: function getExpirationTime() { + var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var hasExpirationTime = body.hasOwnProperty('expiration_time'); + if (!hasExpirationTime) { + return; + } + var expirationTimeParam = body['expiration_time']; + var expirationTime; + if (typeof expirationTimeParam === 'number') { + expirationTime = new Date(expirationTimeParam * 1000); + } else if (typeof expirationTimeParam === 'string') { + expirationTime = new Date(expirationTimeParam); + } else { + throw new _node.Parse.Error(_node.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 _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, body['expiration_time'] + ' is not valid time.'); + } + return expirationTime.valueOf(); + } + + /** + * Get push time from the request body. + * @param {Object} request A request object + * @returns {Number|undefined} The push time if it exists in the request + */ + + }, { + key: 'getPushTime', + value: function getPushTime() { + var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var hasPushTime = body.hasOwnProperty('push_time'); + if (!hasPushTime) { + return; + } + var pushTimeParam = body['push_time']; + var pushTime; + if (typeof pushTimeParam === 'number') { + pushTime = new Date(pushTimeParam * 1000); + } else if (typeof pushTimeParam === 'string') { + pushTime = new Date(pushTimeParam); + } else { + throw new _node.Parse.Error(_node.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(pushTime)) { + throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, body['push_time'] + ' is not valid time.'); + } + return pushTime; + } + }]); + + return PushController; +}(); + +exports.default = PushController; \ No newline at end of file diff --git a/lib/Controllers/SchemaCache.js b/lib/Controllers/SchemaCache.js new file mode 100644 index 0000000000..c78a058267 --- /dev/null +++ b/lib/Controllers/SchemaCache.js @@ -0,0 +1,121 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _cryptoUtils = require("../cryptoUtils"); + +var _defaults = require("../defaults"); + +var _defaults2 = _interopRequireDefault(_defaults); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var MAIN_SCHEMA = "__MAIN_SCHEMA"; +var SCHEMA_CACHE_PREFIX = "__SCHEMA"; +var ALL_KEYS = "__ALL_KEYS"; + +var SchemaCache = function () { + function SchemaCache(cacheController) { + var ttl = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _defaults2.default.schemaCacheTTL; + var singleCache = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + _classCallCheck(this, SchemaCache); + + this.ttl = ttl; + if (typeof ttl == 'string') { + this.ttl = parseInt(ttl); + } + this.cache = cacheController; + this.prefix = SCHEMA_CACHE_PREFIX; + if (!singleCache) { + this.prefix += (0, _cryptoUtils.randomString)(20); + } + } + + _createClass(SchemaCache, [{ + key: "put", + value: function put(key, value) { + var _this = this; + + return this.cache.get(this.prefix + ALL_KEYS).then(function (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)]); + }); + } + }, { + key: "getAllClasses", + value: function getAllClasses() { + if (!this.ttl) { + return Promise.resolve(null); + } + return this.cache.get(this.prefix + MAIN_SCHEMA); + } + }, { + key: "setAllClasses", + value: function setAllClasses(schema) { + if (!this.ttl) { + return Promise.resolve(null); + } + return this.put(this.prefix + MAIN_SCHEMA, schema); + } + }, { + key: "setOneSchema", + value: function setOneSchema(className, schema) { + if (!this.ttl) { + return Promise.resolve(null); + } + return this.put(this.prefix + className, schema); + } + }, { + key: "getOneSchema", + value: function getOneSchema(className) { + var _this2 = this; + + if (!this.ttl) { + return Promise.resolve(null); + } + return this.cache.get(this.prefix + className).then(function (schema) { + if (schema) { + return Promise.resolve(schema); + } + return _this2.cache.get(_this2.prefix + MAIN_SCHEMA).then(function (cachedSchemas) { + cachedSchemas = cachedSchemas || []; + schema = cachedSchemas.find(function (cachedSchema) { + return cachedSchema.className === className; + }); + if (schema) { + return Promise.resolve(schema); + } + return Promise.resolve(null); + }); + }); + } + }, { + key: "clear", + value: function clear() { + var _this3 = this; + + // That clears all caches... + return this.cache.get(this.prefix + ALL_KEYS).then(function (allKeys) { + if (!allKeys) { + return; + } + var promises = Object.keys(allKeys).map(function (key) { + return _this3.cache.del(key); + }); + return Promise.all(promises); + }); + } + }]); + + return SchemaCache; +}(); + +exports.default = SchemaCache; \ No newline at end of file diff --git a/lib/Controllers/SchemaController.js b/lib/Controllers/SchemaController.js new file mode 100644 index 0000000000..d7744cbe26 --- /dev/null +++ b/lib/Controllers/SchemaController.js @@ -0,0 +1,1114 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } + +// This class handles schema validation, persistence, and modification. +// +// Each individual Schema object should be immutable. The helpers to +// do things with the Schema just return a new schema when the schema +// is changed. +// +// The canonical place to store this Schema is in the database itself, +// in a _SCHEMA collection. This is not the right way to do it for an +// open source framework, but it's backward compatible, so we're +// keeping it this way for now. +// +// In API-handling code, you should only use the Schema class via the +// DatabaseController. This will let us replace the schema logic for +// different databases. +// TODO: hide all schema logic inside the database adapter. +var Parse = require('parse/node').Parse; + +var defaultColumns = Object.freeze({ + // Contain the default columns for every parse object type (except _Join collection) + _Default: { + "objectId": { type: 'String' }, + "createdAt": { type: 'Date' }, + "updatedAt": { type: 'Date' }, + "ACL": { type: 'ACL' } + }, + // The additional default columns for the _User collection (in addition to DefaultCols) + _User: { + "username": { type: 'String' }, + "password": { type: 'String' }, + "email": { type: 'String' }, + "emailVerified": { type: 'Boolean' }, + "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' } + }, + // The additional default columns for the _Role collection (in addition to DefaultCols) + _Role: { + "name": { type: 'String' }, + "users": { type: 'Relation', targetClass: '_User' }, + "roles": { type: 'Relation', targetClass: '_Role' } + }, + // The additional default columns for the _Session collection (in addition to DefaultCols) + _Session: { + "restricted": { type: 'Boolean' }, + "user": { type: 'Pointer', targetClass: '_User' }, + "installationId": { type: 'String' }, + "sessionToken": { type: 'String' }, + "expiresAt": { type: 'Date' }, + "createdWith": { type: 'Object' } + }, + _Product: { + "productIdentifier": { type: 'String' }, + "download": { type: 'File' }, + "downloadName": { type: 'String' }, + "icon": { type: 'File' }, + "order": { type: 'Number' }, + "title": { type: 'String' }, + "subtitle": { type: 'String' } + }, + _PushStatus: { + "pushTime": { type: 'String' }, + "source": { type: 'String' }, // rest or webui + "query": { type: 'String' }, // the stringified JSON query + "payload": { type: 'String' }, // the stringified JSON payload, + "title": { type: 'String' }, + "expiry": { type: 'Number' }, + "status": { type: 'String' }, + "numSent": { type: 'Number' }, + "numFailed": { type: 'Number' }, + "pushHash": { type: 'String' }, + "errorMessage": { type: 'Object' }, + "sentPerType": { type: 'Object' }, + "failedPerType": { type: 'Object' }, + "count": { type: 'Number' } + }, + _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' } + }, + _Hooks: { + "functionName": { type: 'String' }, + "className": { type: 'String' }, + "triggerName": { type: 'String' }, + "url": { type: 'String' } + }, + _GlobalConfig: { + "objectId": { type: 'String' }, + "params": { type: 'Object' } + } +}); + +var requiredColumns = Object.freeze({ + _Product: ["productIdentifier", "icon", "order", "title", "subtitle"], + _Role: ["name", "ACL"] +}); + +var systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus', '_JobStatus']); + +var volatileClasses = Object.freeze(['_JobStatus', '_PushStatus', '_Hooks', '_GlobalConfig']); + +// 10 alpha numberic chars + uppercase +var userIdRegex = /^[a-zA-Z0-9]{10}$/; +// Anything that start with role +var roleRegex = /^role:.*/; +// * permission +var publicRegex = /^\*$/; + +var requireAuthenticationRegex = /^requiresAuthentication$/; + +var permissionKeyRegex = Object.freeze([userIdRegex, roleRegex, publicRegex, requireAuthenticationRegex]); + +function verifyPermissionKey(key) { + var result = permissionKeyRegex.reduce(function (isGood, regEx) { + isGood = isGood || key.match(regEx) != null; + return isGood; + }, false); + if (!result) { + throw new Parse.Error(Parse.Error.INVALID_JSON, '\'' + key + '\' is not a valid key for class level permissions'); + } +} + +var CLPValidKeys = Object.freeze(['find', 'count', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']); +function validateCLP(perms, fields) { + if (!perms) { + return; + } + Object.keys(perms).forEach(function (operation) { + if (CLPValidKeys.indexOf(operation) == -1) { + throw new Parse.Error(Parse.Error.INVALID_JSON, operation + ' is not a valid operation for class level permissions'); + } + + if (operation === 'readUserFields' || operation === 'writeUserFields') { + if (!Array.isArray(perms[operation])) { + throw new Parse.Error(Parse.Error.INVALID_JSON, '\'' + perms[operation] + '\' is not a valid value for class level permissions ' + operation); + } else { + perms[operation].forEach(function (key) { + if (!fields[key] || fields[key].type != 'Pointer' || fields[key].targetClass != '_User') { + throw new Parse.Error(Parse.Error.INVALID_JSON, '\'' + key + '\' is not a valid column for class level pointer permissions ' + operation); + } + }); + } + return; + } + + Object.keys(perms[operation]).forEach(function (key) { + verifyPermissionKey(key); + var perm = perms[operation][key]; + if (perm !== true) { + throw new Parse.Error(Parse.Error.INVALID_JSON, '\'' + perm + '\' is not a valid value for class level permissions ' + operation + ':' + key + ':' + perm); + } + }); + }); +} +var joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/; +var classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/; +function classNameIsValid(className) { + // Valid classes must: + return ( + // Be one of _User, _Installation, _Role, _Session OR + systemClasses.indexOf(className) > -1 || + // Be a join table OR + joinClassRegex.test(className) || + // Include only alpha-numeric and underscores, and not start with an underscore or number + fieldNameIsValid(className) + ); +} + +// Valid fields must be alpha-numeric, and not start with an underscore or number +function fieldNameIsValid(fieldName) { + return classAndFieldRegex.test(fieldName); +} + +// Checks that it's not trying to clobber one of the default fields of the class. +function fieldNameIsValidForClass(fieldName, className) { + if (!fieldNameIsValid(fieldName)) { + return false; + } + if (defaultColumns._Default[fieldName]) { + return false; + } + if (defaultColumns[className] && defaultColumns[className][fieldName]) { + return false; + } + return true; +} + +function invalidClassNameMessage(className) { + return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character '; +} + +var invalidJsonError = new Parse.Error(Parse.Error.INVALID_JSON, "invalid JSON"); +var validNonRelationOrPointerTypes = ['Number', 'String', 'Boolean', 'Date', 'Object', 'Array', 'GeoPoint', 'File']; +// Returns an error suitable for throwing if the type is invalid +var fieldTypeIsInvalid = function fieldTypeIsInvalid(_ref) { + var type = _ref.type, + targetClass = _ref.targetClass; + + if (['Pointer', 'Relation'].indexOf(type) >= 0) { + if (!targetClass) { + return new Parse.Error(135, 'type ' + type + ' needs a class name'); + } else if (typeof targetClass !== 'string') { + return invalidJsonError; + } else if (!classNameIsValid(targetClass)) { + return new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(targetClass)); + } else { + return undefined; + } + } + if (typeof type !== 'string') { + return invalidJsonError; + } + if (validNonRelationOrPointerTypes.indexOf(type) < 0) { + return new Parse.Error(Parse.Error.INCORRECT_TYPE, 'invalid field type: ' + type); + } + return undefined; +}; + +var convertSchemaToAdapterSchema = function convertSchemaToAdapterSchema(schema) { + schema = injectDefaultSchema(schema); + delete schema.fields.ACL; + schema.fields._rperm = { type: 'Array' }; + schema.fields._wperm = { type: 'Array' }; + + if (schema.className === '_User') { + delete schema.fields.password; + schema.fields._hashed_password = { type: 'String' }; + } + + return schema; +}; + +var convertAdapterSchemaToParseSchema = function convertAdapterSchemaToParseSchema(_ref2) { + var schema = _objectWithoutProperties(_ref2, []); + + delete schema.fields._rperm; + delete schema.fields._wperm; + + schema.fields.ACL = { type: 'ACL' }; + + if (schema.className === '_User') { + delete schema.fields.authData; //Auth data is implicit + delete schema.fields._hashed_password; + schema.fields.password = { type: 'String' }; + } + + return schema; +}; + +var injectDefaultSchema = function injectDefaultSchema(_ref3) { + var className = _ref3.className, + fields = _ref3.fields, + classLevelPermissions = _ref3.classLevelPermissions; + return { + className: className, + fields: _extends({}, defaultColumns._Default, defaultColumns[className] || {}, fields), + classLevelPermissions: classLevelPermissions + }; +}; + +var _HooksSchema = { className: "_Hooks", fields: defaultColumns._Hooks }; +var _GlobalConfigSchema = { className: "_GlobalConfig", fields: defaultColumns._GlobalConfig }; +var _PushStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ + className: "_PushStatus", + fields: {}, + classLevelPermissions: {} +})); +var _JobStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ + className: "_JobStatus", + fields: {}, + classLevelPermissions: {} +})); +var VolatileClassesSchemas = [_HooksSchema, _JobStatusSchema, _PushStatusSchema, _GlobalConfigSchema]; + +var dbTypeMatchesObjectType = function dbTypeMatchesObjectType(dbType, objectType) { + if (dbType.type !== objectType.type) return false; + if (dbType.targetClass !== objectType.targetClass) return false; + if (dbType === objectType.type) return true; + if (dbType.type === objectType.type) return true; + return false; +}; + +var typeToString = function typeToString(type) { + if (type.targetClass) { + return type.type + '<' + type.targetClass + '>'; + } + return '' + (type.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. + +var SchemaController = function () { + function SchemaController(databaseAdapter, schemaCache) { + _classCallCheck(this, SchemaController); + + this._dbAdapter = databaseAdapter; + this._cache = schemaCache; + // this.data[className][fieldName] tells you the type of that field, in mongo format + this.data = {}; + // this.perms[className][operation] tells you the acl-style permissions + this.perms = {}; + } + + _createClass(SchemaController, [{ + key: 'reloadData', + value: function reloadData() { + var _this = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { clearCache: false }; + + var promise = Promise.resolve(); + if (options.clearCache) { + promise = promise.then(function () { + return _this._cache.clear(); + }); + } + if (this.reloadDataPromise && !options.clearCache) { + return this.reloadDataPromise; + } + this.reloadDataPromise = promise.then(function () { + return _this.getAllClasses(options); + }).then(function (allSchemas) { + var data = {}; + var perms = {}; + allSchemas.forEach(function (schema) { + data[schema.className] = injectDefaultSchema(schema).fields; + perms[schema.className] = schema.classLevelPermissions; + }); + + // Inject the in-memory classes + volatileClasses.forEach(function (className) { + var schema = injectDefaultSchema({ className: className }); + data[className] = schema.fields; + perms[className] = schema.classLevelPermissions; + }); + _this.data = data; + _this.perms = perms; + delete _this.reloadDataPromise; + }, function (err) { + _this.data = {}; + _this.perms = {}; + delete _this.reloadDataPromise; + throw err; + }); + return this.reloadDataPromise; + } + }, { + key: 'getAllClasses', + value: function getAllClasses() { + var _this2 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { clearCache: false }; + + var promise = Promise.resolve(); + if (options.clearCache) { + promise = this._cache.clear(); + } + return promise.then(function () { + return _this2._cache.getAllClasses(); + }).then(function (allClasses) { + if (allClasses && allClasses.length && !options.clearCache) { + return Promise.resolve(allClasses); + } + return _this2._dbAdapter.getAllClasses().then(function (allSchemas) { + return allSchemas.map(injectDefaultSchema); + }).then(function (allSchemas) { + return _this2._cache.setAllClasses(allSchemas).then(function () { + return allSchemas; + }); + }); + }); + } + }, { + key: 'getOneSchema', + value: function getOneSchema(className) { + var _this3 = this; + + var allowVolatileClasses = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : { clearCache: false }; + + var promise = Promise.resolve(); + if (options.clearCache) { + promise = this._cache.clear(); + } + return promise.then(function () { + if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) { + return Promise.resolve({ + className: className, + fields: _this3.data[className], + classLevelPermissions: _this3.perms[className] + }); + } + return _this3._cache.getOneSchema(className).then(function (cached) { + if (cached && !options.clearCache) { + return Promise.resolve(cached); + } + return _this3._dbAdapter.getClass(className).then(injectDefaultSchema).then(function (result) { + return _this3._cache.setOneSchema(className, result).then(function () { + return result; + }); + }); + }); + }); + } + + // Create a new class that includes the three default fields. + // ACL is an implicit column that does not get an entry in the + // _SCHEMAS database. Returns a promise that resolves with the + // created schema, in mongo format. + // on success, and rejects with an error on fail. Ensure you + // have authorization (master key, or client class creation + // enabled) before calling this function. + + }, { + key: 'addClassIfNotExists', + value: function addClassIfNotExists(className) { + var _this4 = this; + + var fields = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var classLevelPermissions = arguments[2]; + + var validationError = this.validateNewClass(className, fields, classLevelPermissions); + if (validationError) { + return Promise.reject(validationError); + } + + return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields: fields, classLevelPermissions: classLevelPermissions, className: className })).then(convertAdapterSchemaToParseSchema).then(function (res) { + return _this4._cache.clear().then(function () { + return Promise.resolve(res); + }); + }).catch(function (error) { + if (error && error.code === Parse.Error.DUPLICATE_VALUE) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class ' + className + ' already exists.'); + } else { + throw error; + } + }); + } + }, { + key: 'updateClass', + value: function updateClass(className, submittedFields, classLevelPermissions, database) { + var _this5 = this; + + return this.getOneSchema(className).then(function (schema) { + var existingFields = schema.fields; + Object.keys(submittedFields).forEach(function (name) { + var field = submittedFields[name]; + if (existingFields[name] && field.__op !== 'Delete') { + throw new Parse.Error(255, 'Field ' + name + ' exists, cannot update.'); + } + if (!existingFields[name] && field.__op === 'Delete') { + throw new Parse.Error(255, 'Field ' + name + ' does not exist, cannot delete.'); + } + }); + + delete existingFields._rperm; + delete existingFields._wperm; + var newSchema = buildMergedSchemaObject(existingFields, submittedFields); + var validationError = _this5.validateSchemaData(className, newSchema, classLevelPermissions, Object.keys(existingFields)); + if (validationError) { + throw new Parse.Error(validationError.code, validationError.error); + } + + // Finally we have checked to make sure the request is valid and we can start deleting fields. + // Do all deletions first, then a single save to _SCHEMA collection to handle all additions. + var deletedFields = []; + var insertedFields = []; + Object.keys(submittedFields).forEach(function (fieldName) { + if (submittedFields[fieldName].__op === 'Delete') { + deletedFields.push(fieldName); + } else { + insertedFields.push(fieldName); + } + }); + + var deletePromise = Promise.resolve(); + if (deletedFields.length > 0) { + deletePromise = _this5.deleteFields(deletedFields, className, database); + } + + return deletePromise // Delete Everything + .then(function () { + return _this5.reloadData({ clearCache: true }); + }) // Reload our Schema, so we have all the new values + .then(function () { + var promises = insertedFields.map(function (fieldName) { + var type = submittedFields[fieldName]; + return _this5.enforceFieldExists(className, fieldName, type); + }); + return Promise.all(promises); + }).then(function () { + return _this5.setPermissions(className, classLevelPermissions, newSchema); + }) + //TODO: Move this logic into the database adapter + .then(function () { + return { + className: className, + fields: _this5.data[className], + classLevelPermissions: _this5.perms[className] + }; + }); + }).catch(function (error) { + if (error === undefined) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class ' + className + ' does not exist.'); + } else { + throw error; + } + }); + } + + // Returns a promise that resolves successfully to the new schema + // object or fails with a reason. + + }, { + key: 'enforceClassExists', + value: function enforceClassExists(className) { + var _this6 = this; + + if (this.data[className]) { + return Promise.resolve(this); + } + // We don't have this class. Update the schema + return this.addClassIfNotExists(className) + // The schema update succeeded. Reload the schema + .then(function () { + return _this6.reloadData({ clearCache: true }); + }).catch(function () { + // 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 _this6.reloadData({ clearCache: true }); + }).then(function () { + // Ensure that the schema now validates + if (_this6.data[className]) { + return _this6; + } else { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'Failed to add ' + className); + } + }).catch(function () { + // The schema still doesn't validate. Give up + throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate'); + }); + } + }, { + key: 'validateNewClass', + value: function validateNewClass(className) { + var fields = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + var classLevelPermissions = arguments[2]; + + if (this.data[className]) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class ' + className + ' already exists.'); + } + if (!classNameIsValid(className)) { + return { + code: Parse.Error.INVALID_CLASS_NAME, + error: invalidClassNameMessage(className) + }; + } + return this.validateSchemaData(className, fields, classLevelPermissions, []); + } + }, { + key: 'validateSchemaData', + value: function validateSchemaData(className, fields, classLevelPermissions, existingFieldNames) { + for (var fieldName in fields) { + if (existingFieldNames.indexOf(fieldName) < 0) { + if (!fieldNameIsValid(fieldName)) { + return { + code: Parse.Error.INVALID_KEY_NAME, + error: 'invalid field name: ' + fieldName + }; + } + if (!fieldNameIsValidForClass(fieldName, className)) { + return { + code: 136, + error: 'field ' + fieldName + ' cannot be added' + }; + } + var error = fieldTypeIsInvalid(fields[fieldName]); + if (error) return { code: error.code, error: error.message }; + } + } + + for (var _fieldName in defaultColumns[className]) { + fields[_fieldName] = defaultColumns[className][_fieldName]; + } + + var geoPoints = Object.keys(fields).filter(function (key) { + return fields[key] && fields[key].type === 'GeoPoint'; + }); + if (geoPoints.length > 1) { + return { + code: Parse.Error.INCORRECT_TYPE, + error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.' + }; + } + validateCLP(classLevelPermissions, fields); + } + + // Sets the Class-level permissions for a given className, which must exist. + + }, { + key: 'setPermissions', + value: function setPermissions(className, perms, newSchema) { + var _this7 = this; + + if (typeof perms === 'undefined') { + return Promise.resolve(); + } + validateCLP(perms, newSchema); + return this._dbAdapter.setClassLevelPermissions(className, perms).then(function () { + return _this7.reloadData({ clearCache: true }); + }); + } + + // Returns a promise that resolves successfully to the new schema + // object if the provided className-fieldName-type tuple is valid. + // The className must already be validated. + // If 'freeze' is true, refuse to update the schema for this field. + + }, { + key: 'enforceFieldExists', + value: function enforceFieldExists(className, fieldName, type) { + var _this8 = this; + + if (fieldName.indexOf(".") > 0) { + // subdocument key (x.y) => ok if x is of type 'object' + fieldName = fieldName.split(".")[0]; + type = 'Object'; + } + if (!fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid field name: ' + fieldName + '.'); + } + + // If someone tries to create a new field with null/undefined as the value, return; + if (!type) { + return Promise.resolve(this); + } + + return this.reloadData().then(function () { + var expectedType = _this8.getExpectedType(className, fieldName); + if (typeof type === 'string') { + type = { type: type }; + } + + if (expectedType) { + if (!dbTypeMatchesObjectType(expectedType, type)) { + throw new Parse.Error(Parse.Error.INCORRECT_TYPE, 'schema mismatch for ' + className + '.' + fieldName + '; expected ' + typeToString(expectedType) + ' but got ' + typeToString(type)); + } + return _this8; + } + + return _this8._dbAdapter.addFieldIfNotExists(className, fieldName, type).then(function () { + // The update succeeded. Reload the schema + return _this8.reloadData({ clearCache: true }); + }, function () { + //TODO: introspect the error and only reload if the error is one for which is makes sense to reload + + // The update failed. This can be okay - it might have been a race + // condition where another client updated the schema in the same + // way that we wanted to. So, just reload the schema + return _this8.reloadData({ clearCache: true }); + }).then(function () { + // Ensure that the schema now validates + if (!dbTypeMatchesObjectType(_this8.getExpectedType(className, fieldName), type)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'Could not add field ' + fieldName); + } + // Remove the cached schema + _this8._cache.clear(); + return _this8; + }); + }); + } + + // maintain compatibility + + }, { + key: 'deleteField', + value: function deleteField(fieldName, className, database) { + return this.deleteFields([fieldName], className, database); + } + + // Delete fields, and remove that data from all objects. This is intended + // to remove unused fields, if other writers are writing objects that include + // this field, the field may reappear. Returns a Promise that resolves with + // no object on success, or rejects with { code, error } on failure. + // Passing the database and prefix is necessary in order to drop relation collections + // and remove fields from objects. Ideally the database would belong to + // a database adapter and this function would close over it or access it via member. + + }, { + key: 'deleteFields', + value: function deleteFields(fieldNames, className, database) { + var _this9 = this; + + if (!classNameIsValid(className)) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className)); + } + + fieldNames.forEach(function (fieldName) { + if (!fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid field name: ' + fieldName); + } + //Don't allow deleting the default fields. + if (!fieldNameIsValidForClass(fieldName, className)) { + throw new Parse.Error(136, 'field ' + fieldName + ' cannot be changed'); + } + }); + + return this.getOneSchema(className, false, { clearCache: true }).catch(function (error) { + if (error === undefined) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class ' + className + ' does not exist.'); + } else { + throw error; + } + }).then(function (schema) { + fieldNames.forEach(function (fieldName) { + if (!schema.fields[fieldName]) { + throw new Parse.Error(255, 'Field ' + fieldName + ' does not exist, cannot delete.'); + } + }); + + var schemaFields = _extends({}, schema.fields); + return database.adapter.deleteFields(className, schema, fieldNames).then(function () { + return Promise.all(fieldNames.map(function (fieldName) { + var 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(function () { + _this9._cache.clear(); + }); + } + + // Validates an object provided in REST format. + // Returns a promise that resolves to the new schema if this object is + // valid. + + }, { + key: 'validateObject', + value: function validateObject(className, object, query) { + var geocount = 0; + var promise = this.enforceClassExists(className); + + var _loop = function _loop(fieldName) { + if (object[fieldName] === undefined) { + return 'continue'; + } + var expected = getType(object[fieldName]); + if (expected === 'GeoPoint') { + geocount++; + } + if (geocount > 1) { + // Make sure all field validation operations run before we return. + // If not - we are continuing to run logic, but already provided response from the server. + return { + v: promise.then(function () { + return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE, 'there can only be one geopoint field in a class')); + }) + }; + } + if (!expected) { + return 'continue'; + } + if (fieldName === 'ACL') { + // Every object has ACL implicitly. + return 'continue'; + } + + promise = promise.then(function (schema) { + return schema.enforceFieldExists(className, fieldName, expected); + }); + }; + + for (var fieldName in object) { + var _ret = _loop(fieldName); + + switch (_ret) { + case 'continue': + continue; + + default: + if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; + } + } + promise = thenValidateRequiredColumns(promise, className, object, query); + return promise; + } + + // Validates that all the properties are set for the object + + }, { + key: 'validateRequiredColumns', + value: function validateRequiredColumns(className, object, query) { + var columns = requiredColumns[className]; + if (!columns || columns.length == 0) { + return Promise.resolve(this); + } + + var missingColumns = columns.filter(function (column) { + if (query && query.objectId) { + if (object[column] && _typeof(object[column]) === "object") { + // Trying to delete a required column + return object[column].__op == 'Delete'; + } + // Not trying to do anything there + return false; + } + return !object[column]; + }); + + if (missingColumns.length > 0) { + throw new Parse.Error(Parse.Error.INCORRECT_TYPE, missingColumns[0] + ' is required.'); + } + return Promise.resolve(this); + } + + // Validates the base CLP for an operation + + }, { + key: 'testBaseCLP', + value: function testBaseCLP(className, aclGroup, operation) { + if (!this.perms[className] || !this.perms[className][operation]) { + return true; + } + var classPerms = this.perms[className]; + var perms = classPerms[operation]; + // Handle the public scenario quickly + if (perms['*']) { + return true; + } + // Check permissions against the aclGroup provided (array of userId/roles) + if (aclGroup.some(function (acl) { + return perms[acl] === true; + })) { + return true; + } + return false; + } + + // Validates an operation passes class-level-permissions set in the schema + + }, { + key: 'validatePermission', + value: function validatePermission(className, aclGroup, operation) { + if (this.testBaseCLP(className, aclGroup, operation)) { + return Promise.resolve(); + } + + if (!this.perms[className] || !this.perms[className][operation]) { + return true; + } + var classPerms = this.perms[className]; + var perms = classPerms[operation]; + + // If only for authenticated users + // make sure we have an aclGroup + 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.'); + } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { + 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' + return Promise.resolve(); + } + + // No matching CLP, let's check the Pointer permissions + // And handle those later + var 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 + '.'); + } + + // Process the readUserFields later + if (Array.isArray(classPerms[permissionField]) && classPerms[permissionField].length > 0) { + return Promise.resolve(); + } + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied for action ' + operation + ' on class ' + className + '.'); + } + + // Returns the expected type for a className+key combination + // or undefined if the schema is not set + + }, { + key: 'getExpectedType', + value: function getExpectedType(className, fieldName) { + if (this.data && this.data[className]) { + var expectedType = this.data[className][fieldName]; + return expectedType === 'map' ? 'Object' : expectedType; + } + return undefined; + } + + // Checks if a given class is in the schema. + + }, { + key: 'hasClass', + value: function hasClass(className) { + var _this10 = this; + + return this.reloadData().then(function () { + return !!_this10.data[className]; + }); + } + }]); + + return SchemaController; +}(); + +// Returns a promise for a new Schema. + + +exports.default = SchemaController; +var load = function load(dbAdapter, schemaCache, options) { + var schema = new SchemaController(dbAdapter, schemaCache); + return schema.reloadData(options).then(function () { + return schema; + }); +}; + +// Builds a new schema (in schema API response format) out of an +// existing mongo schema + a schemas API put request. This response +// does not include the default fields, as it is intended to be passed +// to mongoSchemaFromFieldsAndClassName. No validation is done here, it +// is done in mongoSchemaFromFieldsAndClassName. +function buildMergedSchemaObject(existingFields, putRequest) { + var newSchema = {}; + var sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]); + for (var oldField in existingFields) { + if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') { + if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) { + continue; + } + var fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'; + if (!fieldIsDeleted) { + newSchema[oldField] = existingFields[oldField]; + } + } + } + for (var newField in putRequest) { + if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') { + if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) { + continue; + } + newSchema[newField] = putRequest[newField]; + } + } + return newSchema; +} + +// Given a schema promise, construct another schema promise that +// validates this field once the schema loads. +function thenValidateRequiredColumns(schemaPromise, className, object, query) { + return schemaPromise.then(function (schema) { + return schema.validateRequiredColumns(className, object, query); + }); +} + +// Gets the type from a REST API formatted object, where 'type' is +// extended past javascript types to include the rest of the Parse +// type system. +// The output should be a valid schema value. +// TODO: ensure that this is compatible with the format used in Open DB +function getType(obj) { + var type = typeof obj === 'undefined' ? 'undefined' : _typeof(obj); + switch (type) { + case 'boolean': + return 'Boolean'; + case 'string': + return 'String'; + case 'number': + return 'Number'; + case 'map': + case 'object': + if (!obj) { + return undefined; + } + return getObjectType(obj); + case 'function': + case 'symbol': + case 'undefined': + default: + throw 'bad obj: ' + obj; + } +} + +// This gets the type for non-JSON types like pointers and files, but +// also gets the appropriate type for $ operators. +// Returns null if the type is unknown. +function getObjectType(obj) { + if (obj instanceof Array) { + return 'Array'; + } + if (obj.__type) { + switch (obj.__type) { + case 'Pointer': + if (obj.className) { + return { + type: 'Pointer', + targetClass: obj.className + }; + } + 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; + } + break; + } + throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid " + obj.__type); + } + if (obj['$ne']) { + return getObjectType(obj['$ne']); + } + if (obj.__op) { + switch (obj.__op) { + case 'Increment': + return 'Number'; + case 'Delete': + return null; + case 'Add': + case 'AddUnique': + case 'Remove': + return 'Array'; + case 'AddRelation': + case 'RemoveRelation': + return { + type: 'Relation', + targetClass: obj.objects[0].className + }; + case 'Batch': + return getObjectType(obj.ops[0]); + default: + throw 'unexpected op: ' + obj.__op; + } + } + return 'Object'; +} + +exports.load = load; +exports.classNameIsValid = classNameIsValid; +exports.fieldNameIsValid = fieldNameIsValid; +exports.invalidClassNameMessage = invalidClassNameMessage; +exports.buildMergedSchemaObject = buildMergedSchemaObject; +exports.systemClasses = systemClasses; +exports.defaultColumns = defaultColumns; +exports.convertSchemaToAdapterSchema = convertSchemaToAdapterSchema; +exports.VolatileClassesSchemas = VolatileClassesSchemas; \ No newline at end of file diff --git a/lib/Controllers/UserController.js b/lib/Controllers/UserController.js new file mode 100644 index 0000000000..3519a22b25 --- /dev/null +++ b/lib/Controllers/UserController.js @@ -0,0 +1,318 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.UserController = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _cryptoUtils = require('../cryptoUtils'); + +var _triggers = require('../triggers'); + +var _AdaptableController2 = require('./AdaptableController'); + +var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); + +var _MailAdapter = require('../Adapters/Email/MailAdapter'); + +var _MailAdapter2 = _interopRequireDefault(_MailAdapter); + +var _rest = require('../rest'); + +var _rest2 = _interopRequireDefault(_rest); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var RestQuery = require('../RestQuery'); +var RestWrite = require('../RestWrite'); +var Auth = require('../Auth'); + +var UserController = exports.UserController = function (_AdaptableController) { + _inherits(UserController, _AdaptableController); + + function UserController(adapter, appId) { + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + + _classCallCheck(this, UserController); + + return _possibleConstructorReturn(this, (UserController.__proto__ || Object.getPrototypeOf(UserController)).call(this, adapter, appId, options)); + } + + _createClass(UserController, [{ + key: 'validateAdapter', + value: function validateAdapter(adapter) { + // Allow no adapter + if (!adapter && !this.shouldVerifyEmails) { + return; + } + _get(UserController.prototype.__proto__ || Object.getPrototypeOf(UserController.prototype), 'validateAdapter', this).call(this, adapter); + } + }, { + key: 'expectedAdapterType', + value: function expectedAdapterType() { + return _MailAdapter2.default; + } + }, { + key: 'setEmailVerifyToken', + value: function setEmailVerifyToken(user) { + if (this.shouldVerifyEmails) { + user._email_verify_token = (0, _cryptoUtils.randomString)(25); + user.emailVerified = false; + + if (this.config.emailVerifyTokenValidityDuration) { + user._email_verify_token_expires_at = _node2.default._encode(this.config.generateEmailVerifyTokenExpiresAt()); + } + } + } + }, { + key: 'verifyEmail', + value: function verifyEmail(username, token) { + var _this2 = this; + + if (!this.shouldVerifyEmails) { + // Trying to verify email when not enabled + // TODO: Better error here. + throw undefined; + } + + var query = { username: username, _email_verify_token: token }; + var 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 + if (this.config.emailVerifyTokenValidityDuration) { + query.emailVerified = false; + query._email_verify_token_expires_at = { $gt: _node2.default._encode(new Date()) }; + + updateFields._email_verify_token_expires_at = { __op: 'Delete' }; + } + + var checkIfAlreadyVerified = new RestQuery(this.config, Auth.master(this.config), '_User', { username: username, emailVerified: true }); + return checkIfAlreadyVerified.execute().then(function (result) { + if (result.results.length) { + return Promise.resolve(result.results.length[0]); + } + return new RestWrite(_this2.config, Auth.master(_this2.config), '_User', query, updateFields).execute(); + }); + } + }, { + key: 'checkResetTokenValidity', + value: function checkResetTokenValidity(username, token) { + var _this3 = this; + + return this.config.database.find('_User', { + username: username, + _perishable_token: token + }, { limit: 1 }).then(function (results) { + if (results.length != 1) { + throw undefined; + } + + if (_this3.config.passwordPolicy && _this3.config.passwordPolicy.resetTokenValidityDuration) { + var 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'; + } + + return results[0]; + }); + } + }, { + key: 'getUserIfNeeded', + value: function getUserIfNeeded(user) { + if (user.username && user.email) { + return Promise.resolve(user); + } + var where = {}; + if (user.username) { + where.username = user.username; + } + if (user.email) { + where.email = user.email; + } + + 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]; + }); + } + }, { + key: 'sendVerificationEmail', + value: function sendVerificationEmail(user) { + var _this4 = this; + + if (!this.shouldVerifyEmails) { + return; + } + var token = encodeURIComponent(user._email_verify_token); + // We may need to fetch the user in case of update email + this.getUserIfNeeded(user).then(function (user) { + var username = encodeURIComponent(user.username); + + var link = buildEmailLink(_this4.config.verifyEmailURL, username, token, _this4.config); + var options = { + appName: _this4.config.appName, + link: link, + user: (0, _triggers.inflate)('_User', user) + }; + if (_this4.adapter.sendVerificationEmail) { + _this4.adapter.sendVerificationEmail(options); + } else { + _this4.adapter.sendMail(_this4.defaultVerificationEmail(options)); + } + }); + } + }, { + key: 'resendVerificationEmail', + value: function resendVerificationEmail(username) { + var _this5 = this; + + return this.getUserIfNeeded({ username: username }).then(function (aUser) { + if (!aUser || aUser.emailVerified) { + throw undefined; + } + _this5.setEmailVerifyToken(aUser); + return _this5.config.database.update('_User', { username: username }, aUser).then(function () { + _this5.sendVerificationEmail(aUser); + }); + }); + } + }, { + key: 'setPasswordResetToken', + value: function setPasswordResetToken(email) { + var token = { _perishable_token: (0, _cryptoUtils.randomString)(25) }; + + if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { + token._perishable_token_expires_at = _node2.default._encode(this.config.generatePasswordResetTokenExpiresAt()); + } + + return this.config.database.update('_User', { $or: [{ email: email }, { username: email, email: { $exists: false } }] }, token, {}, true); + } + }, { + key: 'sendPasswordResetEmail', + value: function sendPasswordResetEmail(email) { + var _this6 = this; + + if (!this.adapter) { + throw "Trying to send a reset password but no adapter is set"; + // TODO: No adapter? + } + + return this.setPasswordResetToken(email).then(function (user) { + var token = encodeURIComponent(user._perishable_token); + var username = encodeURIComponent(user.username); + + var link = buildEmailLink(_this6.config.requestResetPasswordURL, username, token, _this6.config); + var options = { + appName: _this6.config.appName, + link: link, + user: (0, _triggers.inflate)('_User', user) + }; + + if (_this6.adapter.sendPasswordResetEmail) { + _this6.adapter.sendPasswordResetEmail(options); + } else { + _this6.adapter.sendMail(_this6.defaultResetPasswordEmail(options)); + } + + return Promise.resolve(user); + }); + } + }, { + key: 'updatePassword', + value: function updatePassword(username, token, password) { + var _this7 = this; + + return this.checkResetTokenValidity(username, token).then(function (user) { + return updateUserPassword(user.objectId, password, _this7.config); + }) + // clear reset password token + .then(function () { + return _this7.config.database.update('_User', { username: username }, { + _perishable_token: { __op: 'Delete' }, + _perishable_token_expires_at: { __op: 'Delete' } + }); + }).catch(function (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); + } + }); + } + }, { + key: 'defaultVerificationEmail', + value: function defaultVerificationEmail(_ref) { + var link = _ref.link, + user = _ref.user, + appName = _ref.appName; + + var 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; + var to = user.get("email"); + var subject = 'Please verify your e-mail for ' + appName; + return { text: text, to: to, subject: subject }; + } + }, { + key: 'defaultResetPasswordEmail', + value: function defaultResetPasswordEmail(_ref2) { + var link = _ref2.link, + user = _ref2.user, + appName = _ref2.appName; + + var text = "Hi,\n\n" + "You requested to reset your password for " + appName + ".\n\n" + "" + "Click here to reset it:\n" + link; + var to = user.get("email") || user.get('username'); + var subject = 'Password Reset for ' + appName; + return { text: text, to: to, subject: subject }; + } + }, { + key: 'shouldVerifyEmails', + get: function get() { + return this.options.verifyUserEmails; + } + }]); + + return UserController; +}(_AdaptableController3.default); + +// Mark this private + + +function updateUserPassword(userId, password, config) { + return _rest2.default.update(config, Auth.master(config), '_User', userId, { + password: password + }); +} + +function buildEmailLink(destination, username, token, config) { + var usernameAndToken = 'token=' + token + '&username=' + username; + + if (config.parseFrameURL) { + var destinationWithoutHost = destination.replace(config.publicServerURL, ''); + + return config.parseFrameURL + '?link=' + encodeURIComponent(destinationWithoutHost) + '&' + usernameAndToken; + } else { + return destination + '?' + usernameAndToken; + } +} + +exports.default = UserController; \ No newline at end of file diff --git a/lib/LiveQuery/Client.js b/lib/LiveQuery/Client.js new file mode 100644 index 0000000000..6ac2e908d3 --- /dev/null +++ b/lib/LiveQuery/Client.js @@ -0,0 +1,158 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Client = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _logger = require('../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var dafaultFields = ['className', 'objectId', 'updatedAt', 'createdAt', 'ACL']; + +var Client = function () { + function Client(id, parseWebSocket) { + _classCallCheck(this, Client); + + this.id = id; + this.parseWebSocket = parseWebSocket; + this.roles = []; + this.subscriptionInfos = new Map(); + this.pushConnect = this._pushEvent('connected'); + this.pushSubscribe = this._pushEvent('subscribed'); + this.pushUnsubscribe = this._pushEvent('unsubscribed'); + this.pushCreate = this._pushEvent('create'); + this.pushEnter = this._pushEvent('enter'); + this.pushUpdate = this._pushEvent('update'); + this.pushDelete = this._pushEvent('delete'); + this.pushLeave = this._pushEvent('leave'); + } + + _createClass(Client, [{ + key: 'addSubscriptionInfo', + value: function addSubscriptionInfo(requestId, subscriptionInfo) { + this.subscriptionInfos.set(requestId, subscriptionInfo); + } + }, { + key: 'getSubscriptionInfo', + value: function getSubscriptionInfo(requestId) { + return this.subscriptionInfos.get(requestId); + } + }, { + key: 'deleteSubscriptionInfo', + value: function deleteSubscriptionInfo(requestId) { + return this.subscriptionInfos.delete(requestId); + } + }, { + key: '_pushEvent', + value: function _pushEvent(type) { + return function (subscriptionId, parseObjectJSON) { + var response = { + 'op': type, + 'clientId': this.id + }; + if (typeof subscriptionId !== 'undefined') { + response['requestId'] = subscriptionId; + } + if (typeof parseObjectJSON !== 'undefined') { + var fields = void 0; + if (this.subscriptionInfos.has(subscriptionId)) { + fields = this.subscriptionInfos.get(subscriptionId).fields; + } + response['object'] = this._toJSONWithFields(parseObjectJSON, fields); + } + Client.pushResponse(this.parseWebSocket, JSON.stringify(response)); + }; + } + }, { + key: '_toJSONWithFields', + value: function _toJSONWithFields(parseObjectJSON, fields) { + if (!fields) { + return parseObjectJSON; + } + var limitedParseObject = {}; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = dafaultFields[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var field = _step.value; + + limitedParseObject[field] = parseObjectJSON[field]; + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = fields[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var _field = _step2.value; + + if (_field in parseObjectJSON) { + limitedParseObject[_field] = parseObjectJSON[_field]; + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + return limitedParseObject; + } + }], [{ + key: 'pushResponse', + value: function pushResponse(parseWebSocket, message) { + _logger2.default.verbose('Push Response : %j', message); + parseWebSocket.send(message); + } + }, { + key: 'pushError', + value: function pushError(parseWebSocket, code, error) { + var reconnect = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; + + Client.pushResponse(parseWebSocket, JSON.stringify({ + 'op': 'error', + 'error': error, + 'code': code, + 'reconnect': reconnect + })); + } + }]); + + return Client; +}(); + +exports.Client = Client; \ No newline at end of file diff --git a/lib/LiveQuery/Id.js b/lib/LiveQuery/Id.js new file mode 100644 index 0000000000..048cc21084 --- /dev/null +++ b/lib/LiveQuery/Id.js @@ -0,0 +1,34 @@ +'use strict'; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Id = function () { + function Id(className, objectId) { + _classCallCheck(this, Id); + + this.className = className; + this.objectId = objectId; + } + + _createClass(Id, [{ + key: 'toString', + value: function toString() { + return this.className + ':' + this.objectId; + } + }], [{ + key: 'fromString', + value: function fromString(str) { + var split = str.split(':'); + if (split.length !== 2) { + throw new TypeError('Cannot create Id object from this string'); + } + return new Id(split[0], split[1]); + } + }]); + + return Id; +}(); + +module.exports = Id; \ No newline at end of file diff --git a/lib/LiveQuery/ParseCloudCodePublisher.js b/lib/LiveQuery/ParseCloudCodePublisher.js new file mode 100644 index 0000000000..9f1bf1d7b1 --- /dev/null +++ b/lib/LiveQuery/ParseCloudCodePublisher.js @@ -0,0 +1,67 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ParseCloudCodePublisher = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _ParsePubSub = require('./ParsePubSub'); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _logger = require('../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ParseCloudCodePublisher = function () { + + // config object of the publisher, right now it only contains the redisURL, + // but we may extend it later. + function ParseCloudCodePublisher() { + var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + _classCallCheck(this, ParseCloudCodePublisher); + + this.parsePublisher = _ParsePubSub.ParsePubSub.createPublisher(config); + } + + _createClass(ParseCloudCodePublisher, [{ + key: 'onCloudCodeAfterSave', + value: function onCloudCodeAfterSave(request) { + this._onCloudCodeMessage(_node2.default.applicationId + 'afterSave', request); + } + }, { + key: 'onCloudCodeAfterDelete', + value: function onCloudCodeAfterDelete(request) { + this._onCloudCodeMessage(_node2.default.applicationId + 'afterDelete', request); + } + + // Request is the request object from cloud code functions. request.object is a ParseObject. + + }, { + key: '_onCloudCodeMessage', + value: function _onCloudCodeMessage(type, request) { + _logger2.default.verbose('Raw request from cloud code current : %j | original : %j', request.object, request.original); + // We need the full JSON which includes className + var message = { + currentParseObject: request.object._toFullJSON() + }; + if (request.original) { + message.originalParseObject = request.original._toFullJSON(); + } + this.parsePublisher.publish(type, JSON.stringify(message)); + } + }]); + + return ParseCloudCodePublisher; +}(); + +exports.ParseCloudCodePublisher = ParseCloudCodePublisher; \ No newline at end of file diff --git a/lib/LiveQuery/ParseLiveQueryServer.js b/lib/LiveQuery/ParseLiveQueryServer.js new file mode 100644 index 0000000000..eb2792ebdb --- /dev/null +++ b/lib/LiveQuery/ParseLiveQueryServer.js @@ -0,0 +1,821 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ParseLiveQueryServer = undefined; + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _tv = require('tv4'); + +var _tv2 = _interopRequireDefault(_tv); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _Subscription = require('./Subscription'); + +var _Client = require('./Client'); + +var _ParseWebSocketServer = require('./ParseWebSocketServer'); + +var _logger = require('../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +var _RequestSchema = require('./RequestSchema'); + +var _RequestSchema2 = _interopRequireDefault(_RequestSchema); + +var _QueryTools = require('./QueryTools'); + +var _ParsePubSub = require('./ParsePubSub'); + +var _SessionTokenCache = require('./SessionTokenCache'); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var ParseLiveQueryServer = function () { + // className -> (queryHash -> subscription) + function ParseLiveQueryServer(server, config) { + var _this = this; + + _classCallCheck(this, ParseLiveQueryServer); + + this.clientId = 0; + this.clients = new Map(); + this.subscriptions = new Map(); + + config = config || {}; + + // Store keys, convert obj to map + var keyPairs = config.keyPairs || {}; + this.keyPairs = new Map(); + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = Object.keys(keyPairs)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var key = _step.value; + + this.keyPairs.set(key, keyPairs[key]); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + _logger2.default.verbose('Support key pairs', this.keyPairs); + + // Initialize Parse + _node2.default.Object.disableSingleInstance(); + + var serverURL = config.serverURL || _node2.default.serverURL; + _node2.default.serverURL = serverURL; + var appId = config.appId || _node2.default.applicationId; + var javascriptKey = _node2.default.javaScriptKey; + var masterKey = config.masterKey || _node2.default.masterKey; + _node2.default.initialize(appId, javascriptKey, masterKey); + + // Initialize websocket server + this.parseWebSocketServer = new _ParseWebSocketServer.ParseWebSocketServer(server, function (parseWebsocket) { + return _this._onConnect(parseWebsocket); + }, config.websocketTimeout); + + // Initialize subscriber + this.subscriber = _ParsePubSub.ParsePubSub.createSubscriber(config); + this.subscriber.subscribe(_node2.default.applicationId + 'afterSave'); + this.subscriber.subscribe(_node2.default.applicationId + 'afterDelete'); + // Register message handler for subscriber. When publisher get messages, it will publish message + // to the subscribers and the handler will be called. + this.subscriber.on('message', function (channel, messageStr) { + _logger2.default.verbose('Subscribe messsage %j', messageStr); + var message = void 0; + try { + message = JSON.parse(messageStr); + } catch (e) { + _logger2.default.error('unable to parse message', messageStr, e); + return; + } + _this._inflateParseObject(message); + if (channel === _node2.default.applicationId + 'afterSave') { + _this._onAfterSave(message); + } else if (channel === _node2.default.applicationId + 'afterDelete') { + _this._onAfterDelete(message); + } else { + _logger2.default.error('Get message %s from unknown channel %j', message, channel); + } + }); + + // Initialize sessionToken cache + this.sessionTokenCache = new _SessionTokenCache.SessionTokenCache(config.cacheTimeout); + } + + // Message is the JSON object from publisher. Message.currentParseObject is the ParseObject JSON after changes. + // Message.originalParseObject is the original ParseObject JSON. + + // The subscriber we use to get object update from publisher + + + _createClass(ParseLiveQueryServer, [{ + key: '_inflateParseObject', + value: function _inflateParseObject(message) { + // Inflate merged object + var currentParseObject = message.currentParseObject; + var className = currentParseObject.className; + var parseObject = new _node2.default.Object(className); + parseObject._finishFetch(currentParseObject); + message.currentParseObject = parseObject; + // Inflate original object + var originalParseObject = message.originalParseObject; + if (originalParseObject) { + className = originalParseObject.className; + parseObject = new _node2.default.Object(className); + parseObject._finishFetch(originalParseObject); + message.originalParseObject = parseObject; + } + } + + // Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes. + // Message.originalParseObject is the original ParseObject. + + }, { + key: '_onAfterDelete', + value: function _onAfterDelete(message) { + var _this2 = this; + + _logger2.default.verbose(_node2.default.applicationId + 'afterDelete is triggered'); + + var deletedParseObject = message.currentParseObject.toJSON(); + var className = deletedParseObject.className; + _logger2.default.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id); + _logger2.default.verbose('Current client number : %d', this.clients.size); + + var classSubscriptions = this.subscriptions.get(className); + if (typeof classSubscriptions === 'undefined') { + _logger2.default.debug('Can not find subscriptions under this class ' + className); + return; + } + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = classSubscriptions.values()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var subscription = _step2.value; + + var isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription); + if (!isSubscriptionMatched) { + continue; + } + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + var _loop = function _loop() { + var _step3$value = _slicedToArray(_step3.value, 2), + clientId = _step3$value[0], + requestIds = _step3$value[1]; + + var client = _this2.clients.get(clientId); + if (typeof client === 'undefined') { + return 'continue'; + } + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + var _loop2 = function _loop2() { + var requestId = _step4.value; + + var acl = message.currentParseObject.getACL(); + // Check ACL + _this2._matchesACL(acl, client, requestId).then(function (isMatched) { + if (!isMatched) { + return null; + } + client.pushDelete(requestId, deletedParseObject); + }, function (error) { + _logger2.default.error('Matching ACL error : ', error); + }); + }; + + for (var _iterator4 = requestIds[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + _loop2(); + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + }; + + for (var _iterator3 = _lodash2.default.entries(subscription.clientRequestIds)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var _ret = _loop(); + + if (_ret === 'continue') continue; + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + } + + // Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes. + // Message.originalParseObject is the original ParseObject. + + }, { + key: '_onAfterSave', + value: function _onAfterSave(message) { + var _this3 = this; + + _logger2.default.verbose(_node2.default.applicationId + 'afterSave is triggered'); + + var originalParseObject = null; + if (message.originalParseObject) { + originalParseObject = message.originalParseObject.toJSON(); + } + var currentParseObject = message.currentParseObject.toJSON(); + var className = currentParseObject.className; + _logger2.default.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id); + _logger2.default.verbose('Current client number : %d', this.clients.size); + + var classSubscriptions = this.subscriptions.get(className); + if (typeof classSubscriptions === 'undefined') { + _logger2.default.debug('Can not find subscriptions under this class ' + className); + return; + } + var _iteratorNormalCompletion5 = true; + var _didIteratorError5 = false; + var _iteratorError5 = undefined; + + try { + var _loop3 = function _loop3() { + var subscription = _step5.value; + + var isOriginalSubscriptionMatched = _this3._matchesSubscription(originalParseObject, subscription); + var isCurrentSubscriptionMatched = _this3._matchesSubscription(currentParseObject, subscription); + var _iteratorNormalCompletion6 = true; + var _didIteratorError6 = false; + var _iteratorError6 = undefined; + + try { + var _loop4 = function _loop4() { + var _step6$value = _slicedToArray(_step6.value, 2), + clientId = _step6$value[0], + requestIds = _step6$value[1]; + + var client = _this3.clients.get(clientId); + if (typeof client === 'undefined') { + return 'continue'; + } + var _iteratorNormalCompletion7 = true; + var _didIteratorError7 = false; + var _iteratorError7 = undefined; + + try { + var _loop5 = function _loop5() { + var requestId = _step7.value; + + // Set orignal ParseObject ACL checking promise, if the object does not match + // subscription, we do not need to check ACL + var originalACLCheckingPromise = void 0; + if (!isOriginalSubscriptionMatched) { + originalACLCheckingPromise = _node2.default.Promise.as(false); + } else { + var originalACL = void 0; + if (message.originalParseObject) { + originalACL = message.originalParseObject.getACL(); + } + originalACLCheckingPromise = _this3._matchesACL(originalACL, client, requestId); + } + // Set current ParseObject ACL checking promise, if the object does not match + // subscription, we do not need to check ACL + var currentACLCheckingPromise = void 0; + if (!isCurrentSubscriptionMatched) { + currentACLCheckingPromise = _node2.default.Promise.as(false); + } else { + var currentACL = message.currentParseObject.getACL(); + currentACLCheckingPromise = _this3._matchesACL(currentACL, client, requestId); + } + + _node2.default.Promise.when(originalACLCheckingPromise, currentACLCheckingPromise).then(function (isOriginalMatched, isCurrentMatched) { + _logger2.default.verbose('Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', originalParseObject, currentParseObject, isOriginalSubscriptionMatched, isCurrentSubscriptionMatched, isOriginalMatched, isCurrentMatched, subscription.hash); + + // Decide event type + var type = void 0; + if (isOriginalMatched && isCurrentMatched) { + type = 'Update'; + } else if (isOriginalMatched && !isCurrentMatched) { + type = 'Leave'; + } else if (!isOriginalMatched && isCurrentMatched) { + if (originalParseObject) { + type = 'Enter'; + } else { + type = 'Create'; + } + } else { + return null; + } + var functionName = 'push' + type; + client[functionName](requestId, currentParseObject); + }, function (error) { + _logger2.default.error('Matching ACL error : ', error); + }); + }; + + for (var _iterator7 = requestIds[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { + _loop5(); + } + } catch (err) { + _didIteratorError7 = true; + _iteratorError7 = err; + } finally { + try { + if (!_iteratorNormalCompletion7 && _iterator7.return) { + _iterator7.return(); + } + } finally { + if (_didIteratorError7) { + throw _iteratorError7; + } + } + } + }; + + for (var _iterator6 = _lodash2.default.entries(subscription.clientRequestIds)[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { + var _ret4 = _loop4(); + + if (_ret4 === 'continue') continue; + } + } catch (err) { + _didIteratorError6 = true; + _iteratorError6 = err; + } finally { + try { + if (!_iteratorNormalCompletion6 && _iterator6.return) { + _iterator6.return(); + } + } finally { + if (_didIteratorError6) { + throw _iteratorError6; + } + } + } + }; + + for (var _iterator5 = classSubscriptions.values()[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { + _loop3(); + } + } catch (err) { + _didIteratorError5 = true; + _iteratorError5 = err; + } finally { + try { + if (!_iteratorNormalCompletion5 && _iterator5.return) { + _iterator5.return(); + } + } finally { + if (_didIteratorError5) { + throw _iteratorError5; + } + } + } + } + }, { + key: '_onConnect', + value: function _onConnect(parseWebsocket) { + var _this4 = this; + + parseWebsocket.on('message', function (request) { + if (typeof request === 'string') { + try { + request = JSON.parse(request); + } catch (e) { + _logger2.default.error('unable to parse request', request, e); + return; + } + } + _logger2.default.verbose('Request: %j', request); + + // Check whether this request is a valid request, return error directly if not + if (!_tv2.default.validate(request, _RequestSchema2.default['general']) || !_tv2.default.validate(request, _RequestSchema2.default[request.op])) { + _Client.Client.pushError(parseWebsocket, 1, _tv2.default.error.message); + _logger2.default.error('Connect message error %s', _tv2.default.error.message); + return; + } + + switch (request.op) { + case 'connect': + _this4._handleConnect(parseWebsocket, request); + break; + case 'subscribe': + _this4._handleSubscribe(parseWebsocket, request); + break; + case 'update': + _this4._handleUpdateSubscription(parseWebsocket, request); + break; + case 'unsubscribe': + _this4._handleUnsubscribe(parseWebsocket, request); + break; + default: + _Client.Client.pushError(parseWebsocket, 3, 'Get unknown operation'); + _logger2.default.error('Get unknown operation', request.op); + } + }); + + parseWebsocket.on('disconnect', function () { + _logger2.default.info('Client disconnect: %d', parseWebsocket.clientId); + var clientId = parseWebsocket.clientId; + if (!_this4.clients.has(clientId)) { + _logger2.default.error('Can not find client %d on disconnect', clientId); + return; + } + + // Delete client + var client = _this4.clients.get(clientId); + _this4.clients.delete(clientId); + + // Delete client from subscriptions + var _iteratorNormalCompletion8 = true; + var _didIteratorError8 = false; + var _iteratorError8 = undefined; + + try { + for (var _iterator8 = _lodash2.default.entries(client.subscriptionInfos)[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { + var _step8$value = _slicedToArray(_step8.value, 2), + _requestId = _step8$value[0], + subscriptionInfo = _step8$value[1]; + + var _subscription = subscriptionInfo.subscription; + _subscription.deleteClientSubscription(clientId, _requestId); + + // If there is no client which is subscribing this subscription, remove it from subscriptions + var classSubscriptions = _this4.subscriptions.get(_subscription.className); + if (!_subscription.hasSubscribingClient()) { + classSubscriptions.delete(_subscription.hash); + } + // If there is no subscriptions under this class, remove it from subscriptions + if (classSubscriptions.size === 0) { + _this4.subscriptions.delete(_subscription.className); + } + } + } catch (err) { + _didIteratorError8 = true; + _iteratorError8 = err; + } finally { + try { + if (!_iteratorNormalCompletion8 && _iterator8.return) { + _iterator8.return(); + } + } finally { + if (_didIteratorError8) { + throw _iteratorError8; + } + } + } + + _logger2.default.verbose('Current clients %d', _this4.clients.size); + _logger2.default.verbose('Current subscriptions %d', _this4.subscriptions.size); + }); + } + }, { + key: '_matchesSubscription', + value: function _matchesSubscription(parseObject, subscription) { + // Object is undefined or null, not match + if (!parseObject) { + return false; + } + return (0, _QueryTools.matchesQuery)(parseObject, subscription.query); + } + }, { + key: '_matchesACL', + value: function _matchesACL(acl, client, requestId) { + var _this5 = this; + + // If ACL is undefined or null, or ACL has public read access, return true directly + if (!acl || acl.getPublicReadAccess()) { + return _node2.default.Promise.as(true); + } + // Check subscription sessionToken matches ACL first + var subscriptionInfo = client.getSubscriptionInfo(requestId); + if (typeof subscriptionInfo === 'undefined') { + return _node2.default.Promise.as(false); + } + + var subscriptionSessionToken = subscriptionInfo.sessionToken; + return this.sessionTokenCache.getUserId(subscriptionSessionToken).then(function (userId) { + return acl.getReadAccess(userId); + }).then(function (isSubscriptionSessionTokenMatched) { + if (isSubscriptionSessionTokenMatched) { + return _node2.default.Promise.as(true); + } + + // Check if the user has any roles that match the ACL + return new _node2.default.Promise(function (resolve, reject) { + + // Resolve false right away if the acl doesn't have any roles + var acl_has_roles = Object.keys(acl.permissionsById).some(function (key) { + return key.startsWith("role:"); + }); + if (!acl_has_roles) { + return resolve(false); + } + + _this5.sessionTokenCache.getUserId(subscriptionSessionToken).then(function (userId) { + + // Pass along a null if there is no user id + if (!userId) { + return _node2.default.Promise.as(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 _node2.default.User(); + user.id = userId; + return user; + }).then(function (user) { + + // Pass along an empty array (of roles) if no user + if (!user) { + return _node2.default.Promise.as([]); + } + + // Then get the user's roles + var rolesQuery = new _node2.default.Query(_node2.default.Role); + rolesQuery.equalTo("users", user); + return rolesQuery.find({ useMasterKey: true }); + }).then(function (roles) { + + // Finally, see if any of the user's roles allow them read access + var _iteratorNormalCompletion9 = true; + var _didIteratorError9 = false; + var _iteratorError9 = undefined; + + try { + for (var _iterator9 = roles[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { + var role = _step9.value; + + if (acl.getRoleReadAccess(role)) { + return resolve(true); + } + } + } catch (err) { + _didIteratorError9 = true; + _iteratorError9 = err; + } finally { + try { + if (!_iteratorNormalCompletion9 && _iterator9.return) { + _iterator9.return(); + } + } finally { + if (_didIteratorError9) { + throw _iteratorError9; + } + } + } + + resolve(false); + }).catch(function (error) { + reject(error); + }); + }); + }).then(function (isRoleMatched) { + + if (isRoleMatched) { + return _node2.default.Promise.as(true); + } + + // Check client sessionToken matches ACL + var clientSessionToken = client.sessionToken; + return _this5.sessionTokenCache.getUserId(clientSessionToken).then(function (userId) { + return acl.getReadAccess(userId); + }); + }).then(function (isMatched) { + return _node2.default.Promise.as(isMatched); + }, function () { + return _node2.default.Promise.as(false); + }); + } + }, { + key: '_handleConnect', + value: function _handleConnect(parseWebsocket, request) { + if (!this._validateKeys(request, this.keyPairs)) { + _Client.Client.pushError(parseWebsocket, 4, 'Key in request is not valid'); + _logger2.default.error('Key in request is not valid'); + return; + } + var client = new _Client.Client(this.clientId, parseWebsocket); + parseWebsocket.clientId = this.clientId; + this.clientId += 1; + this.clients.set(parseWebsocket.clientId, client); + _logger2.default.info('Create new client: %d', parseWebsocket.clientId); + client.pushConnect(); + } + }, { + key: '_validateKeys', + value: function _validateKeys(request, validKeyPairs) { + if (!validKeyPairs || validKeyPairs.size == 0) { + return true; + } + var isValid = false; + var _iteratorNormalCompletion10 = true; + var _didIteratorError10 = false; + var _iteratorError10 = undefined; + + try { + for (var _iterator10 = validKeyPairs[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { + var _step10$value = _slicedToArray(_step10.value, 2), + key = _step10$value[0], + secret = _step10$value[1]; + + if (!request[key] || request[key] !== secret) { + continue; + } + isValid = true; + break; + } + } catch (err) { + _didIteratorError10 = true; + _iteratorError10 = err; + } finally { + try { + if (!_iteratorNormalCompletion10 && _iterator10.return) { + _iterator10.return(); + } + } finally { + if (_didIteratorError10) { + throw _iteratorError10; + } + } + } + + return isValid; + } + }, { + key: '_handleSubscribe', + value: function _handleSubscribe(parseWebsocket, request) { + // If we can not find this client, return error to client + if (!parseWebsocket.hasOwnProperty('clientId')) { + _Client.Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before subscribing'); + _logger2.default.error('Can not find this client, make sure you connect to server before subscribing'); + return; + } + var client = this.clients.get(parseWebsocket.clientId); + + // Get subscription from subscriptions, create one if necessary + var subscriptionHash = (0, _QueryTools.queryHash)(request.query); + // Add className to subscriptions if necessary + var className = request.query.className; + if (!this.subscriptions.has(className)) { + this.subscriptions.set(className, new Map()); + } + var classSubscriptions = this.subscriptions.get(className); + var subscription = void 0; + if (classSubscriptions.has(subscriptionHash)) { + subscription = classSubscriptions.get(subscriptionHash); + } else { + subscription = new _Subscription.Subscription(className, request.query.where, subscriptionHash); + classSubscriptions.set(subscriptionHash, subscription); + } + + // Add subscriptionInfo to client + var subscriptionInfo = { + subscription: subscription + }; + // Add selected fields and sessionToken for this subscription if necessary + if (request.query.fields) { + subscriptionInfo.fields = request.query.fields; + } + if (request.sessionToken) { + subscriptionInfo.sessionToken = request.sessionToken; + } + client.addSubscriptionInfo(request.requestId, subscriptionInfo); + + // Add clientId to subscription + subscription.addClientSubscription(parseWebsocket.clientId, request.requestId); + + client.pushSubscribe(request.requestId); + + _logger2.default.verbose('Create client %d new subscription: %d', parseWebsocket.clientId, request.requestId); + _logger2.default.verbose('Current client number: %d', this.clients.size); + } + }, { + key: '_handleUpdateSubscription', + value: function _handleUpdateSubscription(parseWebsocket, request) { + this._handleUnsubscribe(parseWebsocket, request, false); + this._handleSubscribe(parseWebsocket, request); + } + }, { + key: '_handleUnsubscribe', + value: function _handleUnsubscribe(parseWebsocket, request) { + var notifyClient = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; + + // If we can not find this client, return error to client + if (!parseWebsocket.hasOwnProperty('clientId')) { + _Client.Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before unsubscribing'); + _logger2.default.error('Can not find this client, make sure you connect to server before unsubscribing'); + return; + } + var requestId = request.requestId; + var client = this.clients.get(parseWebsocket.clientId); + if (typeof client === 'undefined') { + _Client.Client.pushError(parseWebsocket, 2, 'Cannot find client with clientId ' + parseWebsocket.clientId + '. Make sure you connect to live query server before unsubscribing.'); + _logger2.default.error('Can not find this client ' + parseWebsocket.clientId); + return; + } + + var subscriptionInfo = client.getSubscriptionInfo(requestId); + if (typeof subscriptionInfo === 'undefined') { + _Client.Client.pushError(parseWebsocket, 2, 'Cannot find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId + '. Make sure you subscribe to live query server before unsubscribing.'); + _logger2.default.error('Can not find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId); + return; + } + + // Remove subscription from client + client.deleteSubscriptionInfo(requestId); + // Remove client from subscription + var subscription = subscriptionInfo.subscription; + var className = subscription.className; + subscription.deleteClientSubscription(parseWebsocket.clientId, requestId); + // If there is no client which is subscribing this subscription, remove it from subscriptions + var classSubscriptions = this.subscriptions.get(className); + if (!subscription.hasSubscribingClient()) { + classSubscriptions.delete(subscription.hash); + } + // If there is no subscriptions under this class, remove it from subscriptions + if (classSubscriptions.size === 0) { + this.subscriptions.delete(className); + } + + if (!notifyClient) { + return; + } + + client.pushUnsubscribe(request.requestId); + + _logger2.default.verbose('Delete client: %d | subscription: %d', parseWebsocket.clientId, request.requestId); + } + }]); + + return ParseLiveQueryServer; +}(); + +exports.ParseLiveQueryServer = ParseLiveQueryServer; \ No newline at end of file diff --git a/lib/LiveQuery/ParsePubSub.js b/lib/LiveQuery/ParsePubSub.js new file mode 100644 index 0000000000..a1a0100160 --- /dev/null +++ b/lib/LiveQuery/ParsePubSub.js @@ -0,0 +1,45 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ParsePubSub = undefined; + +var _AdapterLoader = require('../Adapters/AdapterLoader'); + +var _EventEmitterPubSub = require('../Adapters/PubSub/EventEmitterPubSub'); + +var _RedisPubSub = require('../Adapters/PubSub/RedisPubSub'); + +var ParsePubSub = {}; + +function useRedis(config) { + var redisURL = config.redisURL; + return typeof redisURL !== 'undefined' && redisURL !== ''; +} + +ParsePubSub.createPublisher = function (config) { + if (useRedis(config)) { + return _RedisPubSub.RedisPubSub.createPublisher(config); + } else { + var adapter = (0, _AdapterLoader.loadAdapter)(config.pubSubAdapter, _EventEmitterPubSub.EventEmitterPubSub, config); + if (typeof adapter.createPublisher !== 'function') { + throw 'pubSubAdapter should have createPublisher()'; + } + return adapter.createPublisher(config); + } +}; + +ParsePubSub.createSubscriber = function (config) { + if (useRedis(config)) { + return _RedisPubSub.RedisPubSub.createSubscriber(config); + } else { + var adapter = (0, _AdapterLoader.loadAdapter)(config.pubSubAdapter, _EventEmitterPubSub.EventEmitterPubSub, config); + if (typeof adapter.createSubscriber !== 'function') { + throw 'pubSubAdapter should have createSubscriber()'; + } + return adapter.createSubscriber(config); + } +}; + +exports.ParsePubSub = ParsePubSub; \ No newline at end of file diff --git a/lib/LiveQuery/ParseWebSocketServer.js b/lib/LiveQuery/ParseWebSocketServer.js new file mode 100644 index 0000000000..6c6e71b52e --- /dev/null +++ b/lib/LiveQuery/ParseWebSocketServer.js @@ -0,0 +1,72 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ParseWebSocket = exports.ParseWebSocketServer = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _logger = require('../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var typeMap = new Map([['disconnect', 'close']]); +var getWS = function getWS() { + try { + return require('uws'); + } catch (e) { + return require('ws'); + } +}; + +var ParseWebSocketServer = exports.ParseWebSocketServer = function ParseWebSocketServer(server, onConnect) { + var websocketTimeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 10 * 1000; + + _classCallCheck(this, ParseWebSocketServer); + + var WebSocketServer = getWS().Server; + var wss = new WebSocketServer({ server: server }); + wss.on('listening', function () { + _logger2.default.info('Parse LiveQuery Server starts running'); + }); + wss.on('connection', function (ws) { + onConnect(new ParseWebSocket(ws)); + // Send ping to client periodically + var pingIntervalId = setInterval(function () { + if (ws.readyState == ws.OPEN) { + ws.ping(); + } else { + clearInterval(pingIntervalId); + } + }, websocketTimeout); + }); + this.server = wss; +}; + +var ParseWebSocket = exports.ParseWebSocket = function () { + function ParseWebSocket(ws) { + _classCallCheck(this, ParseWebSocket); + + this.ws = ws; + } + + _createClass(ParseWebSocket, [{ + key: 'on', + value: function on(type, callback) { + var wsType = typeMap.has(type) ? typeMap.get(type) : type; + this.ws.on(wsType, callback); + } + }, { + key: 'send', + value: function send(message) { + this.ws.send(message); + } + }]); + + return ParseWebSocket; +}(); \ No newline at end of file diff --git a/lib/LiveQuery/QueryTools.js b/lib/LiveQuery/QueryTools.js new file mode 100644 index 0000000000..dcaf13b42c --- /dev/null +++ b/lib/LiveQuery/QueryTools.js @@ -0,0 +1,298 @@ +'use strict'; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var equalObjects = require('./equalObjects'); +var Id = require('./Id'); +var Parse = require('parse/node'); + +/** + * Query Hashes are deterministic hashes for Parse Queries. + * Any two queries that have the same set of constraints will produce the same + * hash. This lets us reliably group components by the queries they depend upon, + * and quickly determine if a query has changed. + */ + +/** + * Convert $or queries into an array of where conditions + */ +function flattenOrQueries(where) { + if (!where.hasOwnProperty('$or')) { + return where; + } + var accum = []; + for (var i = 0; i < where.$or.length; i++) { + accum = accum.concat(where.$or[i]); + } + return accum; +} + +/** + * Deterministically turns an object into a string. Disregards ordering + */ +function stringify(object) { + if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) !== 'object' || object === null) { + if (typeof object === 'string') { + return '"' + object.replace(/\|/g, '%|') + '"'; + } + return object + ''; + } + if (Array.isArray(object)) { + var copy = object.map(stringify); + copy.sort(); + return '[' + copy.join(',') + ']'; + } + var sections = []; + var keys = Object.keys(object); + keys.sort(); + for (var k = 0; k < keys.length; k++) { + sections.push(stringify(keys[k]) + ':' + stringify(object[keys[k]])); + } + return '{' + sections.join(',') + '}'; +} + +/** + * Generate a hash from a query, with unique fields for columns, values, order, + * skip, and limit. + */ +function queryHash(query) { + if (query instanceof Parse.Query) { + query = { + className: query.className, + where: query._where + }; + } + var where = flattenOrQueries(query.where || {}); + var columns = []; + var values = []; + var i; + if (Array.isArray(where)) { + var uniqueColumns = {}; + for (i = 0; i < where.length; i++) { + var subValues = {}; + var keys = Object.keys(where[i]); + keys.sort(); + for (var j = 0; j < keys.length; j++) { + subValues[keys[j]] = where[i][keys[j]]; + uniqueColumns[keys[j]] = true; + } + values.push(subValues); + } + columns = Object.keys(uniqueColumns); + columns.sort(); + } else { + columns = Object.keys(where); + columns.sort(); + for (i = 0; i < columns.length; i++) { + values.push(where[columns[i]]); + } + } + + var sections = [columns.join(','), stringify(values)]; + + return query.className + ':' + sections.join('|'); +} + +/** + * matchesQuery -- Determines if an object would be returned by a Parse Query + * It's a lightweight, where-clause only implementation of a full query engine. + * Since we find queries that match objects, rather than objects that match + * queries, we can avoid building a full-blown query tool. + */ +function matchesQuery(object, query) { + if (query instanceof Parse.Query) { + var className = object.id instanceof Id ? object.id.className : object.className; + if (className !== query.className) { + return false; + } + return matchesQuery(object, query._where); + } + for (var field in query) { + if (!matchesKeyConstraints(object, field, query[field])) { + return false; + } + } + return true; +} + +function equalObjectsGeneric(obj, compareTo, eqlFn) { + if (Array.isArray(obj)) { + for (var i = 0; i < obj.length; i++) { + if (eqlFn(obj[i], compareTo)) { + return true; + } + } + return false; + } + + return eqlFn(obj, compareTo); +} + +/** + * Determines whether an object matches a single key's constraints + */ +function matchesKeyConstraints(object, key, constraints) { + if (constraints === null) { + return false; + } + if (key.indexOf(".") >= 0) { + // Key references a subobject + var keyComponents = key.split("."); + var subObjectKey = keyComponents[0]; + var keyRemainder = keyComponents.slice(1).join("."); + return matchesKeyConstraints(object[subObjectKey] || {}, keyRemainder, constraints); + } + var i; + if (key === '$or') { + for (i = 0; i < constraints.length; i++) { + if (matchesQuery(object, constraints[i])) { + return true; + } + } + return false; + } + if (key === '$relatedTo') { + // Bail! We can't handle relational queries locally + return false; + } + // Equality (or Array contains) cases + if ((typeof constraints === 'undefined' ? 'undefined' : _typeof(constraints)) !== 'object') { + if (Array.isArray(object[key])) { + return object[key].indexOf(constraints) > -1; + } + return object[key] === constraints; + } + var compareTo; + if (constraints.__type) { + if (constraints.__type === 'Pointer') { + return equalObjectsGeneric(object[key], constraints, function (obj, ptr) { + return typeof obj !== 'undefined' && ptr.className === obj.className && ptr.objectId === obj.objectId; + }); + } + + return equalObjectsGeneric(object[key], Parse._decode(key, constraints), equalObjects); + } + // More complex cases + for (var condition in constraints) { + compareTo = constraints[condition]; + if (compareTo.__type) { + 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 (compareTo.indexOf(object[key]) < 0) { + return false; + } + break; + case '$nin': + if (compareTo.indexOf(object[key]) > -1) { + return false; + } + break; + case '$all': + for (i = 0; i < compareTo.length; i++) { + if (object[key].indexOf(compareTo[i]) < 0) { + return false; + } + } + break; + case '$exists': + { + var propertyExists = typeof object[key] !== 'undefined'; + var 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 === 'undefined' ? 'undefined' : _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': + var distance = compareTo.radiansTo(object[key]); + var max = constraints.$maxDistance || Infinity; + return distance <= max; + case '$within': + 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; + } + } + return true; +} + +var QueryTools = { + queryHash: queryHash, + matchesQuery: matchesQuery +}; + +module.exports = QueryTools; \ No newline at end of file diff --git a/lib/LiveQuery/RequestSchema.js b/lib/LiveQuery/RequestSchema.js new file mode 100644 index 0000000000..c1cf04a54a --- /dev/null +++ b/lib/LiveQuery/RequestSchema.js @@ -0,0 +1,145 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var general = { + 'title': 'General request schema', + 'type': 'object', + 'properties': { + 'op': { + 'type': 'string', + 'enum': ['connect', 'subscribe', 'unsubscribe', 'update'] + } + } +}; + +var connect = { + 'title': 'Connect operation schema', + 'type': 'object', + 'properties': { + 'op': 'connect', + 'applicationId': { + 'type': 'string' + }, + 'javascriptKey': { + type: 'string' + }, + 'masterKey': { + type: 'string' + }, + 'clientKey': { + type: 'string' + }, + 'windowsKey': { + type: 'string' + }, + 'restAPIKey': { + 'type': 'string' + }, + 'sessionToken': { + 'type': 'string' + } + }, + 'required': ['op', 'applicationId'], + "additionalProperties": false +}; + +var subscribe = { + 'title': 'Subscribe operation schema', + 'type': 'object', + 'properties': { + 'op': 'subscribe', + 'requestId': { + 'type': 'number' + }, + 'query': { + 'title': 'Query field schema', + 'type': 'object', + 'properties': { + 'className': { + 'type': 'string' + }, + 'where': { + 'type': 'object' + }, + 'fields': { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + }, + 'required': ['where', 'className'], + 'additionalProperties': false + }, + 'sessionToken': { + 'type': 'string' + } + }, + 'required': ['op', 'requestId', 'query'], + 'additionalProperties': false +}; + +var update = { + 'title': 'Update operation schema', + 'type': 'object', + 'properties': { + 'op': 'update', + 'requestId': { + 'type': 'number' + }, + 'query': { + 'title': 'Query field schema', + 'type': 'object', + 'properties': { + 'className': { + 'type': 'string' + }, + 'where': { + 'type': 'object' + }, + 'fields': { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + }, + 'required': ['where', 'className'], + 'additionalProperties': false + }, + 'sessionToken': { + 'type': 'string' + } + }, + 'required': ['op', 'requestId', 'query'], + 'additionalProperties': false +}; + +var unsubscribe = { + 'title': 'Unsubscribe operation schema', + 'type': 'object', + 'properties': { + 'op': 'unsubscribe', + 'requestId': { + 'type': 'number' + } + }, + 'required': ['op', 'requestId'], + "additionalProperties": false +}; + +var RequestSchema = { + 'general': general, + 'connect': connect, + 'subscribe': subscribe, + 'update': update, + 'unsubscribe': unsubscribe +}; + +exports.default = RequestSchema; \ No newline at end of file diff --git a/lib/LiveQuery/SessionTokenCache.js b/lib/LiveQuery/SessionTokenCache.js new file mode 100644 index 0000000000..d89c466b9f --- /dev/null +++ b/lib/LiveQuery/SessionTokenCache.js @@ -0,0 +1,78 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.SessionTokenCache = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _lruCache = require('lru-cache'); + +var _lruCache2 = _interopRequireDefault(_lruCache); + +var _logger = require('../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function userForSessionToken(sessionToken) { + var q = new _node2.default.Query("_Session"); + q.equalTo("sessionToken", sessionToken); + return q.first({ useMasterKey: true }).then(function (session) { + if (!session) { + return _node2.default.Promise.error("No session found for session token"); + } + return session.get("user"); + }); +} + +var SessionTokenCache = function () { + function SessionTokenCache() { + var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 30 * 24 * 60 * 60 * 1000; + var maxSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10000; + + _classCallCheck(this, SessionTokenCache); + + this.cache = new _lruCache2.default({ + max: maxSize, + maxAge: timeout + }); + } + + _createClass(SessionTokenCache, [{ + key: 'getUserId', + value: function getUserId(sessionToken) { + var _this = this; + + if (!sessionToken) { + return _node2.default.Promise.error('Empty sessionToken'); + } + var userId = this.cache.get(sessionToken); + if (userId) { + _logger2.default.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken); + return _node2.default.Promise.as(userId); + } + return userForSessionToken(sessionToken).then(function (user) { + _logger2.default.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken); + var userId = user.id; + _this.cache.set(sessionToken, userId); + return _node2.default.Promise.as(userId); + }, function (error) { + _logger2.default.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error); + return _node2.default.Promise.error(error); + }); + } + }]); + + return SessionTokenCache; +}(); + +exports.SessionTokenCache = SessionTokenCache; \ No newline at end of file diff --git a/lib/LiveQuery/Subscription.js b/lib/LiveQuery/Subscription.js new file mode 100644 index 0000000000..55eaa77e33 --- /dev/null +++ b/lib/LiveQuery/Subscription.js @@ -0,0 +1,68 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Subscription = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _logger = require('../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Subscription = function () { + // It is query condition eg query.where + function Subscription(className, query, queryHash) { + _classCallCheck(this, Subscription); + + this.className = className; + this.query = query; + this.hash = queryHash; + this.clientRequestIds = new Map(); + } + + _createClass(Subscription, [{ + key: 'addClientSubscription', + value: function addClientSubscription(clientId, requestId) { + if (!this.clientRequestIds.has(clientId)) { + this.clientRequestIds.set(clientId, []); + } + var requestIds = this.clientRequestIds.get(clientId); + requestIds.push(requestId); + } + }, { + key: 'deleteClientSubscription', + value: function deleteClientSubscription(clientId, requestId) { + var requestIds = this.clientRequestIds.get(clientId); + if (typeof requestIds === 'undefined') { + _logger2.default.error('Can not find client %d to delete', clientId); + return; + } + + var index = requestIds.indexOf(requestId); + if (index < 0) { + _logger2.default.error('Can not find client %d subscription %d to delete', clientId, requestId); + return; + } + requestIds.splice(index, 1); + // Delete client reference if it has no subscription + if (requestIds.length == 0) { + this.clientRequestIds.delete(clientId); + } + } + }, { + key: 'hasSubscribingClient', + value: function hasSubscribingClient() { + return this.clientRequestIds.size > 0; + } + }]); + + return Subscription; +}(); + +exports.Subscription = Subscription; \ No newline at end of file diff --git a/lib/LiveQuery/equalObjects.js b/lib/LiveQuery/equalObjects.js new file mode 100644 index 0000000000..0e6d38fd69 --- /dev/null +++ b/lib/LiveQuery/equalObjects.js @@ -0,0 +1,52 @@ +'use strict'; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var toString = Object.prototype.toString; + +/** + * Determines whether two objects represent the same primitive, special Parse + * type, or full Parse Object. + */ +function equalObjects(a, b) { + if ((typeof a === 'undefined' ? 'undefined' : _typeof(a)) !== (typeof b === 'undefined' ? 'undefined' : _typeof(b))) { + return false; + } + if ((typeof a === 'undefined' ? 'undefined' : _typeof(a)) !== 'object') { + return a === b; + } + if (a === b) { + return true; + } + if (toString.call(a) === '[object Date]') { + if (toString.call(b) === '[object Date]') { + return +a === +b; + } + return false; + } + if (Array.isArray(a)) { + if (Array.isArray(b)) { + if (a.length !== b.length) { + return false; + } + for (var i = 0; i < a.length; i++) { + if (!equalObjects(a[i], b[i])) { + return false; + } + } + return true; + } + return false; + } + if (Object.keys(a).length !== Object.keys(b).length) { + return false; + } + for (var key in a) { + if (!equalObjects(a[key], b[key])) { + return false; + } + } + return true; +} + +module.exports = equalObjects; \ No newline at end of file diff --git a/lib/ParseMessageQueue.js b/lib/ParseMessageQueue.js new file mode 100644 index 0000000000..91d9728147 --- /dev/null +++ b/lib/ParseMessageQueue.js @@ -0,0 +1,30 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ParseMessageQueue = undefined; + +var _AdapterLoader = require('./Adapters/AdapterLoader'); + +var _EventEmitterMQ = require('./Adapters/MessageQueue/EventEmitterMQ'); + +var ParseMessageQueue = {}; + +ParseMessageQueue.createPublisher = function (config) { + var adapter = (0, _AdapterLoader.loadAdapter)(config.messageQueueAdapter, _EventEmitterMQ.EventEmitterMQ, config); + if (typeof adapter.createPublisher !== 'function') { + throw 'pubSubAdapter should have createPublisher()'; + } + return adapter.createPublisher(config); +}; + +ParseMessageQueue.createSubscriber = function (config) { + var adapter = (0, _AdapterLoader.loadAdapter)(config.messageQueueAdapter, _EventEmitterMQ.EventEmitterMQ, config); + if (typeof adapter.createSubscriber !== 'function') { + throw 'messageQueueAdapter should have createSubscriber()'; + } + return adapter.createSubscriber(config); +}; + +exports.ParseMessageQueue = ParseMessageQueue; \ No newline at end of file diff --git a/lib/ParseServer.js b/lib/ParseServer.js new file mode 100644 index 0000000000..5e37b09300 --- /dev/null +++ b/lib/ParseServer.js @@ -0,0 +1,514 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _defaults = require('./defaults'); + +var _defaults2 = _interopRequireDefault(_defaults); + +var _logger = require('./logger'); + +var logging = _interopRequireWildcard(_logger); + +var _cache = require('./cache'); + +var _cache2 = _interopRequireDefault(_cache); + +var _Config = require('./Config'); + +var _Config2 = _interopRequireDefault(_Config); + +var _PromiseRouter = require('./PromiseRouter'); + +var _PromiseRouter2 = _interopRequireDefault(_PromiseRouter); + +var _requiredParameter = require('./requiredParameter'); + +var _requiredParameter2 = _interopRequireDefault(_requiredParameter); + +var _AnalyticsRouter = require('./Routers/AnalyticsRouter'); + +var _ClassesRouter = require('./Routers/ClassesRouter'); + +var _FeaturesRouter = require('./Routers/FeaturesRouter'); + +var _InMemoryCacheAdapter = require('./Adapters/Cache/InMemoryCacheAdapter'); + +var _AnalyticsController = require('./Controllers/AnalyticsController'); + +var _CacheController = require('./Controllers/CacheController'); + +var _AnalyticsAdapter = require('./Adapters/Analytics/AnalyticsAdapter'); + +var _WinstonLoggerAdapter = require('./Adapters/Logger/WinstonLoggerAdapter'); + +var _FilesController = require('./Controllers/FilesController'); + +var _FilesRouter = require('./Routers/FilesRouter'); + +var _FunctionsRouter = require('./Routers/FunctionsRouter'); + +var _GlobalConfigRouter = require('./Routers/GlobalConfigRouter'); + +var _GridStoreAdapter = require('./Adapters/Files/GridStoreAdapter'); + +var _HooksController = require('./Controllers/HooksController'); + +var _HooksRouter = require('./Routers/HooksRouter'); + +var _IAPValidationRouter = require('./Routers/IAPValidationRouter'); + +var _InstallationsRouter = require('./Routers/InstallationsRouter'); + +var _AdapterLoader = require('./Adapters/AdapterLoader'); + +var _LiveQueryController = require('./Controllers/LiveQueryController'); + +var _LoggerController = require('./Controllers/LoggerController'); + +var _LogsRouter = require('./Routers/LogsRouter'); + +var _ParseLiveQueryServer = require('./LiveQuery/ParseLiveQueryServer'); + +var _PublicAPIRouter = require('./Routers/PublicAPIRouter'); + +var _PushController = require('./Controllers/PushController'); + +var _PushQueue = require('./Push/PushQueue'); + +var _PushWorker = require('./Push/PushWorker'); + +var _PushRouter = require('./Routers/PushRouter'); + +var _CloudCodeRouter = require('./Routers/CloudCodeRouter'); + +var _RolesRouter = require('./Routers/RolesRouter'); + +var _SchemasRouter = require('./Routers/SchemasRouter'); + +var _SessionsRouter = require('./Routers/SessionsRouter'); + +var _UserController = require('./Controllers/UserController'); + +var _UsersRouter = require('./Routers/UsersRouter'); + +var _PurgeRouter = require('./Routers/PurgeRouter'); + +var _DatabaseController = require('./Controllers/DatabaseController'); + +var _DatabaseController2 = _interopRequireDefault(_DatabaseController); + +var _SchemaCache = require('./Controllers/SchemaCache'); + +var _SchemaCache2 = _interopRequireDefault(_SchemaCache); + +var _parseServerPushAdapter = require('parse-server-push-adapter'); + +var _parseServerPushAdapter2 = _interopRequireDefault(_parseServerPushAdapter); + +var _MongoStorageAdapter = require('./Adapters/Storage/Mongo/MongoStorageAdapter'); + +var _MongoStorageAdapter2 = _interopRequireDefault(_MongoStorageAdapter); + +var _PostgresStorageAdapter = require('./Adapters/Storage/Postgres/PostgresStorageAdapter'); + +var _PostgresStorageAdapter2 = _interopRequireDefault(_PostgresStorageAdapter); + +var _ParseServerRESTController = require('./ParseServerRESTController'); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +// ParseServer - open-source compatible API Server for Parse apps + +var batch = require('./batch'), + bodyParser = require('body-parser'), + express = require('express'), + middlewares = require('./middlewares'), + Parse = require('parse/node').Parse, + path = require('path'), + url = require('url'), + authDataManager = require('./Adapters/Auth'); + +// Mutate the Parse object to add the Cloud Code handlers +addParseCloud(); + +// ParseServer works like a constructor of an express app. +// The args that we understand are: +// "analyticsAdapter": an adapter class for analytics +// "filesAdapter": a class like GridStoreAdapter providing create, get, +// and delete +// "loggerAdapter": a class like WinstonLoggerAdapter providing info, error, +// and query +// "jsonLogs": log as structured JSON objects +// "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us +// what database this Parse API connects to. +// "cloud": relative location to cloud code to require, or a function +// that is given an instance of Parse as a parameter. Use this instance of Parse +// to register your cloud code hooks and functions. +// "appId": the application id to host +// "masterKey": the master key for requests to this app +// "collectionPrefix": optional prefix for database collection names +// "fileKey": optional key from Parse dashboard for supporting older files +// hosted by Parse +// "clientKey": optional key from Parse dashboard +// "dotNetKey": optional key from Parse dashboard +// "restAPIKey": optional key from Parse dashboard +// "webhookKey": optional key from Parse dashboard +// "javascriptKey": optional key from Parse dashboard +// "push": optional key from configure push +// "sessionLength": optional length in seconds for how long Sessions should be valid for + +var ParseServer = function () { + function ParseServer(_ref) { + var _ref$appId = _ref.appId, + appId = _ref$appId === undefined ? (0, _requiredParameter2.default)('You must provide an appId!') : _ref$appId, + _ref$masterKey = _ref.masterKey, + masterKey = _ref$masterKey === undefined ? (0, _requiredParameter2.default)('You must provide a masterKey!') : _ref$masterKey, + appName = _ref.appName, + analyticsAdapter = _ref.analyticsAdapter, + filesAdapter = _ref.filesAdapter, + push = _ref.push, + _ref$scheduledPush = _ref.scheduledPush, + scheduledPush = _ref$scheduledPush === undefined ? false : _ref$scheduledPush, + loggerAdapter = _ref.loggerAdapter, + _ref$jsonLogs = _ref.jsonLogs, + jsonLogs = _ref$jsonLogs === undefined ? _defaults2.default.jsonLogs : _ref$jsonLogs, + _ref$logsFolder = _ref.logsFolder, + logsFolder = _ref$logsFolder === undefined ? _defaults2.default.logsFolder : _ref$logsFolder, + _ref$verbose = _ref.verbose, + verbose = _ref$verbose === undefined ? _defaults2.default.verbose : _ref$verbose, + _ref$logLevel = _ref.logLevel, + logLevel = _ref$logLevel === undefined ? _defaults2.default.level : _ref$logLevel, + _ref$silent = _ref.silent, + silent = _ref$silent === undefined ? _defaults2.default.silent : _ref$silent, + _ref$databaseURI = _ref.databaseURI, + databaseURI = _ref$databaseURI === undefined ? _defaults2.default.DefaultMongoURI : _ref$databaseURI, + databaseOptions = _ref.databaseOptions, + databaseAdapter = _ref.databaseAdapter, + cloud = _ref.cloud, + _ref$collectionPrefix = _ref.collectionPrefix, + collectionPrefix = _ref$collectionPrefix === undefined ? '' : _ref$collectionPrefix, + clientKey = _ref.clientKey, + javascriptKey = _ref.javascriptKey, + dotNetKey = _ref.dotNetKey, + restAPIKey = _ref.restAPIKey, + webhookKey = _ref.webhookKey, + fileKey = _ref.fileKey, + _ref$userSensitiveFie = _ref.userSensitiveFields, + userSensitiveFields = _ref$userSensitiveFie === undefined ? [] : _ref$userSensitiveFie, + _ref$enableAnonymousU = _ref.enableAnonymousUsers, + enableAnonymousUsers = _ref$enableAnonymousU === undefined ? _defaults2.default.enableAnonymousUsers : _ref$enableAnonymousU, + _ref$allowClientClass = _ref.allowClientClassCreation, + allowClientClassCreation = _ref$allowClientClass === undefined ? _defaults2.default.allowClientClassCreation : _ref$allowClientClass, + _ref$oauth = _ref.oauth, + oauth = _ref$oauth === undefined ? {} : _ref$oauth, + _ref$auth = _ref.auth, + auth = _ref$auth === undefined ? {} : _ref$auth, + _ref$serverURL = _ref.serverURL, + serverURL = _ref$serverURL === undefined ? (0, _requiredParameter2.default)('You must provide a serverURL!') : _ref$serverURL, + _ref$maxUploadSize = _ref.maxUploadSize, + maxUploadSize = _ref$maxUploadSize === undefined ? _defaults2.default.maxUploadSize : _ref$maxUploadSize, + _ref$verifyUserEmails = _ref.verifyUserEmails, + verifyUserEmails = _ref$verifyUserEmails === undefined ? _defaults2.default.verifyUserEmails : _ref$verifyUserEmails, + _ref$preventLoginWith = _ref.preventLoginWithUnverifiedEmail, + preventLoginWithUnverifiedEmail = _ref$preventLoginWith === undefined ? _defaults2.default.preventLoginWithUnverifiedEmail : _ref$preventLoginWith, + emailVerifyTokenValidityDuration = _ref.emailVerifyTokenValidityDuration, + accountLockout = _ref.accountLockout, + passwordPolicy = _ref.passwordPolicy, + cacheAdapter = _ref.cacheAdapter, + emailAdapter = _ref.emailAdapter, + publicServerURL = _ref.publicServerURL, + _ref$customPages = _ref.customPages, + customPages = _ref$customPages === undefined ? { + invalidLink: undefined, + verifyEmailSuccess: undefined, + choosePassword: undefined, + passwordResetSuccess: undefined + } : _ref$customPages, + _ref$liveQuery = _ref.liveQuery, + liveQuery = _ref$liveQuery === undefined ? {} : _ref$liveQuery, + _ref$sessionLength = _ref.sessionLength, + sessionLength = _ref$sessionLength === undefined ? _defaults2.default.sessionLength : _ref$sessionLength, + _ref$expireInactiveSe = _ref.expireInactiveSessions, + expireInactiveSessions = _ref$expireInactiveSe === undefined ? _defaults2.default.expireInactiveSessions : _ref$expireInactiveSe, + _ref$revokeSessionOnP = _ref.revokeSessionOnPasswordReset, + revokeSessionOnPasswordReset = _ref$revokeSessionOnP === undefined ? _defaults2.default.revokeSessionOnPasswordReset : _ref$revokeSessionOnP, + _ref$schemaCacheTTL = _ref.schemaCacheTTL, + schemaCacheTTL = _ref$schemaCacheTTL === undefined ? _defaults2.default.schemaCacheTTL : _ref$schemaCacheTTL, + _ref$enableSingleSche = _ref.enableSingleSchemaCache, + enableSingleSchemaCache = _ref$enableSingleSche === undefined ? false : _ref$enableSingleSche, + _ref$__indexBuildComp = _ref.__indexBuildCompletionCallbackForTests, + __indexBuildCompletionCallbackForTests = _ref$__indexBuildComp === undefined ? function () {} : _ref$__indexBuildComp; + + _classCallCheck(this, ParseServer); + + // Initialize the node client SDK automatically + Parse.initialize(appId, javascriptKey || 'unused', masterKey); + Parse.serverURL = serverURL; + if ((databaseOptions || databaseURI && databaseURI != _defaults2.default.DefaultMongoURI || collectionPrefix !== '') && databaseAdapter) { + throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/collectionPrefix.'; + } else if (!databaseAdapter) { + databaseAdapter = this.getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions); + } else { + databaseAdapter = (0, _AdapterLoader.loadAdapter)(databaseAdapter); + } + + if (!filesAdapter && !databaseURI) { + throw 'When using an explicit database adapter, you must also use an explicit filesAdapter.'; + } + + userSensitiveFields = Array.from(new Set(userSensitiveFields.concat(_defaults2.default.userSensitiveFields, userSensitiveFields))); + + var loggerControllerAdapter = (0, _AdapterLoader.loadAdapter)(loggerAdapter, _WinstonLoggerAdapter.WinstonLoggerAdapter, { jsonLogs: jsonLogs, logsFolder: logsFolder, verbose: verbose, logLevel: logLevel, silent: silent }); + var loggerController = new _LoggerController.LoggerController(loggerControllerAdapter, appId); + logging.setLogger(loggerController); + + var filesControllerAdapter = (0, _AdapterLoader.loadAdapter)(filesAdapter, function () { + return new _GridStoreAdapter.GridStoreAdapter(databaseURI); + }); + var filesController = new _FilesController.FilesController(filesControllerAdapter, appId); + + var pushOptions = Object.assign({}, push); + var pushQueueOptions = pushOptions.queueOptions || {}; + if (pushOptions.queueOptions) { + delete pushOptions.queueOptions; + } + // Pass the push options too as it works with the default + var pushAdapter = (0, _AdapterLoader.loadAdapter)(pushOptions && pushOptions.adapter, _parseServerPushAdapter2.default, pushOptions); + // We pass the options and the base class for the adatper, + // Note that passing an instance would work too + var pushController = new _PushController.PushController(); + + var hasPushSupport = pushAdapter && push; + var hasPushScheduledSupport = pushAdapter && push && scheduledPush; + + var disablePushWorker = pushQueueOptions.disablePushWorker; + + + var pushControllerQueue = new _PushQueue.PushQueue(pushQueueOptions); + var pushWorker = void 0; + if (!disablePushWorker) { + pushWorker = new _PushWorker.PushWorker(pushAdapter, pushQueueOptions); + } + + var emailControllerAdapter = (0, _AdapterLoader.loadAdapter)(emailAdapter); + var userController = new _UserController.UserController(emailControllerAdapter, appId, { verifyUserEmails: verifyUserEmails }); + + var cacheControllerAdapter = (0, _AdapterLoader.loadAdapter)(cacheAdapter, _InMemoryCacheAdapter.InMemoryCacheAdapter, { appId: appId }); + var cacheController = new _CacheController.CacheController(cacheControllerAdapter, appId); + + var analyticsControllerAdapter = (0, _AdapterLoader.loadAdapter)(analyticsAdapter, _AnalyticsAdapter.AnalyticsAdapter); + var analyticsController = new _AnalyticsController.AnalyticsController(analyticsControllerAdapter); + + var liveQueryController = new _LiveQueryController.LiveQueryController(liveQuery); + var databaseController = new _DatabaseController2.default(databaseAdapter, new _SchemaCache2.default(cacheController, schemaCacheTTL, enableSingleSchemaCache)); + var hooksController = new _HooksController.HooksController(appId, databaseController, webhookKey); + + var dbInitPromise = databaseController.performInitialization(); + + if (Object.keys(oauth).length > 0) { + /* eslint-disable no-console */ + console.warn('oauth option is deprecated and will be removed in a future release, please use auth option instead'); + if (Object.keys(auth).length > 0) { + console.warn('You should use only the auth option.'); + } + /* eslint-enable */ + } + + auth = Object.assign({}, oauth, auth); + + _cache2.default.put(appId, { + appId: appId, + masterKey: masterKey, + serverURL: serverURL, + collectionPrefix: collectionPrefix, + clientKey: clientKey, + javascriptKey: javascriptKey, + dotNetKey: dotNetKey, + restAPIKey: restAPIKey, + webhookKey: webhookKey, + fileKey: fileKey, + analyticsController: analyticsController, + cacheController: cacheController, + filesController: filesController, + pushController: pushController, + loggerController: loggerController, + hooksController: hooksController, + userController: userController, + verifyUserEmails: verifyUserEmails, + preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail, + emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration, + accountLockout: accountLockout, + passwordPolicy: passwordPolicy, + allowClientClassCreation: allowClientClassCreation, + authDataManager: authDataManager(auth, enableAnonymousUsers), + appName: appName, + publicServerURL: publicServerURL, + customPages: customPages, + maxUploadSize: maxUploadSize, + liveQueryController: liveQueryController, + sessionLength: Number(sessionLength), + expireInactiveSessions: expireInactiveSessions, + jsonLogs: jsonLogs, + revokeSessionOnPasswordReset: revokeSessionOnPasswordReset, + databaseController: databaseController, + schemaCacheTTL: schemaCacheTTL, + enableSingleSchemaCache: enableSingleSchemaCache, + userSensitiveFields: userSensitiveFields, + pushWorker: pushWorker, + pushControllerQueue: pushControllerQueue, + hasPushSupport: hasPushSupport, + hasPushScheduledSupport: hasPushScheduledSupport + }); + + _Config2.default.validate(_cache2.default.get(appId)); + this.config = _cache2.default.get(appId); + _Config2.default.setupPasswordValidator(this.config.passwordPolicy); + hooksController.load(); + + // Note: Tests will start to fail if any validation happens after this is called. + if (process.env.TESTING) { + __indexBuildCompletionCallbackForTests(dbInitPromise); + } + + if (cloud) { + addParseCloud(); + if (typeof cloud === 'function') { + cloud(Parse); + } else if (typeof cloud === 'string') { + require(path.resolve(process.cwd(), cloud)); + } else { + throw "argument 'cloud' must either be a string or a function"; + } + } + } + + _createClass(ParseServer, [{ + key: 'getDatabaseAdapter', + value: function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) { + var protocol = void 0; + try { + var parsedURI = url.parse(databaseURI); + protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null; + } catch (e) {/* */} + switch (protocol) { + case 'postgres:': + return new _PostgresStorageAdapter2.default({ + uri: databaseURI, + collectionPrefix: collectionPrefix, + databaseOptions: databaseOptions + }); + default: + return new _MongoStorageAdapter2.default({ + uri: databaseURI, + collectionPrefix: collectionPrefix, + mongoOptions: databaseOptions + }); + } + } + }, { + key: 'handleShutdown', + value: function handleShutdown() { + var adapter = this.config.databaseController.adapter; + + if (adapter && typeof adapter.handleShutdown === 'function') { + adapter.handleShutdown(); + } + } + }, { + key: 'app', + get: function get() { + return ParseServer.app(this.config); + } + }], [{ + key: 'app', + value: function app(_ref2) { + var _ref2$maxUploadSize = _ref2.maxUploadSize, + maxUploadSize = _ref2$maxUploadSize === undefined ? '20mb' : _ref2$maxUploadSize, + appId = _ref2.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.FilesRouter().expressRouter({ + maxUploadSize: maxUploadSize + })); + + api.use('/health', function (req, res) { + return res.sendStatus(200); + }); + + api.use('/', bodyParser.urlencoded({ extended: false }), new _PublicAPIRouter.PublicAPIRouter().expressRouter()); + + api.use(bodyParser.json({ 'type': '*/*', limit: maxUploadSize })); + api.use(middlewares.allowCrossDomain); + api.use(middlewares.allowMethodOverride); + api.use(middlewares.handleParseHeaders); + + var appRouter = ParseServer.promiseRouter({ appId: appId }); + api.use(appRouter.expressRouter()); + + api.use(middlewares.handleParseErrors); + + //This causes tests to spew some useless warnings, so disable in test + if (!process.env.TESTING) { + process.on('uncaughtException', function (err) { + if (err.code === "EADDRINUSE") { + // user-friendly message for this common error + /* eslint-disable no-console */ + console.error('Unable to listen on port ' + err.port + '. The port is already in use.'); + /* eslint-enable no-console */ + process.exit(0); + } else { + throw err; + } + }); + } + if (process.env.PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS === '1') { + Parse.CoreManager.setRESTController((0, _ParseServerRESTController.ParseServerRESTController)(appId, appRouter)); + } + return api; + } + }, { + key: 'promiseRouter', + value: function promiseRouter(_ref3) { + var appId = _ref3.appId; + + var routers = [new _ClassesRouter.ClassesRouter(), new _UsersRouter.UsersRouter(), new _SessionsRouter.SessionsRouter(), new _RolesRouter.RolesRouter(), new _AnalyticsRouter.AnalyticsRouter(), new _InstallationsRouter.InstallationsRouter(), new _FunctionsRouter.FunctionsRouter(), new _SchemasRouter.SchemasRouter(), new _PushRouter.PushRouter(), new _LogsRouter.LogsRouter(), new _IAPValidationRouter.IAPValidationRouter(), new _FeaturesRouter.FeaturesRouter(), new _GlobalConfigRouter.GlobalConfigRouter(), new _PurgeRouter.PurgeRouter(), new _HooksRouter.HooksRouter(), new _CloudCodeRouter.CloudCodeRouter()]; + + var routes = routers.reduce(function (memo, router) { + return memo.concat(router.routes); + }, []); + + var appRouter = new _PromiseRouter2.default(routes, appId); + + batch.mountOnto(appRouter); + return appRouter; + } + }, { + key: 'createLiveQueryServer', + value: function createLiveQueryServer(httpServer, config) { + return new _ParseLiveQueryServer.ParseLiveQueryServer(httpServer, config); + } + }]); + + return ParseServer; +}(); + +function addParseCloud() { + var ParseCloud = require("./cloud-code/Parse.Cloud"); + Object.assign(Parse.Cloud, ParseCloud); + global.Parse = Parse; +} + +exports.default = ParseServer; \ No newline at end of file diff --git a/lib/ParseServerRESTController.js b/lib/ParseServerRESTController.js new file mode 100644 index 0000000000..f39caff341 --- /dev/null +++ b/lib/ParseServerRESTController.js @@ -0,0 +1,109 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +var Config = require('./Config'); +var Auth = require('./Auth'); +var RESTController = require('parse/lib/node/RESTController'); +var URL = require('url'); +var Parse = require('parse/node'); + +function getSessionToken(options) { + if (options && typeof options.sessionToken === 'string') { + return Parse.Promise.as(options.sessionToken); + } + return Parse.Promise.as(null); +} + +function getAuth() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var config = arguments[1]; + + var installationId = options.installationId || 'cloud'; + if (options.useMasterKey) { + return Parse.Promise.as(new Auth.Auth({ config: config, isMaster: true, installationId: installationId })); + } + return getSessionToken(options).then(function (sessionToken) { + if (sessionToken) { + options.sessionToken = sessionToken; + return Auth.getAuthForSessionToken({ + config: config, + sessionToken: sessionToken, + installationId: installationId + }); + } else { + return Parse.Promise.as(new Auth.Auth({ config: config, installationId: installationId })); + } + }); +} + +function ParseServerRESTController(applicationId, router) { + function handleRequest(method, path) { + var data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; + var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + + // Store the arguments, for later use if internal fails + var args = arguments; + + var config = new Config(applicationId); + var serverURL = URL.parse(config.serverURL); + if (path.indexOf(serverURL.path) === 0) { + path = path.slice(serverURL.path.length, path.length); + } + + if (path[0] !== "/") { + path = "/" + path; + } + + if (path === '/batch') { + var promises = data.requests.map(function (request) { + return handleRequest(request.method, request.path, request.body, options).then(function (response) { + return Parse.Promise.as({ success: response }); + }, function (error) { + return Parse.Promise.as({ error: { code: error.code, error: error.message } }); + }); + }); + return Parse.Promise.all(promises); + } + + var query = void 0; + if (method === 'GET') { + query = data; + } + + return new Parse.Promise(function (resolve, reject) { + getAuth(options, config).then(function (auth) { + var request = { + body: data, + config: config, + auth: auth, + info: { + applicationId: applicationId, + sessionToken: options.sessionToken + }, + query: query + }; + return Promise.resolve().then(function () { + return router.tryRouteRequest(method, path, request); + }).then(function (response) { + resolve(response.response, response.status, response); + }, function (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 { + request: handleRequest, + ajax: RESTController.ajax + }; +} + +exports.default = ParseServerRESTController; +exports.ParseServerRESTController = ParseServerRESTController; \ No newline at end of file diff --git a/lib/PromiseRouter.js b/lib/PromiseRouter.js new file mode 100644 index 0000000000..1d1de1893f --- /dev/null +++ b/lib/PromiseRouter.js @@ -0,0 +1,302 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // A router that is based on promises rather than req/res/next. +// This is intended to replace the use of express.Router to handle +// subsections of the API surface. +// This will make it easier to have methods like 'batch' that +// themselves use our routing information, without disturbing express +// components that external developers may be modifying. + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _express = require('express'); + +var _express2 = _interopRequireDefault(_express); + +var _logger = require('./logger'); + +var _logger2 = _interopRequireDefault(_logger); + +var _util = require('util'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var Layer = require('express/lib/router/layer'); + +function validateParameter(key, value) { + if (key == 'className') { + if (value.match(/_?[A-Za-z][A-Za-z_0-9]*/)) { + return value; + } + } else if (key == 'objectId') { + if (value.match(/[A-Za-z0-9]+/)) { + return value; + } + } else { + return value; + } +} + +var PromiseRouter = function () { + // Each entry should be an object with: + // path: the path to route, in express format + // method: the HTTP method that this route handles. + // Must be one of: POST, GET, PUT, DELETE + // handler: a function that takes request, and returns a promise. + // Successful handlers should resolve to an object with fields: + // status: optional. the http status code. defaults to 200 + // response: a json object with the content of the response + // location: optional. a location header + function PromiseRouter() { + var routes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; + var appId = arguments[1]; + + _classCallCheck(this, PromiseRouter); + + this.routes = routes; + this.appId = appId; + this.mountRoutes(); + } + + // Leave the opportunity to + // subclasses to mount their routes by overriding + + + _createClass(PromiseRouter, [{ + key: 'mountRoutes', + value: function mountRoutes() {} + + // Merge the routes into this one + + }, { + key: 'merge', + value: function merge(router) { + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = router.routes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var route = _step.value; + + this.routes.push(route); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + } + }, { + key: 'route', + value: function route(method, path) { + for (var _len = arguments.length, handlers = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { + handlers[_key - 2] = arguments[_key]; + } + + switch (method) { + case 'POST': + case 'GET': + case 'PUT': + case 'DELETE': + break; + default: + throw 'cannot route method: ' + method; + } + + var handler = handlers[0]; + + if (handlers.length > 1) { + handler = function handler(req) { + return handlers.reduce(function (promise, handler) { + return promise.then(function () { + return handler(req); + }); + }, Promise.resolve()); + }; + } + + this.routes.push({ + path: path, + method: method, + handler: handler, + layer: new Layer(path, null, handler) + }); + } + + // Returns an object with: + // handler: the handler that should deal with this request + // params: any :-params that got parsed from the path + // Returns undefined if there is no match. + + }, { + key: 'match', + value: function match(method, path) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = this.routes[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var route = _step2.value; + + if (route.method != method) { + continue; + } + var layer = route.layer || new Layer(route.path, null, route.handler); + var match = layer.match(path); + if (match) { + var _ret = function () { + var params = layer.params; + Object.keys(params).forEach(function (key) { + params[key] = validateParameter(key, params[key]); + }); + return { + v: { params: params, handler: route.handler } + }; + }(); + + if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + } + + // Mount the routes on this router onto an express app (or express router) + + }, { + key: 'mountOnto', + value: function mountOnto(expressApp) { + var _this = this; + + this.routes.forEach(function (route) { + var method = route.method.toLowerCase(); + var handler = makeExpressHandler(_this.appId, route.handler); + expressApp[method].call(expressApp, route.path, handler); + }); + return expressApp; + } + }, { + key: 'expressRouter', + value: function expressRouter() { + return this.mountOnto(_express2.default.Router()); + } + }, { + key: 'tryRouteRequest', + value: function tryRouteRequest(method, path, request) { + var match = this.match(method, path); + if (!match) { + throw new _node2.default.Error(_node2.default.Error.INVALID_JSON, 'cannot route ' + method + ' ' + path); + } + request.params = match.params; + return new Promise(function (resolve, reject) { + match.handler(request).then(resolve, reject); + }); + } + }]); + + return PromiseRouter; +}(); + +// A helper function to make an express handler out of a a promise +// handler. +// Express handlers should never throw; if a promise handler throws we +// just treat it like it resolved to an error. + + +exports.default = PromiseRouter; +function makeExpressHandler(appId, promiseHandler) { + return function (req, res, next) { + try { + var url = maskSensitiveUrl(req); + var body = Object.assign({}, req.body); + var stringifiedBody = JSON.stringify(body, null, 2); + _logger2.default.verbose('REQUEST for [' + req.method + '] ' + url + ': ' + stringifiedBody, { + method: req.method, + url: url, + headers: req.headers, + body: body + }); + promiseHandler(req).then(function (result) { + if (!result.response && !result.location && !result.text) { + _logger2.default.error('the handler did not include a "response" or a "location" field'); + throw 'control should not get here'; + } + + var stringifiedResponse = JSON.stringify(result, null, 2); + _logger2.default.verbose('RESPONSE from [' + req.method + '] ' + url + ': ' + stringifiedResponse, { result: 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(function (header) { + res.set(header, result.headers[header]); + }); + } + res.json(result.response); + }, function (e) { + _logger2.default.error('Error generating response. ' + (0, _util.inspect)(e), { error: e }); + next(e); + }); + } catch (e) { + _logger2.default.error('Error handling request: ' + (0, _util.inspect)(e), { error: e }); + next(e); + } + }; +} + +function maskSensitiveUrl(req) { + var maskUrl = req.originalUrl.toString(); + var shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login') && !req.originalUrl.includes('classes'); + if (shouldMaskUrl) { + maskUrl = _logger2.default.maskSensitiveUrl(maskUrl); + } + return maskUrl; +} \ No newline at end of file diff --git a/lib/Push/PushQueue.js b/lib/Push/PushQueue.js new file mode 100644 index 0000000000..286ddd6fed --- /dev/null +++ b/lib/Push/PushQueue.js @@ -0,0 +1,92 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PushQueue = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _ParseMessageQueue = require('../ParseMessageQueue'); + +var _rest = require('../rest'); + +var _rest2 = _interopRequireDefault(_rest); + +var _utils = require('./utils'); + +var _deepcopy = require('deepcopy'); + +var _deepcopy2 = _interopRequireDefault(_deepcopy); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var PUSH_CHANNEL = 'parse-server-push'; +var DEFAULT_BATCH_SIZE = 100; + +var PushQueue = exports.PushQueue = function () { + + // config object of the publisher, right now it only contains the redisURL, + // but we may extend it later. + function PushQueue() { + var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + _classCallCheck(this, PushQueue); + + this.channel = config.channel || PUSH_CHANNEL; + this.batchSize = config.batchSize || DEFAULT_BATCH_SIZE; + this.parsePublisher = _ParseMessageQueue.ParseMessageQueue.createPublisher(config); + } + + _createClass(PushQueue, [{ + key: 'enqueue', + value: function enqueue(body, where, config, auth, pushStatus) { + var _this = this; + + var limit = this.batchSize; + // Order by badge (because the payload is badge dependant) + // and createdAt to fix the order + var order = (0, _utils.isPushIncrementing)(body) ? 'badge,createdAt' : 'createdAt'; + where = (0, _deepcopy2.default)(where); + if (!where.hasOwnProperty('deviceToken')) { + where['deviceToken'] = { '$exists': true }; + } + return Promise.resolve().then(function () { + return _rest2.default.find(config, auth, '_Installation', where, { limit: 0, count: true }); + }).then(function (_ref) { + var results = _ref.results, + count = _ref.count; + + if (!results) { + return Promise.reject({ error: 'PushController: no results in query' }); + } + pushStatus.setRunning(count); + var skip = 0; + while (skip < count) { + var query = { where: where, + limit: limit, + skip: skip, + order: order }; + + var pushWorkItem = { + body: body, + query: query, + pushStatus: { objectId: pushStatus.objectId }, + applicationId: config.applicationId + }; + _this.parsePublisher.publish(_this.channel, JSON.stringify(pushWorkItem)); + skip += limit; + } + }); + } + }], [{ + key: 'defaultPushChannel', + value: function defaultPushChannel() { + return PUSH_CHANNEL; + } + }]); + + return PushQueue; +}(); \ No newline at end of file diff --git a/lib/Push/PushWorker.js b/lib/Push/PushWorker.js new file mode 100644 index 0000000000..0e0bc8c090 --- /dev/null +++ b/lib/Push/PushWorker.js @@ -0,0 +1,144 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PushWorker = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _deepcopy = require('deepcopy'); + +var _deepcopy2 = _interopRequireDefault(_deepcopy); + +var _AdaptableController = require('../Controllers/AdaptableController'); + +var _AdaptableController2 = _interopRequireDefault(_AdaptableController); + +var _Auth = require('../Auth'); + +var _Config = require('../Config'); + +var _Config2 = _interopRequireDefault(_Config); + +var _PushAdapter = require('../Adapters/Push/PushAdapter'); + +var _rest = require('../rest'); + +var _rest2 = _interopRequireDefault(_rest); + +var _StatusHandler = require('../StatusHandler'); + +var _utils = require('./utils'); + +var _ParseMessageQueue = require('../ParseMessageQueue'); + +var _PushQueue = require('./PushQueue'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var UNSUPPORTED_BADGE_KEY = "unsupported"; + +function groupByBadge(installations) { + return installations.reduce(function (map, installation) { + var badge = installation.badge + ''; + if (installation.deviceType != "ios") { + badge = UNSUPPORTED_BADGE_KEY; + } + map[badge] = map[badge] || []; + map[badge].push(installation); + return map; + }, {}); +} + +var PushWorker = exports.PushWorker = function () { + function PushWorker(pushAdapter) { + var _this = this; + + var subscriberConfig = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + + _classCallCheck(this, PushWorker); + + _AdaptableController2.default.validateAdapter(pushAdapter, this, _PushAdapter.PushAdapter); + this.adapter = pushAdapter; + + this.channel = subscriberConfig.channel || _PushQueue.PushQueue.defaultPushChannel(); + this.subscriber = _ParseMessageQueue.ParseMessageQueue.createSubscriber(subscriberConfig); + if (this.subscriber) { + var subscriber = this.subscriber; + subscriber.subscribe(this.channel); + subscriber.on('message', function (channel, messageStr) { + var workItem = JSON.parse(messageStr); + _this.run(workItem); + }); + } + } + + _createClass(PushWorker, [{ + key: 'unsubscribe', + value: function unsubscribe() { + if (this.subscriber) { + this.subscriber.unsubscribe(this.channel); + } + } + }, { + key: 'run', + value: function run(_ref) { + var _this2 = this; + + var body = _ref.body, + query = _ref.query, + pushStatus = _ref.pushStatus, + applicationId = _ref.applicationId; + + var config = new _Config2.default(applicationId); + var auth = (0, _Auth.master)(config); + var where = query.where; + delete query.where; + return _rest2.default.find(config, auth, '_Installation', where, query).then(function (_ref2) { + var results = _ref2.results; + + if (results.length == 0) { + return; + } + return _this2.sendToAdapter(body, results, pushStatus, config); + }, function (err) { + throw err; + }); + } + }, { + key: 'sendToAdapter', + value: function sendToAdapter(body, installations, pushStatus, config) { + var _this3 = this; + + pushStatus = (0, _StatusHandler.pushStatusHandler)(config, pushStatus.objectId); + if (!(0, _utils.isPushIncrementing)(body)) { + return this.adapter.send(body, installations, pushStatus.objectId).then(function (results) { + return pushStatus.trackSent(results); + }); + } + + // Collect the badges to reduce the # of calls + var badgeInstallationsMap = groupByBadge(installations); + + // Map the on the badges count and return the send result + var promises = Object.keys(badgeInstallationsMap).map(function (badge) { + var payload = (0, _deepcopy2.default)(body); + if (badge == UNSUPPORTED_BADGE_KEY) { + delete payload.data.badge; + } else { + payload.data.badge = parseInt(badge); + } + var installations = badgeInstallationsMap[badge]; + return _this3.sendToAdapter(payload, installations, pushStatus, config); + }); + return Promise.all(promises); + } + }]); + + return PushWorker; +}(); + +exports.default = PushWorker; \ No newline at end of file diff --git a/lib/Push/utils.js b/lib/Push/utils.js new file mode 100644 index 0000000000..ce08e94c13 --- /dev/null +++ b/lib/Push/utils.js @@ -0,0 +1,41 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.isPushIncrementing = isPushIncrementing; +exports.validatePushType = validatePushType; + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function isPushIncrementing(body) { + return body.data && body.data.badge && typeof body.data.badge == 'string' && body.data.badge.toLowerCase() == "increment"; +} + +/** + * Check whether the deviceType parameter in qury condition is valid or not. + * @param {Object} where A query condition + * @param {Array} validPushTypes An array of valid push types(string) + */ +function validatePushType() { + var where = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var validPushTypes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; + + var deviceTypeField = where.deviceType || {}; + var deviceTypes = []; + if (typeof deviceTypeField === 'string') { + deviceTypes.push(deviceTypeField); + } else if (Array.isArray(deviceTypeField['$in'])) { + deviceTypes.concat(deviceTypeField['$in']); + } + for (var i = 0; i < deviceTypes.length; i++) { + var deviceType = deviceTypes[i]; + if (validPushTypes.indexOf(deviceType) < 0) { + throw new _node2.default.Error(_node2.default.Error.PUSH_MISCONFIGURED, deviceType + ' is not supported push type.'); + } + } +} \ No newline at end of file diff --git a/lib/RestQuery.js b/lib/RestQuery.js new file mode 100644 index 0000000000..9489f59c63 --- /dev/null +++ b/lib/RestQuery.js @@ -0,0 +1,989 @@ +'use strict'; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +// An object that encapsulates everything we need to run a 'find' +// operation, encoded in the REST API format. + +var SchemaController = require('./Controllers/SchemaController'); +var Parse = require('parse/node').Parse; +var triggers = require('./triggers'); + +var AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt']; +// restOptions can include: +// skip +// limit +// order +// count +// include +// keys +// redirectClassNameForKey +function RestQuery(config, auth, className) { + var restWhere = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; + var restOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; + var clientSDK = arguments[5]; + + + this.config = config; + this.auth = auth; + this.className = className; + this.restWhere = restWhere; + this.restOptions = restOptions; + this.clientSDK = clientSDK; + this.response = null; + this.findOptions = {}; + if (!this.auth.isMaster) { + this.findOptions.acl = this.auth.user ? [this.auth.user.id] : null; + if (this.className == '_Session') { + if (!this.findOptions.acl) { + throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'This session token is invalid.'); + } + this.restWhere = { + '$and': [this.restWhere, { + 'user': { + __type: 'Pointer', + className: '_User', + objectId: this.auth.user.id + } + }] + }; + } + } + + this.doCount = false; + + // The format for this.include is not the same as the format for the + // include option - it's the paths we should include, in order, + // stored as arrays, taking into account that we need to include foo + // before including foo.bar. Also it should dedupe. + // For example, passing an arg of include=foo.bar,foo.baz could lead to + // this.include = [['foo'], ['foo', 'baz'], ['foo', 'bar']] + this.include = []; + + // If we have keys, we probably want to force some includes (n-1 level) + // See issue: https://github.com/ParsePlatform/parse-server/issues/3185 + if (restOptions.hasOwnProperty('keys')) { + var keysForInclude = restOptions.keys.split(',').filter(function (key) { + // At least 2 components + return key.split(".").length > 1; + }).map(function (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. + if (keysForInclude.length > 0) { + if (!restOptions.include || restOptions.include.length == 0) { + restOptions.include = keysForInclude; + } else { + restOptions.include += "," + keysForInclude; + } + } + } + + for (var option in restOptions) { + switch (option) { + case 'keys': + { + var keys = restOptions.keys.split(',').concat(AlwaysSelectedKeys); + this.keys = Array.from(new Set(keys)); + break; + } + case 'count': + this.doCount = true; + break; + case 'skip': + case 'limit': + this.findOptions[option] = restOptions[option]; + break; + case 'order': + var fields = restOptions.order.split(','); + this.findOptions.sort = fields.reduce(function (sortMap, field) { + field = field.trim(); + if (field[0] == '-') { + sortMap[field.slice(1)] = -1; + } else { + sortMap[field] = 1; + } + return sortMap; + }, {}); + break; + case 'include': + { + var paths = restOptions.include.split(','); + // Load the existing includes (from keys) + var pathSet = paths.reduce(function (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(function (memo, path, index, parts) { + memo[parts.slice(0, index + 1).join('.')] = true; + return memo; + }, memo); + }, {}); + + this.include = Object.keys(pathSet).map(function (s) { + return s.split('.'); + }).sort(function (a, b) { + return a.length - b.length; // Sort by number of components + }); + break; + } + case 'redirectClassNameForKey': + this.redirectKey = restOptions.redirectClassNameForKey; + this.redirectClassName = null; + break; + default: + throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad option: ' + option); + } + } +} + +// A convenient method to perform all the steps of processing a query +// in order. +// Returns a promise for the response - an object with optional keys +// 'results' and 'count'. +// TODO: consolidate the replaceX functions +RestQuery.prototype.execute = function (executeOptions) { + var _this = this; + + return Promise.resolve().then(function () { + return _this.buildRestWhere(); + }).then(function () { + return _this.runFind(executeOptions); + }).then(function () { + return _this.runCount(); + }).then(function () { + return _this.handleInclude(); + }).then(function () { + return _this.runAfterFindTrigger(); + }).then(function () { + return _this.response; + }); +}; + +RestQuery.prototype.buildRestWhere = function () { + var _this2 = this; + + return Promise.resolve().then(function () { + return _this2.getUserAndRoleACL(); + }).then(function () { + return _this2.redirectClassNameForKey(); + }).then(function () { + return _this2.validateClientClassCreation(); + }).then(function () { + return _this2.replaceSelect(); + }).then(function () { + return _this2.replaceDontSelect(); + }).then(function () { + return _this2.replaceInQuery(); + }).then(function () { + return _this2.replaceNotInQuery(); + }).then(function () { + return _this2.replaceEquality(); + }); +}; + +// Uses the Auth object to get the list of roles, adds the user id +RestQuery.prototype.getUserAndRoleACL = function () { + var _this3 = this; + + if (this.auth.isMaster || !this.auth.user) { + return Promise.resolve(); + } + return this.auth.getUserRoles().then(function (roles) { + // Concat with the roles to prevent duplications on multiple calls + var aclSet = new Set([].concat(_this3.findOptions.acl, roles)); + _this3.findOptions.acl = Array.from(aclSet); + return Promise.resolve(); + }); +}; + +// Changes the className if redirectClassNameForKey is set. +// Returns a promise. +RestQuery.prototype.redirectClassNameForKey = function () { + var _this4 = this; + + if (!this.redirectKey) { + return Promise.resolve(); + } + + // We need to change the class name based on the schema + return this.config.database.redirectClassNameForKey(this.className, this.redirectKey).then(function (newClassName) { + _this4.className = newClassName; + _this4.redirectClassName = newClassName; + }); +}; + +// Validates this operation against the allowClientClassCreation config. +RestQuery.prototype.validateClientClassCreation = function () { + var _this5 = this; + + if (this.config.allowClientClassCreation === false && !this.auth.isMaster && SchemaController.systemClasses.indexOf(this.className) === -1) { + return this.config.database.loadSchema().then(function (schemaController) { + return schemaController.hasClass(_this5.className); + }).then(function (hasClass) { + if (hasClass !== true) { + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'This user is not allowed to access ' + 'non-existent class: ' + _this5.className); + } + }); + } else { + return Promise.resolve(); + } +}; + +function transformInQuery(inQueryObject, className, results) { + var values = []; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = results[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var result = _step.value; + + values.push({ + __type: 'Pointer', + className: className, + objectId: result.objectId + }); + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + delete inQueryObject['$inQuery']; + if (Array.isArray(inQueryObject['$in'])) { + inQueryObject['$in'] = inQueryObject['$in'].concat(values); + } else { + inQueryObject['$in'] = values; + } +} + +// Replaces a $inQuery clause by running the subquery, if there is an +// $inQuery clause. +// The $inQuery clause turns into an $in with values that are just +// pointers to the objects returned in the subquery. +RestQuery.prototype.replaceInQuery = function () { + var _this6 = this; + + var inQueryObject = findObjectWithKey(this.restWhere, '$inQuery'); + if (!inQueryObject) { + return; + } + + // 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'); + } + + var additionalOptions = { + redirectClassNameForKey: inQueryValue.redirectClassNameForKey + }; + + var subquery = new RestQuery(this.config, this.auth, inQueryValue.className, inQueryValue.where, additionalOptions); + return subquery.execute().then(function (response) { + transformInQuery(inQueryObject, subquery.className, response.results); + // Recurse to repeat + return _this6.replaceInQuery(); + }); +}; + +function transformNotInQuery(notInQueryObject, className, results) { + var values = []; + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = results[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var result = _step2.value; + + values.push({ + __type: 'Pointer', + className: className, + objectId: result.objectId + }); + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + + delete notInQueryObject['$notInQuery']; + if (Array.isArray(notInQueryObject['$nin'])) { + notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values); + } else { + notInQueryObject['$nin'] = values; + } +} + +// Replaces a $notInQuery clause by running the subquery, if there is an +// $notInQuery clause. +// The $notInQuery clause turns into a $nin with values that are just +// pointers to the objects returned in the subquery. +RestQuery.prototype.replaceNotInQuery = function () { + var _this7 = this; + + var notInQueryObject = findObjectWithKey(this.restWhere, '$notInQuery'); + if (!notInQueryObject) { + return; + } + + // 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'); + } + + var additionalOptions = { + redirectClassNameForKey: notInQueryValue.redirectClassNameForKey + }; + + var subquery = new RestQuery(this.config, this.auth, notInQueryValue.className, notInQueryValue.where, additionalOptions); + return subquery.execute().then(function (response) { + transformNotInQuery(notInQueryObject, subquery.className, response.results); + // Recurse to repeat + return _this7.replaceNotInQuery(); + }); +}; + +var transformSelect = function transformSelect(selectObject, key, objects) { + var values = []; + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = objects[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var result = _step3.value; + + values.push(result[key]); + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + + delete selectObject['$select']; + if (Array.isArray(selectObject['$in'])) { + selectObject['$in'] = selectObject['$in'].concat(values); + } else { + selectObject['$in'] = values; + } +}; + +// Replaces a $select clause by running the subquery, if there is a +// $select clause. +// The $select clause turns into an $in with values selected out of +// the subquery. +// Returns a possible-promise. +RestQuery.prototype.replaceSelect = function () { + var _this8 = this; + + var selectObject = findObjectWithKey(this.restWhere, '$select'); + if (!selectObject) { + return; + } + + // 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'); + } + + var additionalOptions = { + redirectClassNameForKey: selectValue.query.redirectClassNameForKey + }; + + var subquery = new RestQuery(this.config, this.auth, selectValue.query.className, selectValue.query.where, additionalOptions); + return subquery.execute().then(function (response) { + transformSelect(selectObject, selectValue.key, response.results); + // Keep replacing $select clauses + return _this8.replaceSelect(); + }); +}; + +var transformDontSelect = function transformDontSelect(dontSelectObject, key, objects) { + var values = []; + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + for (var _iterator4 = objects[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var result = _step4.value; + + values.push(result[key]); + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + + delete dontSelectObject['$dontSelect']; + if (Array.isArray(dontSelectObject['$nin'])) { + dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values); + } else { + dontSelectObject['$nin'] = values; + } +}; + +// Replaces a $dontSelect clause by running the subquery, if there is a +// $dontSelect clause. +// The $dontSelect clause turns into an $nin with values selected out of +// the subquery. +// Returns a possible-promise. +RestQuery.prototype.replaceDontSelect = function () { + var _this9 = this; + + var dontSelectObject = findObjectWithKey(this.restWhere, '$dontSelect'); + if (!dontSelectObject) { + return; + } + + // 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'); + } + var additionalOptions = { + redirectClassNameForKey: dontSelectValue.query.redirectClassNameForKey + }; + + var subquery = new RestQuery(this.config, this.auth, dontSelectValue.query.className, dontSelectValue.query.where, additionalOptions); + return subquery.execute().then(function (response) { + transformDontSelect(dontSelectObject, dontSelectValue.key, response.results); + // Keep replacing $dontSelect clauses + return _this9.replaceDontSelect(); + }); +}; + +var cleanResultOfSensitiveUserInfo = function cleanResultOfSensitiveUserInfo(result, auth, config) { + delete result.password; + + if (auth.isMaster || auth.user && auth.user.id === result.objectId) { + return; + } + + var _iteratorNormalCompletion5 = true; + var _didIteratorError5 = false; + var _iteratorError5 = undefined; + + try { + for (var _iterator5 = config.userSensitiveFields[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { + var field = _step5.value; + + delete result[field]; + } + } catch (err) { + _didIteratorError5 = true; + _iteratorError5 = err; + } finally { + try { + if (!_iteratorNormalCompletion5 && _iterator5.return) { + _iterator5.return(); + } + } finally { + if (_didIteratorError5) { + throw _iteratorError5; + } + } + } +}; + +var cleanResultAuthData = function cleanResultAuthData(result) { + if (result.authData) { + Object.keys(result.authData).forEach(function (provider) { + if (result.authData[provider] === null) { + delete result.authData[provider]; + } + }); + + if (Object.keys(result.authData).length == 0) { + delete result.authData; + } + } +}; + +var replaceEqualityConstraint = function replaceEqualityConstraint(constraint) { + if ((typeof constraint === 'undefined' ? 'undefined' : _typeof(constraint)) !== 'object') { + return constraint; + } + var equalToObject = {}; + var hasDirectConstraint = false; + var hasOperatorConstraint = false; + for (var key in constraint) { + if (key.indexOf('$') !== 0) { + hasDirectConstraint = true; + equalToObject[key] = constraint[key]; + } else { + hasOperatorConstraint = true; + } + } + if (hasDirectConstraint && hasOperatorConstraint) { + constraint['$eq'] = equalToObject; + Object.keys(equalToObject).forEach(function (key) { + delete constraint[key]; + }); + } + return constraint; +}; + +RestQuery.prototype.replaceEquality = function () { + if (_typeof(this.restWhere) !== 'object') { + return; + } + for (var 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 () { + var _this10 = this; + + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + if (this.findOptions.limit === 0) { + this.response = { results: [] }; + return Promise.resolve(); + } + var findOptions = Object.assign({}, this.findOptions); + if (this.keys) { + findOptions.keys = this.keys.map(function (key) { + return key.split('.')[0]; + }); + } + if (options.op) { + findOptions.op = options.op; + } + return this.config.database.find(this.className, this.restWhere, findOptions).then(function (results) { + if (_this10.className === '_User') { + var _iteratorNormalCompletion6 = true; + var _didIteratorError6 = false; + var _iteratorError6 = undefined; + + try { + for (var _iterator6 = results[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { + var result = _step6.value; + + cleanResultOfSensitiveUserInfo(result, _this10.auth, _this10.config); + cleanResultAuthData(result); + } + } catch (err) { + _didIteratorError6 = true; + _iteratorError6 = err; + } finally { + try { + if (!_iteratorNormalCompletion6 && _iterator6.return) { + _iterator6.return(); + } + } finally { + if (_didIteratorError6) { + throw _iteratorError6; + } + } + } + } + + _this10.config.filesController.expandFilesInObject(_this10.config, results); + + if (_this10.redirectClassName) { + var _iteratorNormalCompletion7 = true; + var _didIteratorError7 = false; + var _iteratorError7 = undefined; + + try { + for (var _iterator7 = results[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { + var r = _step7.value; + + r.className = _this10.redirectClassName; + } + } catch (err) { + _didIteratorError7 = true; + _iteratorError7 = err; + } finally { + try { + if (!_iteratorNormalCompletion7 && _iterator7.return) { + _iterator7.return(); + } + } finally { + if (_didIteratorError7) { + throw _iteratorError7; + } + } + } + } + _this10.response = { results: results }; + }); +}; + +// Returns a promise for whether it was successful. +// Populates this.response.count with the count +RestQuery.prototype.runCount = function () { + var _this11 = this; + + if (!this.doCount) { + return; + } + this.findOptions.count = true; + delete this.findOptions.skip; + delete this.findOptions.limit; + return this.config.database.find(this.className, this.restWhere, this.findOptions).then(function (c) { + _this11.response.count = c; + }); +}; + +// Augments this.response with data at the paths provided in this.include. +RestQuery.prototype.handleInclude = function () { + var _this12 = this; + + if (this.include.length == 0) { + return; + } + + var pathResponse = includePath(this.config, this.auth, this.response, this.include[0], this.restOptions); + if (pathResponse.then) { + return pathResponse.then(function (newResponse) { + _this12.response = newResponse; + _this12.include = _this12.include.slice(1); + return _this12.handleInclude(); + }); + } else if (this.include.length > 0) { + this.include = this.include.slice(1); + return this.handleInclude(); + } + + return pathResponse; +}; + +//Returns a promise of a processed set of results +RestQuery.prototype.runAfterFindTrigger = function () { + var _this13 = this; + + if (!this.response) { + return; + } + // Avoid doing any setup for triggers if there is no 'afterFind' trigger for this class. + var hasAfterFindHook = triggers.triggerExists(this.className, triggers.Types.afterFind, this.config.applicationId); + if (!hasAfterFindHook) { + 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(function (results) { + _this13.response.results = results; + }); +}; + +// Adds included values to the response. +// Path is a list of field names. +// Returns a promise for an augmented response. +function includePath(config, auth, response, path) { + var restOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; + + var pointers = findPointers(response.results, path); + if (pointers.length == 0) { + return response; + } + var pointersHash = {}; + var _iteratorNormalCompletion8 = true; + var _didIteratorError8 = false; + var _iteratorError8 = undefined; + + try { + for (var _iterator8 = pointers[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { + var pointer = _step8.value; + + if (!pointer) { + continue; + } + var className = pointer.className; + // only include the good pointers + if (className) { + pointersHash[className] = pointersHash[className] || new Set(); + pointersHash[className].add(pointer.objectId); + } + } + } catch (err) { + _didIteratorError8 = true; + _iteratorError8 = err; + } finally { + try { + if (!_iteratorNormalCompletion8 && _iterator8.return) { + _iterator8.return(); + } + } finally { + if (_didIteratorError8) { + throw _iteratorError8; + } + } + } + + var includeRestOptions = {}; + if (restOptions.keys) { + var keys = new Set(restOptions.keys.split(',')); + var keySet = Array.from(keys).reduce(function (set, key) { + var keyPath = key.split('.'); + var i = 0; + for (i; i < path.length; i++) { + if (path[i] != keyPath[i]) { + return set; + } + } + if (i < keyPath.length) { + set.add(keyPath[i]); + } + return set; + }, new Set()); + if (keySet.size > 0) { + includeRestOptions.keys = Array.from(keySet).join(','); + } + } + + var queryPromises = Object.keys(pointersHash).map(function (className) { + var where = { 'objectId': { '$in': Array.from(pointersHash[className]) } }; + var query = new RestQuery(config, auth, className, where, includeRestOptions); + return query.execute({ op: 'get' }).then(function (results) { + results.className = className; + return Promise.resolve(results); + }); + }); + + // Get the objects for all these object ids + return Promise.all(queryPromises).then(function (responses) { + var replace = responses.reduce(function (replace, includeResponse) { + var _iteratorNormalCompletion9 = true; + var _didIteratorError9 = false; + var _iteratorError9 = undefined; + + try { + for (var _iterator9 = includeResponse.results[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { + var obj = _step9.value; + + obj.__type = 'Object'; + obj.className = includeResponse.className; + + if (obj.className == "_User" && !auth.isMaster) { + delete obj.sessionToken; + delete obj.authData; + } + replace[obj.objectId] = obj; + } + } catch (err) { + _didIteratorError9 = true; + _iteratorError9 = err; + } finally { + try { + if (!_iteratorNormalCompletion9 && _iterator9.return) { + _iterator9.return(); + } + } finally { + if (_didIteratorError9) { + throw _iteratorError9; + } + } + } + + return replace; + }, {}); + + var resp = { + results: replacePointers(response.results, path, replace) + }; + if (response.count) { + resp.count = response.count; + } + return resp; + }); +} + +// Object may be a list of REST-format object to find pointers in, or +// it may be a single object. +// If the path yields things that aren't pointers, this throws an error. +// Path is a list of fields to search into. +// Returns a list of pointers in REST format. +function findPointers(object, path) { + if (object instanceof Array) { + var answer = []; + var _iteratorNormalCompletion10 = true; + var _didIteratorError10 = false; + var _iteratorError10 = undefined; + + try { + for (var _iterator10 = object[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { + var x = _step10.value; + + answer = answer.concat(findPointers(x, path)); + } + } catch (err) { + _didIteratorError10 = true; + _iteratorError10 = err; + } finally { + try { + if (!_iteratorNormalCompletion10 && _iterator10.return) { + _iterator10.return(); + } + } finally { + if (_didIteratorError10) { + throw _iteratorError10; + } + } + } + + return answer; + } + + if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) !== 'object' || !object) { + return []; + } + + if (path.length == 0) { + if (object === null || object.__type == 'Pointer') { + return [object]; + } + return []; + } + + var subobject = object[path[0]]; + if (!subobject) { + return []; + } + return findPointers(subobject, path.slice(1)); +} + +// Object may be a list of REST-format objects to replace pointers +// in, or it may be a single object. +// Path is a list of fields to search into. +// replace is a map from object id -> object. +// Returns something analogous to object, but with the appropriate +// pointers inflated. +function replacePointers(object, path, replace) { + if (object instanceof Array) { + return object.map(function (obj) { + return replacePointers(obj, path, replace); + }).filter(function (obj) { + return typeof obj !== 'undefined'; + }); + } + + if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) !== 'object' || !object) { + return object; + } + + if (path.length === 0) { + if (object && object.__type === 'Pointer') { + return replace[object.objectId]; + } + return object; + } + + var subobject = object[path[0]]; + if (!subobject) { + return object; + } + var newsub = replacePointers(subobject, path.slice(1), replace); + var answer = {}; + for (var key in object) { + if (key == path[0]) { + answer[key] = newsub; + } else { + answer[key] = object[key]; + } + } + return answer; +} + +// Finds a subobject that has the given key, if there is one. +// Returns undefined otherwise. +function findObjectWithKey(root, key) { + if ((typeof root === 'undefined' ? 'undefined' : _typeof(root)) !== 'object') { + return; + } + if (root instanceof Array) { + var _iteratorNormalCompletion11 = true; + var _didIteratorError11 = false; + var _iteratorError11 = undefined; + + try { + for (var _iterator11 = root[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) { + var item = _step11.value; + + var answer = findObjectWithKey(item, key); + if (answer) { + return answer; + } + } + } catch (err) { + _didIteratorError11 = true; + _iteratorError11 = err; + } finally { + try { + if (!_iteratorNormalCompletion11 && _iterator11.return) { + _iterator11.return(); + } + } finally { + if (_didIteratorError11) { + throw _iteratorError11; + } + } + } + } + if (root && root[key]) { + return root; + } + for (var subkey in root) { + var _answer = findObjectWithKey(root[subkey], key); + if (_answer) { + return _answer; + } + } +} + +module.exports = RestQuery; \ No newline at end of file diff --git a/lib/RestWrite.js b/lib/RestWrite.js new file mode 100644 index 0000000000..cd115ac922 --- /dev/null +++ b/lib/RestWrite.js @@ -0,0 +1,1104 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _RestQuery = require('./RestQuery'); + +var _RestQuery2 = _interopRequireDefault(_RestQuery); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// A RestWrite encapsulates everything we need to run an operation +// that writes to the database. +// This could be either a "create" or an "update". + +var SchemaController = require('./Controllers/SchemaController'); +var deepcopy = require('deepcopy'); + +var Auth = require('./Auth'); +var cryptoUtils = require('./cryptoUtils'); +var passwordCrypto = require('./password'); +var Parse = require('parse/node'); +var triggers = require('./triggers'); +var ClientSDK = require('./ClientSDK'); + + +// query and data are both provided in REST API format. So data +// types are encoded by plain old objects. +// If query is null, this is a "create" and the data in data should be +// created. +// Otherwise this is an "update" - the object matching the query +// should get updated with data. +// 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) { + this.config = config; + this.auth = auth; + this.className = className; + this.clientSDK = clientSDK; + this.storage = {}; + this.runOptions = {}; + if (!query && data.objectId) { + 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 + // fields. + // response: the actual data to be returned + // status: the http status code. if not present, treated like a 200 + // location: the location header. if not present, no location header + this.response = null; + + // Processing this operation may mutate our data, so we operate on a + // copy + this.query = deepcopy(query); + this.data = deepcopy(data); + // We never change originalData, so we do not need a deep copy + this.originalData = originalData; + + // The timestamp we'll use for this whole operation + this.updatedAt = Parse._encode(new Date()).iso; +} + +// A convenient method to perform all the steps of processing the +// write, in order. +// Returns a promise for a {response, status, location} object. +// status and location are optional. +RestWrite.prototype.execute = function () { + var _this = this; + + return Promise.resolve().then(function () { + return _this.getUserAndRoleACL(); + }).then(function () { + return _this.validateClientClassCreation(); + }).then(function () { + return _this.handleInstallation(); + }).then(function () { + return _this.handleSession(); + }).then(function () { + return _this.validateAuthData(); + }).then(function () { + return _this.runBeforeTrigger(); + }).then(function () { + return _this.validateSchema(); + }).then(function () { + return _this.setRequiredFieldsIfNeeded(); + }).then(function () { + return _this.transformUser(); + }).then(function () { + return _this.expandFilesForExistingObjects(); + }).then(function () { + return _this.runDatabaseOperation(); + }).then(function () { + return _this.createSessionTokenIfNeeded(); + }).then(function () { + return _this.handleFollowup(); + }).then(function () { + return _this.runAfterTrigger(); + }).then(function () { + return _this.cleanUserAuthData(); + }).then(function () { + return _this.response; + }); +}; + +// Uses the Auth object to get the list of roles, adds the user id +RestWrite.prototype.getUserAndRoleACL = function () { + var _this2 = this; + + if (this.auth.isMaster) { + return Promise.resolve(); + } + + this.runOptions.acl = ['*']; + + if (this.auth.user) { + return this.auth.getUserRoles().then(function (roles) { + roles.push(_this2.auth.user.id); + _this2.runOptions.acl = _this2.runOptions.acl.concat(roles); + return; + }); + } else { + return Promise.resolve(); + } +}; + +// Validates this operation against the allowClientClassCreation config. +RestWrite.prototype.validateClientClassCreation = function () { + var _this3 = this; + + if (this.config.allowClientClassCreation === false && !this.auth.isMaster && SchemaController.systemClasses.indexOf(this.className) === -1) { + return this.config.database.loadSchema().then(function (schemaController) { + return schemaController.hasClass(_this3.className); + }).then(function (hasClass) { + if (hasClass !== true) { + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'This user is not allowed to access ' + 'non-existent class: ' + _this3.className); + } + }); + } else { + return Promise.resolve(); + } +}; + +// Validates this operation against the schema. +RestWrite.prototype.validateSchema = function () { + return this.config.database.validateObject(this.className, this.data, this.query, this.runOptions); +}; + +// Runs any beforeSave triggers against this operation. +// Any change leads to our data being mutated. +RestWrite.prototype.runBeforeTrigger = function () { + var _this4 = this; + + if (this.response) { + return; + } + + // 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)) { + return Promise.resolve(); + } + + // Cloud code gets a bit of extra data for its objects + var extraData = { className: this.className }; + if (this.query && this.query.objectId) { + extraData.objectId = this.query.objectId; + } + + var originalObject = null; + var updatedObject = triggers.inflate(extraData, this.originalData); + if (this.query && this.query.objectId) { + // This is an update for existing object. + originalObject = triggers.inflate(extraData, this.originalData); + } + updatedObject.set(this.sanitizedData()); + + return Promise.resolve().then(function () { + return triggers.maybeRunTrigger(triggers.Types.beforeSave, _this4.auth, updatedObject, originalObject, _this4.config); + }).then(function (response) { + if (response && response.object) { + _this4.storage.fieldsChangedByTrigger = _lodash2.default.reduce(response.object, function (result, value, key) { + if (!_lodash2.default.isEqual(_this4.data[key], value)) { + result.push(key); + } + return result; + }, []); + _this4.data = response.object; + // We should delete the objectId for an update write + if (_this4.query && _this4.query.objectId) { + delete _this4.data.objectId; + } + } + }); +}; + +RestWrite.prototype.setRequiredFieldsIfNeeded = function () { + if (this.data) { + // Add default fields + this.data.updatedAt = this.updatedAt; + if (!this.query) { + this.data.createdAt = this.updatedAt; + + // Only assign new objectId if we are creating new object + if (!this.data.objectId) { + this.data.objectId = cryptoUtils.newObjectId(); + } + } + } + return Promise.resolve(); +}; + +// Transforms auth data for a user object. +// Does nothing if this isn't a user object. +// Returns a promise for when we're done if it can't finish this tick. +RestWrite.prototype.validateAuthData = function () { + if (this.className !== '_User') { + return; + } + + if (!this.query && !this.data.authData) { + if (typeof this.data.username !== 'string' || _lodash2.default.isEmpty(this.data.username)) { + throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'bad or missing username'); + } + if (typeof this.data.password !== 'string' || _lodash2.default.isEmpty(this.data.password)) { + throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'password is required'); + } + } + + if (!this.data.authData || !Object.keys(this.data.authData).length) { + return; + } + + var authData = this.data.authData; + var providers = Object.keys(authData); + if (providers.length > 0) { + var canHandleAuthData = providers.reduce(function (canHandle, provider) { + var providerAuthData = authData[provider]; + 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.'); +}; + +RestWrite.prototype.handleAuthDataValidation = function (authData) { + var _this5 = this; + + var validations = Object.keys(authData).map(function (provider) { + if (authData[provider] === null) { + return Promise.resolve(); + } + var validateAuthData = _this5.config.authDataManager.getValidatorForProvider(provider); + if (!validateAuthData) { + 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) { + var providers = Object.keys(authData); + var query = providers.reduce(function (memo, provider) { + if (!authData[provider]) { + return memo; + } + var queryKey = 'authData.' + provider + '.id'; + var query = {}; + query[queryKey] = authData[provider].id; + memo.push(query); + return memo; + }, []).filter(function (q) { + return typeof q !== 'undefined'; + }); + + var findPromise = Promise.resolve([]); + if (query.length > 0) { + findPromise = this.config.database.find(this.className, { '$or': query }, {}); + } + + return findPromise; +}; + +RestWrite.prototype.handleAuthData = function (authData) { + var _this6 = this; + + var results = void 0; + return this.findUsersWithAuthData(authData).then(function (r) { + results = 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'); + } + + _this6.storage['authProvider'] = Object.keys(authData).join(','); + + if (results.length > 0) { + if (!_this6.query) { + // Login with auth data + delete results[0].password; + var userResult = results[0]; + + // need to set the objectId first otherwise location has trailing undefined + _this6.data.objectId = userResult.objectId; + + // Determine if authData was updated + var mutatedAuthData = {}; + Object.keys(authData).forEach(function (provider) { + var providerData = authData[provider]; + var userAuthData = userResult.authData[provider]; + if (!_lodash2.default.isEqual(providerData, userAuthData)) { + mutatedAuthData[provider] = providerData; + } + }); + _this6.response = { + response: userResult, + location: _this6.location() + }; + + // If we didn't change the auth data, just keep going + if (Object.keys(mutatedAuthData).length === 0) { + return; + } + // We have authData that is updated on login + // that can happen when token are refreshed, + // We should update the token and let the user in + // We should only check the mutated keys + return _this6.handleAuthDataValidation(mutatedAuthData).then(function () { + // Assign the new authData in the response + Object.keys(mutatedAuthData).forEach(function (provider) { + _this6.response.response.authData[provider] = mutatedAuthData[provider]; + }); + // Run the DB update directly, as 'master' + // Just update the authData part + return _this6.config.database.update(_this6.className, { objectId: _this6.data.objectId }, { authData: mutatedAuthData }, {}); + }); + } else if (_this6.query && _this6.query.objectId) { + // Trying to update auth data but users + // are different + if (results[0].objectId !== _this6.query.objectId) { + throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used'); + } + } + } + return _this6.handleAuthDataValidation(authData); + }); +}; + +// The non-third-party parts of User transformation +RestWrite.prototype.transformUser = function () { + var _this7 = this; + + var promise = Promise.resolve(); + + if (this.className !== '_User') { + return promise; + } + + if (!this.auth.isMaster && "emailVerified" in this.data) { + var error = 'Clients aren\'t allowed to manually update email verification.'; + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + } + + // Do not cleanup session if objectId is not set + if (this.query && this.objectId()) { + // If we're updating a _User object, we need to clear out the cache for that user. Find all their + // session tokens, and remove them from the cache. + promise = new _RestQuery2.default(this.config, Auth.master(this.config), '_Session', { + user: { + __type: "Pointer", + className: "_User", + objectId: this.objectId() + } + }).execute().then(function (results) { + results.results.forEach(function (session) { + return _this7.config.cacheController.user.del(session.sessionToken); + }); + }); + } + + return promise.then(function () { + // Transform the password + if (_this7.data.password === undefined) { + // ignore only if undefined. should proceed if empty ('') + return Promise.resolve(); + } + + if (_this7.query) { + _this7.storage['clearSessions'] = true; + // Generate a new session only if the user requested + if (!_this7.auth.isMaster) { + _this7.storage['generateNewSession'] = true; + } + } + + return _this7._validatePasswordPolicy().then(function () { + return passwordCrypto.hash(_this7.data.password).then(function (hashedPassword) { + _this7.data._hashed_password = hashedPassword; + delete _this7.data.password; + }); + }); + }).then(function () { + return _this7._validateUserName(); + }).then(function () { + return _this7._validateEmail(); + }); +}; + +RestWrite.prototype._validateUserName = function () { + // Check for username uniqueness + if (!this.data.username) { + if (!this.query) { + this.data.username = cryptoUtils.randomString(25); + this.responseShouldHaveUsername = true; + } + return Promise.resolve(); + } + // 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(function (results) { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); + } + return; + }); +}; + +RestWrite.prototype._validateEmail = function () { + var _this8 = this; + + if (!this.data.email || this.data.email.__op === 'Delete') { + return Promise.resolve(); + } + // 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.')); + } + // 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(function (results) { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); + } + // We updated the email, send a new validation + _this8.storage['sendVerificationEmail'] = true; + _this8.config.userController.setEmailVerifyToken(_this8.data); + }); +}; + +RestWrite.prototype._validatePasswordPolicy = function () { + var _this9 = this; + + if (!this.config.passwordPolicy) return Promise.resolve(); + return this._validatePasswordRequirements().then(function () { + return _this9._validatePasswordHistory(); + }); +}; + +RestWrite.prototype._validatePasswordRequirements = function () { + var _this10 = this; + + // check if the password conforms to the defined password policy if configured + var 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)); + } + + // 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.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() }).then(function (results) { + if (results.length != 1) { + throw undefined; + } + if (_this10.data.password.indexOf(results[0].username) >= 0) return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError)); + return Promise.resolve(); + }); + } + } + return Promise.resolve(); +}; + +RestWrite.prototype._validatePasswordHistory = function () { + var _this11 = this; + + // 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"] }).then(function (results) { + if (results.length != 1) { + throw undefined; + } + var user = results[0]; + var oldPasswords = []; + if (user._password_history) oldPasswords = _lodash2.default.take(user._password_history, _this11.config.passwordPolicy.maxPasswordHistory - 1); + oldPasswords.push(user.password); + var newPassword = _this11.data.password; + // compare the new password hash with all old password hashes + var promises = oldPasswords.map(function (hash) { + return passwordCrypto.compare(newPassword, hash).then(function (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(function () { + return Promise.resolve(); + }).catch(function (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 ' + _this11.config.passwordPolicy.maxPasswordHistory + ' passwords.')); + throw err; + }); + }); + } + return Promise.resolve(); +}; + +RestWrite.prototype.createSessionTokenIfNeeded = function () { + if (this.className !== '_User') { + return; + } + if (this.query) { + return; + } + return this.createSessionToken(); +}; + +RestWrite.prototype.createSessionToken = function () { + // cloud installationId from Cloud Code, + // never create session tokens from there. + if (this.auth.installationId && this.auth.installationId === 'cloud') { + return; + } + var token = 'r:' + cryptoUtils.newToken(); + + var expiresAt = this.config.generateSessionExpiresAt(); + var sessionData = { + sessionToken: token, + user: { + __type: 'Pointer', + className: '_User', + objectId: this.objectId() + }, + createdWith: { + 'action': 'signup', + 'authProvider': this.storage['authProvider'] || 'password' + }, + restricted: false, + installationId: this.auth.installationId, + expiresAt: Parse._encode(expiresAt) + }; + if (this.response && this.response.response) { + this.response.response.sessionToken = token; + } + var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData); + return create.execute(); +}; + +// Handles any followup logic +RestWrite.prototype.handleFollowup = function () { + if (this.storage && this.storage['clearSessions'] && this.config.revokeSessionOnPasswordReset) { + var sessionQuery = { + user: { + __type: 'Pointer', + className: '_User', + objectId: this.objectId() + } + }; + delete this.storage['clearSessions']; + 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)); + } + + if (this.storage && this.storage['sendVerificationEmail']) { + delete this.storage['sendVerificationEmail']; + // Fire and forget! + this.config.userController.sendVerificationEmail(this.data); + return this.handleFollowup.bind(this); + } +}; + +// Handles the _Session class specialness. +// Does nothing if this isn't an installation object. +RestWrite.prototype.handleSession = function () { + var _this12 = this; + + if (this.response || this.className !== '_Session') { + return; + } + + if (!this.auth.user && !this.auth.isMaster) { + 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.'); + } + + if (!this.query && !this.auth.isMaster) { + var token = 'r:' + cryptoUtils.newToken(); + var expiresAt = this.config.generateSessionExpiresAt(); + var sessionData = { + sessionToken: token, + user: { + __type: 'Pointer', + className: '_User', + objectId: this.auth.user.id + }, + createdWith: { + 'action': 'create' + }, + restricted: true, + expiresAt: Parse._encode(expiresAt) + }; + for (var key in this.data) { + if (key == 'objectId') { + continue; + } + sessionData[key] = this.data[key]; + } + var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData); + return create.execute().then(function (results) { + if (!results.response) { + throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Error creating session.'); + } + sessionData['objectId'] = results.response['objectId']; + _this12.response = { + status: 201, + location: results.location, + response: sessionData + }; + }); + } +}; + +// Handles the _Installation class specialness. +// Does nothing if this isn't an installation object. +// If an installation is found, this can mutate this.query and turn a create +// into an update. +// Returns a promise for when we're done if it can't finish this tick. +RestWrite.prototype.handleInstallation = function () { + var _this13 = this; + + if (this.response || this.className !== '_Installation') { + return; + } + + 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'); + } + + // If the device token is 64 characters long, we assume it is for iOS + // and lowercase it. + if (this.data.deviceToken && this.data.deviceToken.length == 64) { + this.data.deviceToken = this.data.deviceToken.toLowerCase(); + } + + // We lowercase the installationId if present + if (this.data.installationId) { + this.data.installationId = this.data.installationId.toLowerCase(); + } + + var installationId = this.data.installationId; + + // If data.installationId is not set and we're not master, we can lookup in auth + if (!installationId && !this.auth.isMaster) { + installationId = this.auth.installationId; + } + + if (installationId) { + installationId = installationId.toLowerCase(); + } + + // Updating _Installation but not updating anything critical + if (this.query && !this.data.deviceToken && !installationId && !this.data.deviceType) { + return; + } + + var promise = Promise.resolve(); + + var idMatch; // Will be a match on either objectId or installationId + var objectIdMatch; + var installationIdMatch; + var deviceTokenMatches = []; + + // Instead of issuing 3 reads, let's do it with one OR. + var orQueries = []; + if (this.query && this.query.objectId) { + orQueries.push({ + objectId: this.query.objectId + }); + } + if (installationId) { + orQueries.push({ + 'installationId': installationId + }); + } + if (this.data.deviceToken) { + orQueries.push({ 'deviceToken': this.data.deviceToken }); + } + + if (orQueries.length == 0) { + return; + } + + promise = promise.then(function () { + return _this13.config.database.find('_Installation', { + '$or': orQueries + }, {}); + }).then(function (results) { + results.forEach(function (result) { + if (_this13.query && _this13.query.objectId && result.objectId == _this13.query.objectId) { + objectIdMatch = result; + } + if (result.installationId == installationId) { + installationIdMatch = result; + } + if (result.deviceToken == _this13.data.deviceToken) { + deviceTokenMatches.push(result); + } + }); + + // Sanity checks when running a query + if (_this13.query && _this13.query.objectId) { + if (!objectIdMatch) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for update.'); + } + if (_this13.data.installationId && objectIdMatch.installationId && _this13.data.installationId !== objectIdMatch.installationId) { + throw new Parse.Error(136, 'installationId may not be changed in this ' + 'operation'); + } + if (_this13.data.deviceToken && objectIdMatch.deviceToken && _this13.data.deviceToken !== objectIdMatch.deviceToken && !_this13.data.installationId && !objectIdMatch.installationId) { + throw new Parse.Error(136, 'deviceToken may not be changed in this ' + 'operation'); + } + if (_this13.data.deviceType && _this13.data.deviceType && _this13.data.deviceType !== objectIdMatch.deviceType) { + throw new Parse.Error(136, 'deviceType may not be changed in this ' + 'operation'); + } + } + + if (_this13.query && _this13.query.objectId && objectIdMatch) { + idMatch = objectIdMatch; + } + + if (installationId && installationIdMatch) { + idMatch = installationIdMatch; + } + // need to specify deviceType only if it's new + if (!_this13.query && !_this13.data.deviceType && !idMatch) { + throw new Parse.Error(135, 'deviceType must be specified in this operation'); + } + }).then(function () { + 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 (!_this13.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': _this13.data.deviceToken, + 'installationId': { + '$ne': installationId + } + }; + if (_this13.data.appIdentifier) { + delQuery['appIdentifier'] = _this13.data.appIdentifier; + } + _this13.config.database.destroy('_Installation', delQuery).catch(function (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 (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. + var _delQuery = { objectId: idMatch.objectId }; + return _this13.config.database.destroy('_Installation', _delQuery).then(function () { + return deviceTokenMatches[0]['objectId']; + }).catch(function (err) { + if (err.code == Parse.Error.OBJECT_NOT_FOUND) { + // no deletions were made. Can be ignored + return; + } + // rethrow the error + throw err; + }); + } else { + if (_this13.data.deviceToken && idMatch.deviceToken != _this13.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. + var _delQuery2 = { + 'deviceToken': _this13.data.deviceToken + }; + // We have a unique install Id, use that to preserve + // the interesting installation + if (_this13.data.installationId) { + _delQuery2['installationId'] = { + '$ne': _this13.data.installationId + }; + } else if (idMatch.objectId && _this13.data.objectId && idMatch.objectId == _this13.data.objectId) { + // we passed an objectId, preserve that instalation + _delQuery2['objectId'] = { + '$ne': idMatch.objectId + }; + } else { + // What to do here? can't really clean up everything... + return idMatch.objectId; + } + if (_this13.data.appIdentifier) { + _delQuery2['appIdentifier'] = _this13.data.appIdentifier; + } + _this13.config.database.destroy('_Installation', _delQuery2).catch(function (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; + } + } + }).then(function (objId) { + if (objId) { + _this13.query = { objectId: objId }; + delete _this13.data.objectId; + delete _this13.data.createdAt; + } + // TODO: Validate ops (add/remove on channels, $inc on badge, etc.) + }); + return promise; +}; + +// If we short-circuted the object response - then we need to make sure we expand all the files, +// since this might not have a query, meaning it won't return the full result back. +// TODO: (nlutsenko) This should die when we move to per-class based controllers on _Session/_User +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); + } +}; + +RestWrite.prototype.runDatabaseOperation = function () { + var _this14 = this; + + if (this.response) { + return; + } + + if (this.className === '_Role') { + this.config.cacheController.role.clear(); + } + + if (this.className === '_User' && this.query && !this.auth.couldUpdateUserId(this.query.objectId)) { + throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Cannot modify user ' + this.query.objectId + '.'); + } + + if (this.className === '_Product' && this.data.download) { + this.data.downloadName = this.data.download.name; + } + + // TODO: Add better detection for ACL, ensuring a user can't be locked from + // their own user record. + if (this.data.ACL && this.data.ACL['*unresolved']) { + throw new Parse.Error(Parse.Error.INVALID_ACL, 'Invalid ACL.'); + } + + if (this.query) { + // Force the user to not lockout + // Matched with parse.com + if (this.className === '_User' && this.data.ACL) { + this.data.ACL[this.query.objectId] = { read: true, write: true }; + } + // update password timestamp if user password is being changed + 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 + delete this.data.createdAt; + + var 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(function (results) { + if (results.length != 1) { + throw undefined; + } + var user = results[0]; + var oldPasswords = []; + if (user._password_history) { + oldPasswords = _lodash2.default.take(user._password_history, _this14.config.passwordPolicy.maxPasswordHistory); + } + //n-1 passwords go into history including last password + while (oldPasswords.length > _this14.config.passwordPolicy.maxPasswordHistory - 2) { + oldPasswords.shift(); + } + oldPasswords.push(user.password); + _this14.data._password_history = oldPasswords; + }); + } + + return defer.then(function () { + // Run an update + return _this14.config.database.update(_this14.className, _this14.query, _this14.data, _this14.runOptions).then(function (response) { + response.updatedAt = _this14.updatedAt; + _this14._updateResponseWithData(response, _this14.data); + _this14.response = { response: response }; + }); + }); + } else { + // Set the default ACL and password timestamp for the new _User + if (this.className === '_User') { + var ACL = this.data.ACL; + // default public r/w ACL + if (!ACL) { + ACL = {}; + ACL['*'] = { read: true, write: false }; + } + // make sure the user is not locked down + ACL[this.data.objectId] = { read: true, write: true }; + this.data.ACL = ACL; + // password timestamp to be used when password expiry policy is enforced + 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).catch(function (error) { + if (_this14.className !== '_User' || error.code !== Parse.Error.DUPLICATE_VALUE) { + throw error; + } + // If this was a failed user creation due to username or email already taken, we need to + // check whether it was username or email and return the appropriate error. + + // TODO: See if we can later do this without additional queries by using named indexes. + return _this14.config.database.find(_this14.className, { username: _this14.data.username, objectId: { '$ne': _this14.objectId() } }, { limit: 1 }).then(function (results) { + if (results.length > 0) { + throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); + } + return _this14.config.database.find(_this14.className, { email: _this14.data.email, objectId: { '$ne': _this14.objectId() } }, { limit: 1 }); + }).then(function (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.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); + }); + }).then(function (response) { + response.objectId = _this14.data.objectId; + response.createdAt = _this14.data.createdAt; + + if (_this14.responseShouldHaveUsername) { + response.username = _this14.data.username; + } + _this14._updateResponseWithData(response, _this14.data); + _this14.response = { + status: 201, + response: response, + location: _this14.location() + }; + }); + } +}; + +// Returns nothing - doesn't wait for the trigger. +RestWrite.prototype.runAfterTrigger = function () { + if (!this.response || !this.response.response) { + return; + } + + // Avoid doing any setup for triggers if there is no 'afterSave' trigger for this class. + var hasAfterSaveHook = triggers.triggerExists(this.className, triggers.Types.afterSave, this.config.applicationId); + var hasLiveQuery = this.config.liveQueryController.hasLiveQuery(this.className); + if (!hasAfterSaveHook && !hasLiveQuery) { + return Promise.resolve(); + } + + var extraData = { className: this.className }; + if (this.query && this.query.objectId) { + extraData.objectId = this.query.objectId; + } + + // Build the original object, we only do this for a update write. + var originalObject = void 0; + if (this.query && this.query.objectId) { + originalObject = triggers.inflate(extraData, this.originalData); + } + + // Build the inflated object, different from beforeSave, originalData is not empty + // since developers can change data in the beforeSave. + var updatedObject = triggers.inflate(extraData, this.originalData); + updatedObject.set(this.sanitizedData()); + updatedObject._handleSaveResponse(this.response.response, this.response.status || 200); + + // Notifiy LiveQueryServer if possible + this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject); + + // Run afterSave trigger + return triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config); +}; + +// A helper to figure out what location this operation happens at. +RestWrite.prototype.location = function () { + var middle = this.className === '_User' ? '/users/' : '/classes/' + this.className + '/'; + return this.config.mount + middle + this.data.objectId; +}; + +// A helper to get the object id for this operation. +// Because it could be either on the query or on the data +RestWrite.prototype.objectId = function () { + return this.data.objectId || this.query.objectId; +}; + +// Returns a copy of the data and delete bad keys (_auth_data, _hashed_password...) +RestWrite.prototype.sanitizedData = function () { + var data = Object.keys(this.data).reduce(function (data, key) { + // Regexp comes from Parse.Object.prototype.validate + if (!/^[A-Za-z][0-9A-Za-z_]*$/.test(key)) { + delete data[key]; + } + return data; + }, deepcopy(this.data)); + return Parse._decode(undefined, data); +}; + +RestWrite.prototype.cleanUserAuthData = function () { + if (this.response && this.response.response && this.className === '_User') { + var user = this.response.response; + if (user.authData) { + Object.keys(user.authData).forEach(function (provider) { + if (user.authData[provider] === null) { + delete user.authData[provider]; + } + }); + if (Object.keys(user.authData).length == 0) { + delete user.authData; + } + } + } +}; + +RestWrite.prototype._updateResponseWithData = function (response, data) { + if (_lodash2.default.isEmpty(this.storage.fieldsChangedByTrigger)) { + return response; + } + var clientSupportsDelete = ClientSDK.supportsForwardDelete(this.clientSDK); + this.storage.fieldsChangedByTrigger.forEach(function (fieldName) { + var dataValue = data[fieldName]; + var responseValue = response[fieldName]; + + response[fieldName] = responseValue || dataValue; + + // Strips operations from responses + if (response[fieldName] && response[fieldName].__op) { + delete response[fieldName]; + if (clientSupportsDelete && dataValue.__op == 'Delete') { + response[fieldName] = dataValue; + } + } + }); + return response; +}; + +exports.default = RestWrite; + +module.exports = RestWrite; \ No newline at end of file diff --git a/lib/Routers/AnalyticsRouter.js b/lib/Routers/AnalyticsRouter.js new file mode 100644 index 0000000000..323cd1d5eb --- /dev/null +++ b/lib/Routers/AnalyticsRouter.js @@ -0,0 +1,51 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.AnalyticsRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // AnalyticsRouter.js + + +function appOpened(req) { + var analyticsController = req.config.analyticsController; + return analyticsController.appOpened(req); +} + +function trackEvent(req) { + var analyticsController = req.config.analyticsController; + return analyticsController.trackEvent(req); +} + +var AnalyticsRouter = exports.AnalyticsRouter = function (_PromiseRouter) { + _inherits(AnalyticsRouter, _PromiseRouter); + + function AnalyticsRouter() { + _classCallCheck(this, AnalyticsRouter); + + return _possibleConstructorReturn(this, (AnalyticsRouter.__proto__ || Object.getPrototypeOf(AnalyticsRouter)).apply(this, arguments)); + } + + _createClass(AnalyticsRouter, [{ + key: 'mountRoutes', + value: function mountRoutes() { + this.route('POST', '/events/AppOpened', appOpened); + this.route('POST', '/events/:eventName', trackEvent); + } + }]); + + return AnalyticsRouter; +}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/ClassesRouter.js b/lib/Routers/ClassesRouter.js new file mode 100644 index 0000000000..e8599e0f6e --- /dev/null +++ b/lib/Routers/ClassesRouter.js @@ -0,0 +1,281 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ClassesRouter = undefined; + +var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _rest = require('../rest'); + +var _rest2 = _interopRequireDefault(_rest); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var ALLOWED_GET_QUERY_KEYS = ['keys', 'include']; + +var ClassesRouter = exports.ClassesRouter = function (_PromiseRouter) { + _inherits(ClassesRouter, _PromiseRouter); + + function ClassesRouter() { + _classCallCheck(this, ClassesRouter); + + return _possibleConstructorReturn(this, (ClassesRouter.__proto__ || Object.getPrototypeOf(ClassesRouter)).apply(this, arguments)); + } + + _createClass(ClassesRouter, [{ + key: 'handleFind', + value: function handleFind(req) { + var body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); + var options = {}; + var allowConstraints = ['skip', 'limit', 'order', 'count', 'keys', 'include', 'redirectClassNameForKey', 'where']; + + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = Object.keys(body)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var key = _step.value; + + if (allowConstraints.indexOf(key) === -1) { + throw new _node2.default.Error(_node2.default.Error.INVALID_QUERY, 'Invalid parameter for query: ' + key); + } + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator.return) { + _iterator.return(); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + if (body.skip) { + options.skip = Number(body.skip); + } + if (body.limit || body.limit === 0) { + options.limit = Number(body.limit); + } else { + options.limit = Number(100); + } + if (body.order) { + options.order = String(body.order); + } + if (body.count) { + options.count = true; + } + if (typeof body.keys == 'string') { + options.keys = body.keys; + } + if (body.include) { + options.include = String(body.include); + } + if (body.redirectClassNameForKey) { + options.redirectClassNameForKey = String(body.redirectClassNameForKey); + } + if (typeof body.where === 'string') { + body.where = JSON.parse(body.where); + } + return _rest2.default.find(req.config, req.auth, req.params.className, body.where, options, req.info.clientSDK).then(function (response) { + if (response && response.results) { + var _iteratorNormalCompletion2 = true; + var _didIteratorError2 = false; + var _iteratorError2 = undefined; + + try { + for (var _iterator2 = response.results[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { + var result = _step2.value; + + if (result.sessionToken) { + result.sessionToken = req.info.sessionToken || result.sessionToken; + } + } + } catch (err) { + _didIteratorError2 = true; + _iteratorError2 = err; + } finally { + try { + if (!_iteratorNormalCompletion2 && _iterator2.return) { + _iterator2.return(); + } + } finally { + if (_didIteratorError2) { + throw _iteratorError2; + } + } + } + } + return { response: response }; + }); + } + + // Returns a promise for a {response} object. + + }, { + key: 'handleGet', + value: function handleGet(req) { + var body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); + var options = {}; + + var _iteratorNormalCompletion3 = true; + var _didIteratorError3 = false; + var _iteratorError3 = undefined; + + try { + for (var _iterator3 = Object.keys(body)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { + var key = _step3.value; + + if (ALLOWED_GET_QUERY_KEYS.indexOf(key) === -1) { + throw new _node2.default.Error(_node2.default.Error.INVALID_QUERY, 'Improper encode of parameter'); + } + } + } catch (err) { + _didIteratorError3 = true; + _iteratorError3 = err; + } finally { + try { + if (!_iteratorNormalCompletion3 && _iterator3.return) { + _iterator3.return(); + } + } finally { + if (_didIteratorError3) { + throw _iteratorError3; + } + } + } + + if (typeof body.keys == 'string') { + options.keys = body.keys; + } + if (body.include) { + options.include = String(body.include); + } + + return _rest2.default.get(req.config, req.auth, req.params.className, req.params.objectId, options, req.info.clientSDK).then(function (response) { + if (!response.results || response.results.length == 0) { + throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } + + if (req.params.className === "_User") { + + delete response.results[0].sessionToken; + + var user = response.results[0]; + + if (req.auth.user && user.objectId == req.auth.user.id) { + // Force the session token + response.results[0].sessionToken = req.info.sessionToken; + } + } + return { response: response.results[0] }; + }); + } + }, { + key: 'handleCreate', + value: function handleCreate(req) { + return _rest2.default.create(req.config, req.auth, req.params.className, req.body, req.info.clientSDK); + } + }, { + key: 'handleUpdate', + value: function handleUpdate(req) { + return _rest2.default.update(req.config, req.auth, req.params.className, req.params.objectId, req.body, req.info.clientSDK); + } + }, { + key: 'handleDelete', + value: function handleDelete(req) { + return _rest2.default.del(req.config, req.auth, req.params.className, req.params.objectId, req.info.clientSDK).then(function () { + return { response: {} }; + }); + } + }, { + key: 'mountRoutes', + value: function mountRoutes() { + var _this2 = this; + + this.route('GET', '/classes/:className', function (req) { + return _this2.handleFind(req); + }); + this.route('GET', '/classes/:className/:objectId', function (req) { + return _this2.handleGet(req); + }); + this.route('POST', '/classes/:className', function (req) { + return _this2.handleCreate(req); + }); + this.route('PUT', '/classes/:className/:objectId', function (req) { + return _this2.handleUpdate(req); + }); + this.route('DELETE', '/classes/:className/:objectId', function (req) { + return _this2.handleDelete(req); + }); + } + }], [{ + key: 'JSONFromQuery', + value: function JSONFromQuery(query) { + var json = {}; + var _iteratorNormalCompletion4 = true; + var _didIteratorError4 = false; + var _iteratorError4 = undefined; + + try { + for (var _iterator4 = _lodash2.default.entries(query)[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { + var _step4$value = _slicedToArray(_step4.value, 2), + key = _step4$value[0], + value = _step4$value[1]; + + try { + json[key] = JSON.parse(value); + } catch (e) { + json[key] = value; + } + } + } catch (err) { + _didIteratorError4 = true; + _iteratorError4 = err; + } finally { + try { + if (!_iteratorNormalCompletion4 && _iterator4.return) { + _iterator4.return(); + } + } finally { + if (_didIteratorError4) { + throw _iteratorError4; + } + } + } + + return json; + } + }]); + + return ClassesRouter; +}(_PromiseRouter3.default); + +exports.default = ClassesRouter; \ No newline at end of file diff --git a/lib/Routers/CloudCodeRouter.js b/lib/Routers/CloudCodeRouter.js new file mode 100644 index 0000000000..9ca85d9203 --- /dev/null +++ b/lib/Routers/CloudCodeRouter.js @@ -0,0 +1,54 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.CloudCodeRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var triggers = require('../triggers'); + +var CloudCodeRouter = exports.CloudCodeRouter = function (_PromiseRouter) { + _inherits(CloudCodeRouter, _PromiseRouter); + + function CloudCodeRouter() { + _classCallCheck(this, CloudCodeRouter); + + return _possibleConstructorReturn(this, (CloudCodeRouter.__proto__ || Object.getPrototypeOf(CloudCodeRouter)).apply(this, arguments)); + } + + _createClass(CloudCodeRouter, [{ + key: 'mountRoutes', + value: function mountRoutes() { + this.route('GET', '/cloud_code/jobs', CloudCodeRouter.getJobs); + } + }], [{ + key: 'getJobs', + value: function getJobs(req) { + var config = req.config; + var jobs = triggers.getJobs(config.applicationId) || {}; + return Promise.resolve({ + response: Object.keys(jobs).map(function (jobName) { + return { + jobName: jobName + }; + }) + }); + } + }]); + + return CloudCodeRouter; +}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/FeaturesRouter.js b/lib/Routers/FeaturesRouter.js new file mode 100644 index 0000000000..683c9592b6 --- /dev/null +++ b/lib/Routers/FeaturesRouter.js @@ -0,0 +1,93 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.FeaturesRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _package = require('../../package.json'); + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _middlewares = require('../middlewares'); + +var middleware = _interopRequireWildcard(_middlewares); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var FeaturesRouter = exports.FeaturesRouter = function (_PromiseRouter) { + _inherits(FeaturesRouter, _PromiseRouter); + + function FeaturesRouter() { + _classCallCheck(this, FeaturesRouter); + + return _possibleConstructorReturn(this, (FeaturesRouter.__proto__ || Object.getPrototypeOf(FeaturesRouter)).apply(this, arguments)); + } + + _createClass(FeaturesRouter, [{ + key: 'mountRoutes', + value: function mountRoutes() { + this.route('GET', '/serverInfo', middleware.promiseEnforceMasterKeyAccess, function (req) { + var 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: false + }, + schemas: { + addField: true, + removeField: true, + addClass: true, + removeClass: true, + clearAllDataFromClass: true, + exportClass: false, + editClassLevelPermissions: true, + editPointerPermissions: true + } + }; + + return { response: { + features: features, + parseServerVersion: _package.version + } }; + }); + } + }]); + + return FeaturesRouter; +}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/FilesRouter.js b/lib/Routers/FilesRouter.js new file mode 100644 index 0000000000..e25c29a3b7 --- /dev/null +++ b/lib/Routers/FilesRouter.js @@ -0,0 +1,234 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.FilesRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _express = require('express'); + +var _express2 = _interopRequireDefault(_express); + +var _bodyParser = require('body-parser'); + +var _bodyParser2 = _interopRequireDefault(_bodyParser); + +var _middlewares = require('../middlewares'); + +var Middlewares = _interopRequireWildcard(_middlewares); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _Config = require('../Config'); + +var _Config2 = _interopRequireDefault(_Config); + +var _mime = require('mime'); + +var _mime2 = _interopRequireDefault(_mime); + +var _logger = require('../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var FilesRouter = exports.FilesRouter = function () { + function FilesRouter() { + _classCallCheck(this, FilesRouter); + } + + _createClass(FilesRouter, [{ + key: 'expressRouter', + value: function expressRouter() { + var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + var router = _express2.default.Router(); + router.get('/files/:appId/:filename', this.getHandler); + + router.post('/files', function (req, res, next) { + next(new _node2.default.Error(_node2.default.Error.INVALID_FILE_NAME, 'Filename not provided.')); + }); + + router.post('/files/:filename', Middlewares.allowCrossDomain, _bodyParser2.default.raw({ type: function type() { + return true; + }, limit: options.maxUploadSize || '20mb' }), // Allow uploads without Content-Type, or with any Content-Type. + Middlewares.handleParseHeaders, this.createHandler); + + router.delete('/files/:filename', Middlewares.allowCrossDomain, Middlewares.handleParseHeaders, Middlewares.enforceMasterKeyAccess, this.deleteHandler); + return router; + } + }, { + key: 'getHandler', + value: function getHandler(req, res) { + var config = new _Config2.default(req.params.appId); + var filesController = config.filesController; + var filename = req.params.filename; + var contentType = _mime2.default.lookup(filename); + if (isFileStreamable(req, filesController)) { + filesController.getFileStream(config, filename).then(function (stream) { + handleFileStream(stream, req, res, contentType); + }).catch(function () { + res.status(404); + res.set('Content-Type', 'text/plain'); + res.end('File not found.'); + }); + } else { + filesController.getFileData(config, filename).then(function (data) { + res.status(200); + res.set('Content-Type', contentType); + res.set('Content-Length', data.length); + res.end(data); + }).catch(function () { + res.status(404); + res.set('Content-Type', 'text/plain'); + res.end('File not found.'); + }); + } + } + }, { + key: 'createHandler', + value: function createHandler(req, res, next) { + if (!req.body || !req.body.length) { + next(new _node2.default.Error(_node2.default.Error.FILE_SAVE_ERROR, 'Invalid file upload.')); + return; + } + + if (req.params.filename.length > 128) { + next(new _node2.default.Error(_node2.default.Error.INVALID_FILE_NAME, 'Filename too long.')); + return; + } + + if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { + next(new _node2.default.Error(_node2.default.Error.INVALID_FILE_NAME, 'Filename contains invalid characters.')); + return; + } + + var filename = req.params.filename; + var contentType = req.get('Content-type'); + var config = req.config; + var filesController = config.filesController; + + filesController.createFile(config, filename, req.body, contentType).then(function (result) { + res.status(201); + res.set('Location', result.url); + res.json(result); + }).catch(function (e) { + _logger2.default.error(e.message, e); + next(new _node2.default.Error(_node2.default.Error.FILE_SAVE_ERROR, 'Could not store file.')); + }); + } + }, { + key: 'deleteHandler', + value: function deleteHandler(req, res, next) { + var filesController = req.config.filesController; + filesController.deleteFile(req.config, req.params.filename).then(function () { + res.status(200); + // TODO: return useful JSON here? + res.end(); + }).catch(function () { + next(new _node2.default.Error(_node2.default.Error.FILE_DELETE_ERROR, 'Could not delete file.')); + }); + } + }]); + + return FilesRouter; +}(); + +function isFileStreamable(req, filesController) { + if (req.get('Range')) { + if (!(typeof filesController.adapter.getFileStream === 'function')) { + return false; + } + if (typeof filesController.adapter.constructor.name !== 'undefined') { + if (filesController.adapter.constructor.name == 'GridStoreAdapter') { + return true; + } + } + } + return false; +} + +// handleFileStream is licenced under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/). +// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/). +function handleFileStream(stream, req, res, contentType) { + var buffer_size = 1024 * 1024; //1024Kb + // Range request, partiall stream the file + var parts = req.get('Range').replace(/bytes=/, "").split("-"); + var partialstart = parts[0]; + var partialend = parts[1]; + var start = partialstart ? parseInt(partialstart, 10) : 0; + var end = partialend ? parseInt(partialend, 10) : stream.length - 1; + var chunksize = end - start + 1; + + if (chunksize == 1) { + start = 0; + partialend = false; + } + + if (!partialend) { + if (stream.length - 1 - start < buffer_size) { + end = stream.length - 1; + } else { + end = start + buffer_size; + } + chunksize = end - start + 1; + } + + if (start == 0 && end == 2) { + chunksize = 1; + } + + res.writeHead(206, { + 'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length, + 'Accept-Ranges': 'bytes', + 'Content-Length': chunksize, + 'Content-Type': contentType + }); + + stream.seek(start, function () { + // get gridFile stream + var gridFileStream = stream.stream(true); + var bufferAvail = 0; + var range = end - start + 1; + var totalbyteswanted = end - start + 1; + var totalbyteswritten = 0; + // write to response + gridFileStream.on('data', function (buff) { + bufferAvail += buff.length; + //Ok check if we have enough to cover our range + if (bufferAvail < range) { + //Not enough bytes to satisfy our full range + if (bufferAvail > 0) { + //Write full buffer + res.write(buff); + totalbyteswritten += buff.length; + range -= buff.length; + bufferAvail -= buff.length; + } + } else { + //Enough bytes to satisfy our full range! + if (bufferAvail > 0) { + var buffer = buff.slice(0, range); + res.write(buffer); + totalbyteswritten += buffer.length; + bufferAvail -= range; + } + } + if (totalbyteswritten >= totalbyteswanted) { + //totalbytes = 0; + stream.close(); + res.end(); + this.destroy(); + } + }); + }); +} \ No newline at end of file diff --git a/lib/Routers/FunctionsRouter.js b/lib/Routers/FunctionsRouter.js new file mode 100644 index 0000000000..961c5c38fb --- /dev/null +++ b/lib/Routers/FunctionsRouter.js @@ -0,0 +1,205 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.FunctionsRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _middlewares = require('../middlewares'); + +var _StatusHandler = require('../StatusHandler'); + +var _lodash = require('lodash'); + +var _lodash2 = _interopRequireDefault(_lodash); + +var _logger = require('../logger'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +// FunctionsRouter.js + +var Parse = require('parse/node').Parse, + triggers = require('../triggers'); + +function parseObject(obj) { + if (Array.isArray(obj)) { + return obj.map(function (item) { + return parseObject(item); + }); + } else if (obj && obj.__type == 'Date') { + return Object.assign(new Date(obj.iso), obj); + } else if (obj && obj.__type == 'File') { + return Parse.File.fromJSON(obj); + } else if (obj && (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object') { + return parseParams(obj); + } else { + return obj; + } +} + +function parseParams(params) { + return _lodash2.default.mapValues(params, parseObject); +} + +var FunctionsRouter = exports.FunctionsRouter = function (_PromiseRouter) { + _inherits(FunctionsRouter, _PromiseRouter); + + function FunctionsRouter() { + _classCallCheck(this, FunctionsRouter); + + return _possibleConstructorReturn(this, (FunctionsRouter.__proto__ || Object.getPrototypeOf(FunctionsRouter)).apply(this, arguments)); + } + + _createClass(FunctionsRouter, [{ + key: 'mountRoutes', + value: function mountRoutes() { + this.route('POST', '/functions/:functionName', FunctionsRouter.handleCloudFunction); + this.route('POST', '/jobs/:jobName', _middlewares.promiseEnforceMasterKeyAccess, function (req) { + return FunctionsRouter.handleCloudJob(req); + }); + this.route('POST', '/jobs', _middlewares.promiseEnforceMasterKeyAccess, function (req) { + return FunctionsRouter.handleCloudJob(req); + }); + } + }], [{ + key: 'handleCloudJob', + value: function handleCloudJob(req) { + var jobName = req.params.jobName || req.body.jobName; + var applicationId = req.config.applicationId; + var jobHandler = (0, _StatusHandler.jobStatusHandler)(req.config); + var jobFunction = triggers.getJob(jobName, applicationId); + if (!jobFunction) { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid job.'); + } + var params = Object.assign({}, req.body, req.query); + params = parseParams(params); + var request = { + params: params, + log: req.config.loggerController, + headers: req.headers, + jobName: jobName + }; + var status = { + success: jobHandler.setSucceeded.bind(jobHandler), + error: jobHandler.setFailed.bind(jobHandler), + message: jobHandler.setMessage.bind(jobHandler) + }; + return jobHandler.setRunning(jobName, params).then(function (jobStatus) { + request.jobId = jobStatus.objectId; + // run the function async + process.nextTick(function () { + jobFunction(request, status); + }); + return { + headers: { + 'X-Parse-Job-Status-Id': jobStatus.objectId + }, + response: {} + }; + }); + } + }, { + key: 'createResponseObject', + value: function createResponseObject(resolve, reject, message) { + return { + success: function success(result) { + resolve({ + response: { + result: Parse._encode(result) + } + }); + }, + error: function error(code, message) { + if (!message) { + message = code; + code = Parse.Error.SCRIPT_FAILED; + } + reject(new Parse.Error(code, message)); + }, + message: message + }; + } + }, { + key: 'handleCloudFunction', + value: function handleCloudFunction(req) { + var functionName = req.params.functionName; + var applicationId = req.config.applicationId; + var theFunction = triggers.getFunction(functionName, applicationId); + var theValidator = triggers.getValidator(req.params.functionName, applicationId); + if (theFunction) { + var params = Object.assign({}, req.body, req.query); + params = parseParams(params); + var request = { + params: params, + master: req.auth && req.auth.isMaster, + user: req.auth && req.auth.user, + installationId: req.info.installationId, + log: req.config.loggerController, + headers: req.headers, + functionName: functionName + }; + + if (theValidator && typeof theValidator === "function") { + var result = theValidator(request); + if (!result) { + throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Validation failed.'); + } + } + + return new Promise(function (resolve, reject) { + var userString = req.auth && req.auth.user ? req.auth.user.id : undefined; + var cleanInput = _logger.logger.truncateLogMessage(JSON.stringify(params)); + var response = FunctionsRouter.createResponseObject(function (result) { + try { + var cleanResult = _logger.logger.truncateLogMessage(JSON.stringify(result.response.result)); + _logger.logger.info('Ran cloud function ' + functionName + ' for user ' + userString + ' ' + ('with:\n Input: ' + cleanInput + '\n Result: ' + cleanResult), { + functionName: functionName, + params: params, + user: userString + }); + resolve(result); + } catch (e) { + reject(e); + } + }, function (error) { + try { + _logger.logger.error('Failed running cloud function ' + functionName + ' for ' + ('user ' + userString + ' with:\n Input: ' + cleanInput + '\n Error: ') + JSON.stringify(error), { + functionName: functionName, + error: error, + params: params, + user: userString + }); + reject(error); + } catch (e) { + reject(e); + } + }); + // Force the keys before the function calls. + Parse.applicationId = req.config.applicationId; + Parse.javascriptKey = req.config.javascriptKey; + Parse.masterKey = req.config.masterKey; + theFunction(request, response); + }); + } else { + throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid function: "' + functionName + '"'); + } + } + }]); + + return FunctionsRouter; +}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/GlobalConfigRouter.js b/lib/Routers/GlobalConfigRouter.js new file mode 100644 index 0000000000..54ff2c3b8f --- /dev/null +++ b/lib/Routers/GlobalConfigRouter.js @@ -0,0 +1,79 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.GlobalConfigRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _middlewares = require('../middlewares'); + +var middleware = _interopRequireWildcard(_middlewares); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // global_config.js + +var GlobalConfigRouter = exports.GlobalConfigRouter = function (_PromiseRouter) { + _inherits(GlobalConfigRouter, _PromiseRouter); + + function GlobalConfigRouter() { + _classCallCheck(this, GlobalConfigRouter); + + return _possibleConstructorReturn(this, (GlobalConfigRouter.__proto__ || Object.getPrototypeOf(GlobalConfigRouter)).apply(this, arguments)); + } + + _createClass(GlobalConfigRouter, [{ + key: 'getGlobalConfig', + value: function getGlobalConfig(req) { + return req.config.database.find('_GlobalConfig', { objectId: "1" }, { limit: 1 }).then(function (results) { + if (results.length != 1) { + // If there is no config in the database - return empty config. + return { response: { params: {} } }; + } + var globalConfig = results[0]; + return { response: { params: globalConfig.params } }; + }); + } + }, { + key: 'updateGlobalConfig', + value: function updateGlobalConfig(req) { + var params = req.body.params; + // Transform in dot notation to make sure it works + var update = Object.keys(params).reduce(function (acc, key) { + acc['params.' + key] = params[key]; + return acc; + }, {}); + return req.config.database.update('_GlobalConfig', { objectId: "1" }, update, { upsert: true }).then(function () { + return { response: { result: true } }; + }); + } + }, { + key: 'mountRoutes', + value: function mountRoutes() { + var _this2 = this; + + this.route('GET', '/config', function (req) { + return _this2.getGlobalConfig(req); + }); + this.route('PUT', '/config', middleware.promiseEnforceMasterKeyAccess, function (req) { + return _this2.updateGlobalConfig(req); + }); + } + }]); + + return GlobalConfigRouter; +}(_PromiseRouter3.default); + +exports.default = GlobalConfigRouter; \ No newline at end of file diff --git a/lib/Routers/HooksRouter.js b/lib/Routers/HooksRouter.js new file mode 100644 index 0000000000..f196b5d73d --- /dev/null +++ b/lib/Routers/HooksRouter.js @@ -0,0 +1,155 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.HooksRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _node = require('parse/node'); + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _middlewares = require('../middlewares'); + +var middleware = _interopRequireWildcard(_middlewares); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var HooksRouter = exports.HooksRouter = function (_PromiseRouter) { + _inherits(HooksRouter, _PromiseRouter); + + function HooksRouter() { + _classCallCheck(this, HooksRouter); + + return _possibleConstructorReturn(this, (HooksRouter.__proto__ || Object.getPrototypeOf(HooksRouter)).apply(this, arguments)); + } + + _createClass(HooksRouter, [{ + key: 'createHook', + value: function createHook(aHook, config) { + return config.hooksController.createHook(aHook).then(function (hook) { + return { response: hook }; + }); + } + }, { + key: 'updateHook', + value: function updateHook(aHook, config) { + return config.hooksController.updateHook(aHook).then(function (hook) { + return { response: hook }; + }); + } + }, { + key: 'handlePost', + value: function handlePost(req) { + return this.createHook(req.body, req.config); + } + }, { + key: 'handleGetFunctions', + value: function handleGetFunctions(req) { + var hooksController = req.config.hooksController; + if (req.params.functionName) { + return hooksController.getFunction(req.params.functionName).then(function (foundFunction) { + if (!foundFunction) { + throw new _node.Parse.Error(143, 'no function named: ' + req.params.functionName + ' is defined'); + } + return Promise.resolve({ response: foundFunction }); + }); + } + + return hooksController.getFunctions().then(function (functions) { + return { response: functions || [] }; + }, function (err) { + throw err; + }); + } + }, { + key: 'handleGetTriggers', + value: function handleGetTriggers(req) { + var hooksController = req.config.hooksController; + if (req.params.className && req.params.triggerName) { + + return hooksController.getTrigger(req.params.className, req.params.triggerName).then(function (foundTrigger) { + if (!foundTrigger) { + throw new _node.Parse.Error(143, 'class ' + req.params.className + ' does not exist'); + } + return Promise.resolve({ response: foundTrigger }); + }); + } + + return hooksController.getTriggers().then(function (triggers) { + return { response: triggers || [] }; + }); + } + }, { + key: 'handleDelete', + value: function handleDelete(req) { + var hooksController = req.config.hooksController; + if (req.params.functionName) { + return hooksController.deleteFunction(req.params.functionName).then(function () { + return { response: {} }; + }); + } else if (req.params.className && req.params.triggerName) { + return hooksController.deleteTrigger(req.params.className, req.params.triggerName).then(function () { + return { response: {} }; + }); + } + return Promise.resolve({ response: {} }); + } + }, { + key: 'handleUpdate', + value: function handleUpdate(req) { + var hook; + if (req.params.functionName && req.body.url) { + hook = {}; + hook.functionName = req.params.functionName; + hook.url = req.body.url; + } else if (req.params.className && req.params.triggerName && req.body.url) { + hook = {}; + hook.className = req.params.className; + hook.triggerName = req.params.triggerName; + hook.url = req.body.url; + } else { + throw new _node.Parse.Error(143, "invalid hook declaration"); + } + return this.updateHook(hook, req.config); + } + }, { + key: 'handlePut', + value: function handlePut(req) { + var body = req.body; + if (body.__op == "Delete") { + return this.handleDelete(req); + } else { + return this.handleUpdate(req); + } + } + }, { + key: 'mountRoutes', + value: function 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)); + } + }]); + + return HooksRouter; +}(_PromiseRouter3.default); + +exports.default = HooksRouter; \ No newline at end of file diff --git a/lib/Routers/IAPValidationRouter.js b/lib/Routers/IAPValidationRouter.js new file mode 100644 index 0000000000..534cb7e7de --- /dev/null +++ b/lib/Routers/IAPValidationRouter.js @@ -0,0 +1,147 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.IAPValidationRouter = undefined; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _PromiseRouter2 = require("../PromiseRouter"); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _node = require("parse/node"); + +var _node2 = _interopRequireDefault(_node); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var request = require("request"); +var rest = require("../rest"); + + +// TODO move validation logic in IAPValidationController +var IAP_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"; +var IAP_PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt"; + +var 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." +}; + +function appStoreError(status) { + status = parseInt(status); + 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(); + } + // 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) { + var products = result.results; + if (!products || products.length != 1) { + // Error not found or too many + throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } + + var download = products[0].download; + return Promise.resolve({ response: download }); + }); +} + +var IAPValidationRouter = exports.IAPValidationRouter = function (_PromiseRouter) { + _inherits(IAPValidationRouter, _PromiseRouter); + + function IAPValidationRouter() { + _classCallCheck(this, IAPValidationRouter); + + return _possibleConstructorReturn(this, (IAPValidationRouter.__proto__ || Object.getPrototypeOf(IAPValidationRouter)).apply(this, arguments)); + } + + _createClass(IAPValidationRouter, [{ + key: "handleRequest", + value: function handleRequest(req) { + var receipt = req.body.receipt; + var productIdentifier = req.body.productIdentifier; + + if (!receipt || !productIdentifier) { + // TODO: Error, malformed request + throw new _node2.default.Error(_node2.default.Error.INVALID_JSON, "missing receipt or productIdentifier"); + } + + // Transform the object if there + // otherwise assume it's in Base64 already + if ((typeof receipt === "undefined" ? "undefined" : _typeof(receipt)) == "object") { + if (receipt["__type"] == "Bytes") { + receipt = receipt.base64; + } + } + + if (process.env.NODE_ENV == "test" && req.body.bypassAppStoreValidation) { + return getFileForProductIdentifier(productIdentifier, req); + } + + function successCallback() { + return getFileForProductIdentifier(productIdentifier, req); + } + + function errorCallback(error) { + return Promise.resolve({ response: appStoreError(error.status) }); + } + + return validateWithAppStore(IAP_PRODUCTION_URL, receipt).then(function () { + + return successCallback(); + }, function (error) { + if (error.status == 21007) { + return validateWithAppStore(IAP_SANDBOX_URL, receipt).then(function () { + return successCallback(); + }, function (error) { + return errorCallback(error); + }); + } + + return errorCallback(error); + }); + } + }, { + key: "mountRoutes", + value: function mountRoutes() { + this.route("POST", "/validate_purchase", this.handleRequest); + } + }]); + + return IAPValidationRouter; +}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/InstallationsRouter.js b/lib/Routers/InstallationsRouter.js new file mode 100644 index 0000000000..6898c47238 --- /dev/null +++ b/lib/Routers/InstallationsRouter.js @@ -0,0 +1,113 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.InstallationsRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _ClassesRouter2 = require('./ClassesRouter'); + +var _ClassesRouter3 = _interopRequireDefault(_ClassesRouter2); + +var _rest = require('../rest'); + +var _rest2 = _interopRequireDefault(_rest); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // InstallationsRouter.js + +var InstallationsRouter = exports.InstallationsRouter = function (_ClassesRouter) { + _inherits(InstallationsRouter, _ClassesRouter); + + function InstallationsRouter() { + _classCallCheck(this, InstallationsRouter); + + return _possibleConstructorReturn(this, (InstallationsRouter.__proto__ || Object.getPrototypeOf(InstallationsRouter)).apply(this, arguments)); + } + + _createClass(InstallationsRouter, [{ + key: 'handleFind', + value: function handleFind(req) { + var body = Object.assign(req.body, _ClassesRouter3.default.JSONFromQuery(req.query)); + var options = {}; + + if (body.skip) { + options.skip = Number(body.skip); + } + if (body.limit || body.limit === 0) { + options.limit = Number(body.limit); + } + if (body.order) { + options.order = String(body.order); + } + if (body.count) { + options.count = true; + } + if (body.include) { + options.include = String(body.include); + } + + return _rest2.default.find(req.config, req.auth, '_Installation', body.where, options, req.info.clientSDK).then(function (response) { + return { response: response }; + }); + } + }, { + key: 'handleGet', + value: function handleGet(req) { + req.params.className = '_Installation'; + return _get(InstallationsRouter.prototype.__proto__ || Object.getPrototypeOf(InstallationsRouter.prototype), 'handleGet', this).call(this, req); + } + }, { + key: 'handleCreate', + value: function handleCreate(req) { + req.params.className = '_Installation'; + return _get(InstallationsRouter.prototype.__proto__ || Object.getPrototypeOf(InstallationsRouter.prototype), 'handleCreate', this).call(this, req); + } + }, { + key: 'handleUpdate', + value: function handleUpdate(req) { + req.params.className = '_Installation'; + return _get(InstallationsRouter.prototype.__proto__ || Object.getPrototypeOf(InstallationsRouter.prototype), 'handleUpdate', this).call(this, req); + } + }, { + key: 'handleDelete', + value: function handleDelete(req) { + req.params.className = '_Installation'; + return _get(InstallationsRouter.prototype.__proto__ || Object.getPrototypeOf(InstallationsRouter.prototype), 'handleDelete', this).call(this, req); + } + }, { + key: 'mountRoutes', + value: function mountRoutes() { + var _this2 = this; + + this.route('GET', '/installations', function (req) { + return _this2.handleFind(req); + }); + this.route('GET', '/installations/:objectId', function (req) { + return _this2.handleGet(req); + }); + this.route('POST', '/installations', function (req) { + return _this2.handleCreate(req); + }); + this.route('PUT', '/installations/:objectId', function (req) { + return _this2.handleUpdate(req); + }); + this.route('DELETE', '/installations/:objectId', function (req) { + return _this2.handleDelete(req); + }); + } + }]); + + return InstallationsRouter; +}(_ClassesRouter3.default); + +exports.default = InstallationsRouter; \ No newline at end of file diff --git a/lib/Routers/LogsRouter.js b/lib/Routers/LogsRouter.js new file mode 100644 index 0000000000..e2378c1e81 --- /dev/null +++ b/lib/Routers/LogsRouter.js @@ -0,0 +1,96 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.LogsRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _node = require('parse/node'); + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _middlewares = require('../middlewares'); + +var middleware = _interopRequireWildcard(_middlewares); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var LogsRouter = exports.LogsRouter = function (_PromiseRouter) { + _inherits(LogsRouter, _PromiseRouter); + + function LogsRouter() { + _classCallCheck(this, LogsRouter); + + return _possibleConstructorReturn(this, (LogsRouter.__proto__ || Object.getPrototypeOf(LogsRouter)).apply(this, arguments)); + } + + _createClass(LogsRouter, [{ + key: 'mountRoutes', + value: function mountRoutes() { + var _this2 = this; + + this.route('GET', '/scriptlog', middleware.promiseEnforceMasterKeyAccess, this.validateRequest, function (req) { + return _this2.handleGET(req); + }); + } + }, { + key: 'validateRequest', + value: function validateRequest(req) { + if (!req.config || !req.config.loggerController) { + throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Logger adapter is not available'); + } + } + + // Returns a promise for a {response} object. + // query params: + // level (optional) Level of logging you want to query for (info || error) + // from (optional) Start time for the search. Defaults to 1 week ago. + // until (optional) End time for the search. Defaults to current time. + // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”. + // size (optional) Number of rows returned by search. Defaults to 10 + // n same as size, overrides size if set + + }, { + key: 'handleGET', + value: function handleGET(req) { + var from = req.query.from; + var until = req.query.until; + var size = req.query.size; + if (req.query.n) { + size = req.query.n; + } + + var order = req.query.order; + var level = req.query.level; + var options = { + from: from, + until: until, + size: size, + order: order, + level: level + }; + + return req.config.loggerController.getLogs(options).then(function (result) { + return Promise.resolve({ + response: result + }); + }); + } + }]); + + return LogsRouter; +}(_PromiseRouter3.default); + +exports.default = LogsRouter; \ No newline at end of file diff --git a/lib/Routers/PublicAPIRouter.js b/lib/Routers/PublicAPIRouter.js new file mode 100644 index 0000000000..9b62c7295b --- /dev/null +++ b/lib/Routers/PublicAPIRouter.js @@ -0,0 +1,285 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PublicAPIRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _Config = require('../Config'); + +var _Config2 = _interopRequireDefault(_Config); + +var _express = require('express'); + +var _express2 = _interopRequireDefault(_express); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +var _fs = require('fs'); + +var _fs2 = _interopRequireDefault(_fs); + +var _querystring = require('querystring'); + +var _querystring2 = _interopRequireDefault(_querystring); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var public_html = _path2.default.resolve(__dirname, "../../public_html"); +var views = _path2.default.resolve(__dirname, '../../views'); + +var PublicAPIRouter = exports.PublicAPIRouter = function (_PromiseRouter) { + _inherits(PublicAPIRouter, _PromiseRouter); + + function PublicAPIRouter() { + _classCallCheck(this, PublicAPIRouter); + + return _possibleConstructorReturn(this, (PublicAPIRouter.__proto__ || Object.getPrototypeOf(PublicAPIRouter)).apply(this, arguments)); + } + + _createClass(PublicAPIRouter, [{ + key: 'verifyEmail', + value: function verifyEmail(req) { + var _this2 = this; + + var _req$query = req.query, + token = _req$query.token, + username = _req$query.username; + + var appId = req.params.appId; + var config = new _Config2.default(appId); + + if (!config.publicServerURL) { + return this.missingPublicServerURL(); + } + + if (!token || !username) { + return this.invalidLink(req); + } + + var userController = config.userController; + return userController.verifyEmail(username, token).then(function () { + var params = _querystring2.default.stringify({ username: username }); + return Promise.resolve({ + status: 302, + location: config.verifyEmailSuccessURL + '?' + params + }); + }, function () { + return _this2.invalidVerificationLink(req); + }); + } + }, { + key: 'resendVerificationEmail', + value: function resendVerificationEmail(req) { + var username = req.body.username; + var appId = req.params.appId; + var config = new _Config2.default(appId); + + if (!config.publicServerURL) { + return this.missingPublicServerURL(); + } + + if (!username) { + return this.invalidLink(req); + } + + var userController = config.userController; + + return userController.resendVerificationEmail(username).then(function () { + return Promise.resolve({ + status: 302, + location: '' + config.linkSendSuccessURL + }); + }, function () { + return Promise.resolve({ + status: 302, + location: '' + config.linkSendFailURL + }); + }); + } + }, { + key: 'changePassword', + value: function changePassword(req) { + return new Promise(function (resolve, reject) { + var config = new _Config2.default(req.query.id); + if (!config.publicServerURL) { + return resolve({ + status: 404, + text: 'Not found.' + }); + } + // Should we keep the file in memory or leave like that? + _fs2.default.readFile(_path2.default.resolve(views, "choose_password"), 'utf-8', function (err, data) { + if (err) { + return reject(err); + } + data = data.replace("PARSE_SERVER_URL", '\'' + config.publicServerURL + '\''); + resolve({ + text: data + }); + }); + }); + } + }, { + key: 'requestResetPassword', + value: function requestResetPassword(req) { + var _this3 = this; + + var config = req.config; + + if (!config.publicServerURL) { + return this.missingPublicServerURL(); + } + + var _req$query2 = req.query, + username = _req$query2.username, + token = _req$query2.token; + + + if (!username || !token) { + return this.invalidLink(req); + } + + return config.userController.checkResetTokenValidity(username, token).then(function () { + var params = _querystring2.default.stringify({ token: token, id: config.applicationId, username: username, app: config.appName }); + return Promise.resolve({ + status: 302, + location: config.choosePasswordURL + '?' + params + }); + }, function () { + return _this3.invalidLink(req); + }); + } + }, { + key: 'resetPassword', + value: function resetPassword(req) { + + var config = req.config; + + if (!config.publicServerURL) { + return this.missingPublicServerURL(); + } + + var _req$body = req.body, + username = _req$body.username, + token = _req$body.token, + new_password = _req$body.new_password; + + + if (!username || !token || !new_password) { + return this.invalidLink(req); + } + + return config.userController.updatePassword(username, token, new_password).then(function () { + var params = _querystring2.default.stringify({ username: username }); + return Promise.resolve({ + status: 302, + location: config.passwordResetSuccessURL + '?' + params + }); + }, function (err) { + var params = _querystring2.default.stringify({ username: username, token: token, id: config.applicationId, error: err, app: config.appName }); + return Promise.resolve({ + status: 302, + location: config.choosePasswordURL + '?' + params + }); + }); + } + }, { + key: 'invalidLink', + value: function invalidLink(req) { + return Promise.resolve({ + status: 302, + location: req.config.invalidLinkURL + }); + } + }, { + key: 'invalidVerificationLink', + value: function invalidVerificationLink(req) { + var config = req.config; + if (req.query.username && req.params.appId) { + var params = _querystring2.default.stringify({ username: req.query.username, appId: req.params.appId }); + return Promise.resolve({ + status: 302, + location: config.invalidVerificationLinkURL + '?' + params + }); + } else { + return this.invalidLink(req); + } + } + }, { + key: 'missingPublicServerURL', + value: function missingPublicServerURL() { + return Promise.resolve({ + text: 'Not found.', + status: 404 + }); + } + }, { + key: 'setConfig', + value: function setConfig(req) { + req.config = new _Config2.default(req.params.appId); + return Promise.resolve(); + } + }, { + key: 'mountRoutes', + value: function mountRoutes() { + var _this4 = this; + + this.route('GET', '/apps/:appId/verify_email', function (req) { + _this4.setConfig(req); + }, function (req) { + return _this4.verifyEmail(req); + }); + + this.route('POST', '/apps/:appId/resend_verification_email', function (req) { + _this4.setConfig(req); + }, function (req) { + return _this4.resendVerificationEmail(req); + }); + + this.route('GET', '/apps/choose_password', function (req) { + return _this4.changePassword(req); + }); + + this.route('POST', '/apps/:appId/request_password_reset', function (req) { + _this4.setConfig(req); + }, function (req) { + return _this4.resetPassword(req); + }); + + this.route('GET', '/apps/:appId/request_password_reset', function (req) { + _this4.setConfig(req); + }, function (req) { + return _this4.requestResetPassword(req); + }); + } + }, { + key: 'expressRouter', + value: function expressRouter() { + var router = _express2.default.Router(); + router.use("/apps", _express2.default.static(public_html)); + router.use("/", _get(PublicAPIRouter.prototype.__proto__ || Object.getPrototypeOf(PublicAPIRouter.prototype), 'expressRouter', this).call(this)); + return router; + } + }]); + + return PublicAPIRouter; +}(_PromiseRouter3.default); + +exports.default = PublicAPIRouter; \ No newline at end of file diff --git a/lib/Routers/PurgeRouter.js b/lib/Routers/PurgeRouter.js new file mode 100644 index 0000000000..1d7b853821 --- /dev/null +++ b/lib/Routers/PurgeRouter.js @@ -0,0 +1,64 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PurgeRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _middlewares = require('../middlewares'); + +var middleware = _interopRequireWildcard(_middlewares); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var PurgeRouter = exports.PurgeRouter = function (_PromiseRouter) { + _inherits(PurgeRouter, _PromiseRouter); + + function PurgeRouter() { + _classCallCheck(this, PurgeRouter); + + return _possibleConstructorReturn(this, (PurgeRouter.__proto__ || Object.getPrototypeOf(PurgeRouter)).apply(this, arguments)); + } + + _createClass(PurgeRouter, [{ + key: 'handlePurge', + value: function handlePurge(req) { + return req.config.database.purgeCollection(req.params.className).then(function () { + var cacheAdapter = req.config.cacheController; + if (req.params.className == '_Session') { + cacheAdapter.user.clear(); + } else if (req.params.className == '_Role') { + cacheAdapter.role.clear(); + } + return { response: {} }; + }); + } + }, { + key: 'mountRoutes', + value: function mountRoutes() { + var _this2 = this; + + this.route('DELETE', '/purge/:className', middleware.promiseEnforceMasterKeyAccess, function (req) { + return _this2.handlePurge(req); + }); + } + }]); + + return PurgeRouter; +}(_PromiseRouter3.default); + +exports.default = PurgeRouter; \ No newline at end of file diff --git a/lib/Routers/PushRouter.js b/lib/Routers/PushRouter.js new file mode 100644 index 0000000000..e6d37e706b --- /dev/null +++ b/lib/Routers/PushRouter.js @@ -0,0 +1,104 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.PushRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _PromiseRouter2 = require("../PromiseRouter"); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _middlewares = require("../middlewares"); + +var middleware = _interopRequireWildcard(_middlewares); + +var _node = require("parse/node"); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var PushRouter = exports.PushRouter = function (_PromiseRouter) { + _inherits(PushRouter, _PromiseRouter); + + function PushRouter() { + _classCallCheck(this, PushRouter); + + return _possibleConstructorReturn(this, (PushRouter.__proto__ || Object.getPrototypeOf(PushRouter)).apply(this, arguments)); + } + + _createClass(PushRouter, [{ + key: "mountRoutes", + value: function mountRoutes() { + this.route("POST", "/push", middleware.promiseEnforceMasterKeyAccess, PushRouter.handlePOST); + } + }], [{ + key: "handlePOST", + value: function handlePOST(req) { + var pushController = req.config.pushController; + if (!pushController) { + throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Push controller is not set'); + } + + var where = PushRouter.getQueryCondition(req); + var resolve = void 0; + var promise = new Promise(function (_resolve) { + resolve = _resolve; + }); + pushController.sendPush(req.body, where, req.config, req.auth, function (pushStatusId) { + resolve({ + headers: { + 'X-Parse-Push-Status-Id': pushStatusId + }, + response: { + result: true + } + }); + }); + return promise; + } + + /** + * Get query condition from the request body. + * @param {Object} req A request object + * @returns {Object} The query condition, the where field in a query api call + */ + + }, { + key: "getQueryCondition", + value: function getQueryCondition(req) { + var body = req.body || {}; + var hasWhere = typeof body.where !== 'undefined'; + var hasChannels = typeof body.channels !== 'undefined'; + + var where = void 0; + if (hasWhere && hasChannels) { + throw new _node.Parse.Error(_node.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 + } + }; + } else { + throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Sending a push requires either "channels" or a "where" query.'); + } + return where; + } + }]); + + return PushRouter; +}(_PromiseRouter3.default); + +exports.default = PushRouter; \ No newline at end of file diff --git a/lib/Routers/RolesRouter.js b/lib/Routers/RolesRouter.js new file mode 100644 index 0000000000..b9f6742511 --- /dev/null +++ b/lib/Routers/RolesRouter.js @@ -0,0 +1,89 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.RolesRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _ClassesRouter2 = require('./ClassesRouter'); + +var _ClassesRouter3 = _interopRequireDefault(_ClassesRouter2); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var RolesRouter = exports.RolesRouter = function (_ClassesRouter) { + _inherits(RolesRouter, _ClassesRouter); + + function RolesRouter() { + _classCallCheck(this, RolesRouter); + + return _possibleConstructorReturn(this, (RolesRouter.__proto__ || Object.getPrototypeOf(RolesRouter)).apply(this, arguments)); + } + + _createClass(RolesRouter, [{ + key: 'handleFind', + value: function handleFind(req) { + req.params.className = '_Role'; + return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleFind', this).call(this, req); + } + }, { + key: 'handleGet', + value: function handleGet(req) { + req.params.className = '_Role'; + return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleGet', this).call(this, req); + } + }, { + key: 'handleCreate', + value: function handleCreate(req) { + req.params.className = '_Role'; + return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleCreate', this).call(this, req); + } + }, { + key: 'handleUpdate', + value: function handleUpdate(req) { + req.params.className = '_Role'; + return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleUpdate', this).call(this, req); + } + }, { + key: 'handleDelete', + value: function handleDelete(req) { + req.params.className = '_Role'; + return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleDelete', this).call(this, req); + } + }, { + key: 'mountRoutes', + value: function mountRoutes() { + var _this2 = this; + + this.route('GET', '/roles', function (req) { + return _this2.handleFind(req); + }); + this.route('GET', '/roles/:objectId', function (req) { + return _this2.handleGet(req); + }); + this.route('POST', '/roles', function (req) { + return _this2.handleCreate(req); + }); + this.route('PUT', '/roles/:objectId', function (req) { + return _this2.handleUpdate(req); + }); + this.route('DELETE', '/roles/:objectId', function (req) { + return _this2.handleDelete(req); + }); + } + }]); + + return RolesRouter; +}(_ClassesRouter3.default); + +exports.default = RolesRouter; \ No newline at end of file diff --git a/lib/Routers/SchemasRouter.js b/lib/Routers/SchemasRouter.js new file mode 100644 index 0000000000..5a57202c24 --- /dev/null +++ b/lib/Routers/SchemasRouter.js @@ -0,0 +1,125 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.SchemasRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _PromiseRouter2 = require('../PromiseRouter'); + +var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); + +var _middlewares = require('../middlewares'); + +var middleware = _interopRequireWildcard(_middlewares); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +// schemas.js + +var Parse = require('parse/node').Parse, + SchemaController = require('../Controllers/SchemaController'); + +function classNameMismatchResponse(bodyClass, pathClass) { + throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class name mismatch between ' + bodyClass + ' and ' + pathClass + '.'); +} + +function getAllSchemas(req) { + return req.config.database.loadSchema({ clearCache: true }).then(function (schemaController) { + return schemaController.getAllClasses(true); + }).then(function (schemas) { + return { response: { results: schemas } }; + }); +} + +function getOneSchema(req) { + var className = req.params.className; + return req.config.database.loadSchema({ clearCache: true }).then(function (schemaController) { + return schemaController.getOneSchema(className, true); + }).then(function (schema) { + return { response: schema }; + }).catch(function (error) { + if (error === undefined) { + 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.'); + } + }); +} + +function createSchema(req) { + if (req.params.className && req.body.className) { + if (req.params.className != req.body.className) { + return classNameMismatchResponse(req.body.className, req.params.className); + } + } + + var className = req.params.className || req.body.className; + if (!className) { + throw new Parse.Error(135, 'POST ' + req.path + ' needs a class name.'); + } + + return req.config.database.loadSchema({ clearCache: true }).then(function (schema) { + return schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions); + }).then(function (schema) { + return { response: schema }; + }); +} + +function modifySchema(req) { + if (req.body.className && req.body.className != req.params.className) { + return classNameMismatchResponse(req.body.className, req.params.className); + } + + var submittedFields = req.body.fields || {}; + var className = req.params.className; + + return req.config.database.loadSchema({ clearCache: true }).then(function (schema) { + return schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.config.database); + }).then(function (result) { + return { response: result }; + }); +} + +var deleteSchema = function deleteSchema(req) { + if (!SchemaController.classNameIsValid(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).then(function () { + return { response: {} }; + }); +}; + +var SchemasRouter = exports.SchemasRouter = function (_PromiseRouter) { + _inherits(SchemasRouter, _PromiseRouter); + + function SchemasRouter() { + _classCallCheck(this, SchemasRouter); + + return _possibleConstructorReturn(this, (SchemasRouter.__proto__ || Object.getPrototypeOf(SchemasRouter)).apply(this, arguments)); + } + + _createClass(SchemasRouter, [{ + key: 'mountRoutes', + value: function 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); + } + }]); + + return SchemasRouter; +}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/SessionsRouter.js b/lib/Routers/SessionsRouter.js new file mode 100644 index 0000000000..817b8e88cf --- /dev/null +++ b/lib/Routers/SessionsRouter.js @@ -0,0 +1,167 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.SessionsRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _ClassesRouter2 = require('./ClassesRouter'); + +var _ClassesRouter3 = _interopRequireDefault(_ClassesRouter2); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _rest = require('../rest'); + +var _rest2 = _interopRequireDefault(_rest); + +var _Auth = require('../Auth'); + +var _Auth2 = _interopRequireDefault(_Auth); + +var _RestWrite = require('../RestWrite'); + +var _RestWrite2 = _interopRequireDefault(_RestWrite); + +var _cryptoUtils = require('../cryptoUtils'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } + +var SessionsRouter = exports.SessionsRouter = function (_ClassesRouter) { + _inherits(SessionsRouter, _ClassesRouter); + + function SessionsRouter() { + _classCallCheck(this, SessionsRouter); + + return _possibleConstructorReturn(this, (SessionsRouter.__proto__ || Object.getPrototypeOf(SessionsRouter)).apply(this, arguments)); + } + + _createClass(SessionsRouter, [{ + key: 'handleFind', + value: function handleFind(req) { + req.params.className = '_Session'; + return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleFind', this).call(this, req); + } + }, { + key: 'handleGet', + value: function handleGet(req) { + req.params.className = '_Session'; + return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleGet', this).call(this, req); + } + }, { + key: 'handleCreate', + value: function handleCreate(req) { + req.params.className = '_Session'; + return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleCreate', this).call(this, req); + } + }, { + key: 'handleUpdate', + value: function handleUpdate(req) { + req.params.className = '_Session'; + return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleUpdate', this).call(this, req); + } + }, { + key: 'handleDelete', + value: function handleDelete(req) { + req.params.className = '_Session'; + return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleDelete', this).call(this, req); + } + }, { + key: 'handleMe', + value: function handleMe(req) { + // TODO: Verify correct behavior + if (!req.info || !req.info.sessionToken) { + throw new _node2.default.Error(_node2.default.Error.INVALID_SESSION_TOKEN, 'Session token required.'); + } + return _rest2.default.find(req.config, _Auth2.default.master(req.config), '_Session', { sessionToken: req.info.sessionToken }, undefined, req.info.clientSDK).then(function (response) { + if (!response.results || response.results.length == 0) { + throw new _node2.default.Error(_node2.default.Error.INVALID_SESSION_TOKEN, 'Session token not found.'); + } + return { + response: response.results[0] + }; + }); + } + }, { + key: 'handleUpdateToRevocableSession', + value: function handleUpdateToRevocableSession(req) { + var config = req.config; + var masterAuth = _Auth2.default.master(config); + var user = req.auth.user; + // Issue #2720 + // Calling without a session token would result in a not found user + if (!user) { + throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'invalid session'); + } + var expiresAt = config.generateSessionExpiresAt(); + var sessionData = { + sessionToken: 'r:' + (0, _cryptoUtils.newToken)(), + user: { + __type: 'Pointer', + className: '_User', + objectId: user.id + }, + createdWith: { + 'action': 'upgrade' + }, + restricted: false, + installationId: req.auth.installationId, + expiresAt: _node2.default._encode(expiresAt) + }; + var create = new _RestWrite2.default(config, masterAuth, '_Session', null, sessionData); + return create.execute().then(function () { + // delete the session token, use the db to skip beforeSave + return config.database.update('_User', { + objectId: user.id + }, { + sessionToken: { __op: 'Delete' } + }); + }).then(function () { + return Promise.resolve({ response: sessionData }); + }); + } + }, { + key: 'mountRoutes', + value: function mountRoutes() { + var _this2 = this; + + this.route('GET', '/sessions/me', function (req) { + return _this2.handleMe(req); + }); + this.route('GET', '/sessions', function (req) { + return _this2.handleFind(req); + }); + this.route('GET', '/sessions/:objectId', function (req) { + return _this2.handleGet(req); + }); + this.route('POST', '/sessions', function (req) { + return _this2.handleCreate(req); + }); + this.route('PUT', '/sessions/:objectId', function (req) { + return _this2.handleUpdate(req); + }); + this.route('DELETE', '/sessions/:objectId', function (req) { + return _this2.handleDelete(req); + }); + this.route('POST', '/upgradeToRevocableSession', function (req) { + return _this2.handleUpdateToRevocableSession(req); + }); + } + }]); + + return SessionsRouter; +}(_ClassesRouter3.default); + +exports.default = SessionsRouter; \ No newline at end of file diff --git a/lib/Routers/UsersRouter.js b/lib/Routers/UsersRouter.js new file mode 100644 index 0000000000..8f95101d22 --- /dev/null +++ b/lib/Routers/UsersRouter.js @@ -0,0 +1,367 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.UsersRouter = undefined; + +var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + +var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; + +var _deepcopy = require('deepcopy'); + +var _deepcopy2 = _interopRequireDefault(_deepcopy); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _Config = require('../Config'); + +var _Config2 = _interopRequireDefault(_Config); + +var _AccountLockout = require('../AccountLockout'); + +var _AccountLockout2 = _interopRequireDefault(_AccountLockout); + +var _ClassesRouter2 = require('./ClassesRouter'); + +var _ClassesRouter3 = _interopRequireDefault(_ClassesRouter2); + +var _rest = require('../rest'); + +var _rest2 = _interopRequireDefault(_rest); + +var _Auth = require('../Auth'); + +var _Auth2 = _interopRequireDefault(_Auth); + +var _password = require('../password'); + +var _password2 = _interopRequireDefault(_password); + +var _RestWrite = require('../RestWrite'); + +var _RestWrite2 = _interopRequireDefault(_RestWrite); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } + +function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // These methods handle the User-related routes. + +var cryptoUtils = require('../cryptoUtils'); + +var UsersRouter = exports.UsersRouter = function (_ClassesRouter) { + _inherits(UsersRouter, _ClassesRouter); + + function UsersRouter() { + _classCallCheck(this, UsersRouter); + + return _possibleConstructorReturn(this, (UsersRouter.__proto__ || Object.getPrototypeOf(UsersRouter)).apply(this, arguments)); + } + + _createClass(UsersRouter, [{ + key: 'handleFind', + value: function handleFind(req) { + req.params.className = '_User'; + return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleFind', this).call(this, req); + } + }, { + key: 'handleGet', + value: function handleGet(req) { + req.params.className = '_User'; + return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleGet', this).call(this, req); + } + }, { + key: 'handleCreate', + value: function handleCreate(req) { + var data = (0, _deepcopy2.default)(req.body); + req.body = data; + req.params.className = '_User'; + + return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleCreate', this).call(this, req); + } + }, { + key: 'handleUpdate', + value: function handleUpdate(req) { + req.params.className = '_User'; + return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleUpdate', this).call(this, req); + } + }, { + key: 'handleDelete', + value: function handleDelete(req) { + req.params.className = '_User'; + return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleDelete', this).call(this, req); + } + }, { + key: 'handleMe', + value: function handleMe(req) { + if (!req.info || !req.info.sessionToken) { + throw new _node2.default.Error(_node2.default.Error.INVALID_SESSION_TOKEN, 'invalid session token'); + } + var sessionToken = req.info.sessionToken; + return _rest2.default.find(req.config, _Auth2.default.master(req.config), '_Session', { sessionToken: sessionToken }, { include: 'user' }, req.info.clientSDK).then(function (response) { + if (!response.results || response.results.length == 0 || !response.results[0].user) { + throw new _node2.default.Error(_node2.default.Error.INVALID_SESSION_TOKEN, 'invalid session token'); + } else { + var user = response.results[0].user; + // Send token back on the login, because SDKs expect that. + user.sessionToken = sessionToken; + + // Remove hidden properties. + for (var key in user) { + if (user.hasOwnProperty(key)) { + // Regexp comes from Parse.Object.prototype.validate + if (key !== "__type" && !/^[A-Za-z][0-9A-Za-z_]*$/.test(key)) { + delete user[key]; + } + } + } + + return { response: user }; + } + }); + } + }, { + key: 'handleLogIn', + value: function handleLogIn(req) { + // Use query parameters instead if provided in url + if (!req.body.username && req.query.username) { + req.body = req.query; + } + + // TODO: use the right error codes / descriptions. + if (!req.body.username) { + throw new _node2.default.Error(_node2.default.Error.USERNAME_MISSING, 'username is required.'); + } + if (!req.body.password) { + throw new _node2.default.Error(_node2.default.Error.PASSWORD_MISSING, 'password is required.'); + } + if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string') { + throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); + } + + var user = void 0; + var isValidPassword = false; + + return req.config.database.find('_User', { username: req.body.username }).then(function (results) { + if (!results.length) { + throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); + } + user = results[0]; + + if (req.config.verifyUserEmails && req.config.preventLoginWithUnverifiedEmail && !user.emailVerified) { + throw new _node2.default.Error(_node2.default.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); + } + return _password2.default.compare(req.body.password, user.password); + }).then(function (correct) { + isValidPassword = correct; + var accountLockoutPolicy = new _AccountLockout2.default(user, req.config); + return accountLockoutPolicy.handleLoginAttempt(isValidPassword); + }).then(function () { + if (!isValidPassword) { + throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); + } + + // handle password expiry policy + if (req.config.passwordPolicy && req.config.passwordPolicy.maxPasswordAge) { + var 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: _node2.default._encode(changedAt) }); + } else { + // check whether the password has expired + if (changedAt.__type == 'Date') { + changedAt = new Date(changedAt.iso); + } + // Calculate the expiry time. + var _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 _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Your password has expired. Please reset your password.'); + } + } + + var token = 'r:' + cryptoUtils.newToken(); + user.sessionToken = token; + delete user.password; + + // Sometimes the authData still has null on that keys + // https://github.com/ParsePlatform/parse-server/issues/935 + if (user.authData) { + Object.keys(user.authData).forEach(function (provider) { + if (user.authData[provider] === null) { + delete user.authData[provider]; + } + }); + if (Object.keys(user.authData).length == 0) { + delete user.authData; + } + } + + req.config.filesController.expandFilesInObject(req.config, user); + + var expiresAt = req.config.generateSessionExpiresAt(); + var sessionData = { + sessionToken: token, + user: { + __type: 'Pointer', + className: '_User', + objectId: user.objectId + }, + createdWith: { + 'action': 'login', + 'authProvider': 'password' + }, + restricted: false, + expiresAt: _node2.default._encode(expiresAt) + }; + + if (req.info.installationId) { + sessionData.installationId = req.info.installationId; + } + + var create = new _RestWrite2.default(req.config, _Auth2.default.master(req.config), '_Session', null, sessionData); + return create.execute(); + }).then(function () { + return { response: user }; + }); + } + }, { + key: 'handleLogOut', + value: function handleLogOut(req) { + var success = { response: {} }; + if (req.info && req.info.sessionToken) { + return _rest2.default.find(req.config, _Auth2.default.master(req.config), '_Session', { sessionToken: req.info.sessionToken }, undefined, req.info.clientSDK).then(function (records) { + if (records.results && records.results.length) { + return _rest2.default.del(req.config, _Auth2.default.master(req.config), '_Session', records.results[0].objectId).then(function () { + return Promise.resolve(success); + }); + } + return Promise.resolve(success); + }); + } + return Promise.resolve(success); + } + }, { + key: '_throwOnBadEmailConfig', + value: function _throwOnBadEmailConfig(req) { + try { + _Config2.default.validateEmailConfiguration({ + emailAdapter: req.config.userController.adapter, + appName: req.config.appName, + publicServerURL: req.config.publicServerURL, + 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 _node2.default.Error(_node2.default.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.'); + } else { + throw e; + } + } + } + }, { + key: 'handleResetRequest', + value: function handleResetRequest(req) { + this._throwOnBadEmailConfig(req); + + var email = req.body.email; + + if (!email) { + throw new _node2.default.Error(_node2.default.Error.EMAIL_MISSING, "you must provide an email"); + } + if (typeof email !== 'string') { + throw new _node2.default.Error(_node2.default.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string'); + } + var userController = req.config.userController; + return userController.sendPasswordResetEmail(email).then(function () { + return Promise.resolve({ + response: {} + }); + }, function (err) { + if (err.code === _node2.default.Error.OBJECT_NOT_FOUND) { + throw new _node2.default.Error(_node2.default.Error.EMAIL_NOT_FOUND, 'No user found with email ' + email + '.'); + } else { + throw err; + } + }); + } + }, { + key: 'handleVerificationEmailRequest', + value: function handleVerificationEmailRequest(req) { + this._throwOnBadEmailConfig(req); + + var email = req.body.email; + + if (!email) { + throw new _node2.default.Error(_node2.default.Error.EMAIL_MISSING, 'you must provide an email'); + } + if (typeof email !== 'string') { + throw new _node2.default.Error(_node2.default.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string'); + } + + return req.config.database.find('_User', { email: email }).then(function (results) { + if (!results.length || results.length < 1) { + throw new _node2.default.Error(_node2.default.Error.EMAIL_NOT_FOUND, 'No user found with email ' + email); + } + var user = results[0]; + + if (user.emailVerified) { + throw new _node2.default.Error(_node2.default.Error.OTHER_CAUSE, 'Email ' + email + ' is already verified.'); + } + + var userController = req.config.userController; + userController.sendVerificationEmail(user); + return { response: {} }; + }); + } + }, { + key: 'mountRoutes', + value: function mountRoutes() { + var _this2 = this; + + this.route('GET', '/users', function (req) { + return _this2.handleFind(req); + }); + this.route('POST', '/users', function (req) { + return _this2.handleCreate(req); + }); + this.route('GET', '/users/me', function (req) { + return _this2.handleMe(req); + }); + this.route('GET', '/users/:objectId', function (req) { + return _this2.handleGet(req); + }); + this.route('PUT', '/users/:objectId', function (req) { + return _this2.handleUpdate(req); + }); + this.route('DELETE', '/users/:objectId', function (req) { + return _this2.handleDelete(req); + }); + this.route('GET', '/login', function (req) { + return _this2.handleLogIn(req); + }); + this.route('POST', '/logout', function (req) { + return _this2.handleLogOut(req); + }); + this.route('POST', '/requestPasswordReset', function (req) { + return _this2.handleResetRequest(req); + }); + this.route('POST', '/verificationEmailRequest', function (req) { + return _this2.handleVerificationEmailRequest(req); + }); + } + }]); + + return UsersRouter; +}(_ClassesRouter3.default); + +exports.default = UsersRouter; \ No newline at end of file diff --git a/lib/StatusHandler.js b/lib/StatusHandler.js new file mode 100644 index 0000000000..6512e7da1b --- /dev/null +++ b/lib/StatusHandler.js @@ -0,0 +1,263 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +exports.flatten = flatten; +exports.jobStatusHandler = jobStatusHandler; +exports.pushStatusHandler = pushStatusHandler; + +var _cryptoUtils = require('./cryptoUtils'); + +var _logger = require('./logger'); + +var PUSH_STATUS_COLLECTION = '_PushStatus'; +var JOB_STATUS_COLLECTION = '_JobStatus'; + +var incrementOp = function incrementOp() { + var object = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var key = arguments[1]; + var amount = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; + + if (!object[key]) { + object[key] = { __op: 'Increment', amount: amount }; + } else { + object[key].amount += amount; + } + return object[key]; +}; + +function flatten(array) { + var flattened = []; + for (var i = 0; i < array.length; i++) { + if (Array.isArray(array[i])) { + flattened = flattened.concat(flatten(array[i])); + } else { + flattened.push(array[i]); + } + } + return flattened; +} + +function statusHandler(className, database) { + var lastPromise = Promise.resolve(); + + function create(object) { + lastPromise = lastPromise.then(function () { + return database.create(className, object).then(function () { + return Promise.resolve(object); + }); + }); + return lastPromise; + } + + function update(where, object) { + lastPromise = lastPromise.then(function () { + return database.update(className, where, object); + }); + return lastPromise; + } + + return Object.freeze({ + create: create, + update: update + }); +} + +function jobStatusHandler(config) { + var jobStatus = void 0; + var objectId = (0, _cryptoUtils.newObjectId)(); + var database = config.database; + var handler = statusHandler(JOB_STATUS_COLLECTION, database); + var setRunning = function setRunning(jobName, params) { + var now = new Date(); + jobStatus = { + objectId: objectId, + jobName: jobName, + params: params, + status: 'running', + source: 'api', + createdAt: now, + // lockdown! + ACL: {} + }; + + return handler.create(jobStatus); + }; + + var setMessage = function setMessage(message) { + if (!message || typeof message !== 'string') { + return Promise.resolve(); + } + return handler.update({ objectId: objectId }, { message: message }); + }; + + var setSucceeded = function setSucceeded(message) { + return setFinalStatus('succeeded', message); + }; + + var setFailed = function setFailed(message) { + return setFinalStatus('failed', message); + }; + + var setFinalStatus = function setFinalStatus(status) { + var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; + + var finishedAt = new Date(); + var update = { status: status, finishedAt: finishedAt }; + if (message && typeof message === 'string') { + update.message = message; + } + return handler.update({ objectId: objectId }, update); + }; + + return Object.freeze({ + setRunning: setRunning, + setSucceeded: setSucceeded, + setMessage: setMessage, + setFailed: setFailed + }); +} + +function pushStatusHandler(config) { + var objectId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : (0, _cryptoUtils.newObjectId)(); + + + var pushStatus = void 0; + var database = config.database; + var handler = statusHandler(PUSH_STATUS_COLLECTION, database); + var setInitial = function setInitial() { + var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + var where = arguments[1]; + var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : { source: 'rest' }; + + var now = new Date(); + var pushTime = new Date(); + var status = 'pending'; + if (body.hasOwnProperty('push_time')) { + if (config.hasPushScheduledSupport) { + pushTime = body.push_time; + status = 'scheduled'; + } else { + _logger.logger.warn('Trying to schedule a push while server is not configured.'); + _logger.logger.warn('Push will be sent immediately'); + } + } + + var data = body.data || {}; + var payloadString = JSON.stringify(data); + var pushHash = void 0; + if (typeof data.alert === 'string') { + pushHash = (0, _cryptoUtils.md5Hash)(data.alert); + } else if (_typeof(data.alert) === 'object') { + pushHash = (0, _cryptoUtils.md5Hash)(JSON.stringify(data.alert)); + } else { + pushHash = 'd41d8cd98f00b204e9800998ecf8427e'; + } + var object = { + objectId: objectId, + createdAt: now, + pushTime: pushTime.toISOString(), + query: JSON.stringify(where), + payload: payloadString, + source: options.source, + title: options.title, + expiry: body.expiration_time, + status: status, + numSent: 0, + pushHash: pushHash, + // lockdown! + ACL: {} + }; + + return handler.create(object).then(function () { + pushStatus = { + objectId: objectId + }; + return Promise.resolve(pushStatus); + }); + }; + + var setRunning = function setRunning(count) { + _logger.logger.verbose('_PushStatus ' + objectId + ': sending push to %d installations', count); + return handler.update({ status: "pending", objectId: objectId }, { status: "running", updatedAt: new Date(), count: count }); + }; + + var trackSent = function trackSent(results) { + var _this = this; + + var update = { + updatedAt: new Date(), + numSent: 0, + numFailed: 0 + }; + if (Array.isArray(results)) { + results = flatten(results); + results.reduce(function (memo, result) { + // Cannot handle that + if (!result || !result.device || !result.device.deviceType) { + return memo; + } + var deviceType = result.device.deviceType; + var key = result.transmitted ? 'sentPerType.' + deviceType : 'failedPerType.' + deviceType; + memo[key] = incrementOp(memo, key); + if (result.transmitted) { + memo.numSent++; + } else { + memo.numFailed++; + } + return memo; + }, update); + incrementOp(update, 'count', -results.length); + } + + _logger.logger.verbose('_PushStatus ' + objectId + ': sent push! %d success, %d failures', update.numSent, update.numFailed); + + ['numSent', 'numFailed'].forEach(function (key) { + if (update[key] > 0) { + update[key] = { + __op: 'Increment', + amount: update[key] + }; + } else { + delete update[key]; + } + }); + + return handler.update({ objectId: objectId }, update).then(function (res) { + if (res && res.count === 0) { + return _this.complete(); + } + }); + }; + + var complete = function complete() { + return handler.update({ objectId: objectId }, { + status: 'succeeded', + count: { __op: 'Delete' }, + updatedAt: new Date() + }); + }; + + var fail = function fail(err) { + var update = { + errorMessage: JSON.stringify(err), + status: 'failed', + updatedAt: new Date() + }; + _logger.logger.warn('_PushStatus ' + objectId + ': error while sending push', err); + return handler.update({ objectId: objectId }, update); + }; + + return Object.freeze({ + objectId: objectId, + setInitial: setInitial, + setRunning: setRunning, + trackSent: trackSent, + complete: complete, + fail: fail + }); +} \ No newline at end of file diff --git a/lib/TestUtils.js b/lib/TestUtils.js new file mode 100644 index 0000000000..4741bd088d --- /dev/null +++ b/lib/TestUtils.js @@ -0,0 +1,27 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.destroyAllDataPermanently = destroyAllDataPermanently; + +var _cache = require('./cache'); + +var _cache2 = _interopRequireDefault(_cache); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +//Used by tests +function destroyAllDataPermanently() { + if (!process.env.TESTING) { + throw 'Only supported in test environment'; + } + return Promise.all(Object.keys(_cache2.default.cache).map(function (appId) { + var app = _cache2.default.get(appId); + if (app.databaseController) { + return app.databaseController.deleteEverything(); + } else { + return Promise.resolve(); + } + })); +} \ No newline at end of file diff --git a/lib/batch.js b/lib/batch.js new file mode 100644 index 0000000000..2774575d07 --- /dev/null +++ b/lib/batch.js @@ -0,0 +1,98 @@ +'use strict'; + +var Parse = require('parse/node').Parse; +var url = require('url'); +var path = require('path'); +// These methods handle batch requests. +var batchPath = '/batch'; + +// Mounts a batch-handler onto a PromiseRouter. +function mountOnto(router) { + router.route('POST', batchPath, function (req) { + return handleBatch(router, req); + }); +} + +function parseURL(URL) { + if (typeof URL === 'string') { + return url.parse(URL); + } + return undefined; +} + +function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) { + serverURL = serverURL ? parseURL(serverURL) : undefined; + publicServerURL = publicServerURL ? parseURL(publicServerURL) : undefined; + + var apiPrefixLength = originalUrl.length - batchPath.length; + var apiPrefix = originalUrl.slice(0, apiPrefixLength); + + var makeRoutablePath = function makeRoutablePath(requestPath) { + // The routablePath is the path minus the api prefix + if (requestPath.slice(0, apiPrefix.length) != apiPrefix) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot route batch path ' + requestPath); + } + return path.posix.join('/', requestPath.slice(apiPrefix.length)); + }; + + if (serverURL && publicServerURL && serverURL.path != publicServerURL.path) { + var localPath = serverURL.path; + var publicPath = publicServerURL.path; + // Override the api prefix + apiPrefix = localPath; + return function (requestPath) { + // Build the new path by removing the public path + // and joining with the local path + var newPath = path.posix.join('/', localPath, '/', requestPath.slice(publicPath.length)); + // Use the method for local routing + return makeRoutablePath(newPath); + }; + } + + return makeRoutablePath; +} + +// Returns a promise for a {response} object. +// 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'); + } + + // The batch paths are all from the root of our domain. + // That means they include the API prefix, that the API is mounted + // to. However, our promise router does not route the api prefix. So + // we need to figure out the API prefix, so that we can strip it + // from all the subrequests. + if (!req.originalUrl.endsWith(batchPath)) { + throw 'internal routing problem - expected url to end with batch'; + } + + var makeRoutablePath = makeBatchRoutingPathFunction(req.originalUrl, req.config.serverURL, req.config.publicServerURL); + + var promises = req.body.requests.map(function (restRequest) { + var routablePath = makeRoutablePath(restRequest.path); + // Construct a request that we can send to a handler + var request = { + body: restRequest.body, + config: req.config, + auth: req.auth, + info: req.info + }; + + return router.tryRouteRequest(restRequest.method, routablePath, request).then(function (response) { + return { success: response.response }; + }, function (error) { + return { error: { code: error.code, error: error.message } }; + }); + }); + + return Promise.all(promises).then(function (results) { + return { response: results }; + }); +} + +module.exports = { + mountOnto: mountOnto, + makeBatchRoutingPathFunction: makeBatchRoutingPathFunction +}; \ No newline at end of file diff --git a/lib/cache.js b/lib/cache.js new file mode 100644 index 0000000000..51641150c3 --- /dev/null +++ b/lib/cache.js @@ -0,0 +1,11 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.AppCache = undefined; + +var _InMemoryCache = require('./Adapters/Cache/InMemoryCache'); + +var AppCache = exports.AppCache = new _InMemoryCache.InMemoryCache({ ttl: NaN }); +exports.default = AppCache; \ No newline at end of file diff --git a/lib/cli/definitions/parse-live-query-server.js b/lib/cli/definitions/parse-live-query-server.js new file mode 100644 index 0000000000..637fa9fd3f --- /dev/null +++ b/lib/cli/definitions/parse-live-query-server.js @@ -0,0 +1,45 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _parsers = require("../utils/parsers"); + +exports.default = { + "appId": { + required: true, + help: "Required. 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": { + required: true, + help: "Required. 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": { + required: true, + help: "Required. 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." + }, + "redisURL": { + help: "Optional. 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." + }, + "keyPairs": { + help: "Optional. 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." + }, + "websocketTimeout": { + help: "Optional. 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: (0, _parsers.numberParser)("websocketTimeout") + }, + "cacheTimeout": { + help: "Optional. 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: (0, _parsers.numberParser)("cacheTimeout") + }, + "logLevel": { + help: "Optional. This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE. Defaults to INFO." + }, + "port": { + env: "PORT", + help: "The port to run the ParseServer. defaults to 1337.", + default: 1337, + action: (0, _parsers.numberParser)("port") + } +}; \ No newline at end of file diff --git a/lib/cli/definitions/parse-server.js b/lib/cli/definitions/parse-server.js new file mode 100644 index 0000000000..b79a961cf7 --- /dev/null +++ b/lib/cli/definitions/parse-server.js @@ -0,0 +1,257 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _parsers = require("../utils/parsers"); + +exports.default = { + "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 + }, + "port": { + env: "PORT", + help: "The port to run the ParseServer. defaults to 1337.", + default: 1337, + action: (0, _parsers.numberParser)("port") + }, + "host": { + env: "PARSE_SERVER_HOST", + help: "The host to serve ParseServer on. defaults to 0.0.0.0", + default: '0.0.0.0' + }, + "databaseURI": { + env: "PARSE_SERVER_DATABASE_URI", + help: "The full URI to your mongodb database" + }, + "databaseOptions": { + env: "PARSE_SERVER_DATABASE_OPTIONS", + help: "Options to pass to the mongodb client", + action: _parsers.objectParser + }, + "collectionPrefix": { + env: "PARSE_SERVER_COLLECTION_PREFIX", + help: 'A collection prefix for the classes' + }, + "serverURL": { + env: "PARSE_SERVER_URL", + help: "URL to your parse server with http:// or https://." + }, + "publicServerURL": { + env: "PARSE_PUBLIC_SERVER_URL", + help: "Public URL to your parse server with http:// or https://." + }, + "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" + }, + "restAPIKey": { + env: "PARSE_SERVER_REST_API_KEY", + help: "Key for REST calls" + }, + "dotNetKey": { + env: "PARSE_SERVER_DOT_NET_KEY", + help: "Key for Unity and .Net SDK" + }, + "webhookKey": { + env: "PARSE_SERVER_WEBHOOK_KEY", + help: "Key sent with outgoing webhook calls" + }, + "cloud": { + env: "PARSE_SERVER_CLOUD_CODE_MAIN", + help: "Full path to your cloud code main.js" + }, + "push": { + env: "PARSE_SERVER_PUSH", + help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push", + action: _parsers.objectParser + }, + "scheduledPush": { + env: "PARSE_SERVER_SCHEDULED_PUSH", + help: "Configuration for push scheduling. Defaults to false.", + action: _parsers.booleanParser + }, + "oauth": { + env: "PARSE_SERVER_OAUTH_PROVIDERS", + help: "[DEPRECATED (use auth option)] Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth", + action: _parsers.objectParser + }, + "auth": { + env: "PARSE_SERVER_AUTH_PROVIDERS", + help: "Configuration for your authentication providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth", + action: _parsers.objectParser + }, + "fileKey": { + env: "PARSE_SERVER_FILE_KEY", + help: "Key for your files" + }, + "facebookAppIds": { + env: "PARSE_SERVER_FACEBOOK_APP_IDS", + help: "[DEPRECATED (use auth option)]", + action: function action() { + throw 'facebookAppIds is deprecated, please use { auth: \ + {facebook: \ + { appIds: [] } \ + }\ + }\ + }'; + } + }, + "enableAnonymousUsers": { + env: "PARSE_SERVER_ENABLE_ANON_USERS", + help: "Enable (or disable) anon users, defaults to true", + action: _parsers.booleanParser + }, + "allowClientClassCreation": { + env: "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION", + help: "Enable (or disable) client class creation, defaults to true", + action: _parsers.booleanParser + }, + "mountPath": { + env: "PARSE_SERVER_MOUNT_PATH", + help: "Mount path for the server, defaults to /parse", + default: "/parse" + }, + "filesAdapter": { + env: "PARSE_SERVER_FILES_ADAPTER", + help: "Adapter module for the files sub-system", + action: _parsers.moduleOrObjectParser + }, + "emailAdapter": { + env: "PARSE_SERVER_EMAIL_ADAPTER", + help: "Adapter module for the email sending", + action: _parsers.moduleOrObjectParser + }, + "verifyUserEmails": { + env: "PARSE_SERVER_VERIFY_USER_EMAILS", + help: "Enable (or disable) user email validation, defaults to false", + action: _parsers.booleanParser + }, + "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 + }, + "emailVerifyTokenValidityDuration": { + env: "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION", + help: "Email verification token validity duration", + action: (0, _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 + }, + "appName": { + env: "PARSE_SERVER_APP_NAME", + help: "Sets the app name" + }, + "loggerAdapter": { + env: "PARSE_SERVER_LOGGER_ADAPTER", + help: "Adapter module for the logging sub-system", + action: _parsers.moduleOrObjectParser + }, + "customPages": { + env: "PARSE_SERVER_CUSTOM_PAGES", + help: "custom pages for password validation and reset", + action: _parsers.objectParser + }, + "maxUploadSize": { + env: "PARSE_SERVER_MAX_UPLOAD_SIZE", + help: "Max file size for uploads.", + default: "20mb" + }, + "userSensitiveFields": { + help: "Personally identifiable information fields in the user table the should be removed for non-authorized users.", + default: ["email"] + }, + "sessionLength": { + env: "PARSE_SERVER_SESSION_LENGTH", + help: "Session duration, defaults to 1 year", + action: (0, _parsers.numberParser)("sessionLength") + }, + "verbose": { + env: "VERBOSE", + help: "Set the logging to verbose" + }, + "jsonLogs": { + env: "JSON_LOGS", + help: "Log as structured JSON objects" + }, + "logLevel": { + env: "PARSE_SERVER_LOG_LEVEL", + help: "Sets the level for logs" + }, + "logsFolder": { + env: "PARSE_SERVER_LOGS_FOLDER", + help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging", + action: _parsers.nullParser + }, + "silent": { + help: "Disables console output" + }, + "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 + }, + "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 0; disabled.", + action: (0, _parsers.numberParser)("schemaCacheTTL") + }, + "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 + }, + "cluster": { + env: "PARSE_SERVER_CLUSTER", + help: "Run with cluster, optionally set the number of processes default to os.cpus().length", + action: (0, _parsers.numberOrBoolParser)("cluster") + }, + "liveQuery": { + env: "PARSE_SERVER_LIVE_QUERY_OPTIONS", + help: "parse-server's LiveQuery configuration object", + action: _parsers.objectParser + }, + "liveQuery.classNames": { + help: "parse-server's LiveQuery classNames", + action: _parsers.arrayParser + }, + "liveQuery.redisURL": { + help: "parse-server's LiveQuery redisURL" + }, + "startLiveQueryServer": { + help: "Starts the liveQuery server", + action: _parsers.booleanParser + }, + "liveQueryPort": { + help: 'Specific port to start the live query server', + action: (0, _parsers.numberParser)("liveQueryPort") + }, + "liveQueryServerOptions": { + help: "Live query server configuration options (will start the liveQuery server)", + action: _parsers.objectParser + }, + "middleware": { + help: "middleware for express server, can be string or function" + } +}; \ No newline at end of file diff --git a/lib/cli/parse-live-query-server.js b/lib/cli/parse-live-query-server.js new file mode 100644 index 0000000000..dbe2e52a8c --- /dev/null +++ b/lib/cli/parse-live-query-server.js @@ -0,0 +1,28 @@ +'use strict'; + +var _parseLiveQueryServer = require('./definitions/parse-live-query-server'); + +var _parseLiveQueryServer2 = _interopRequireDefault(_parseLiveQueryServer); + +var _runner = require('./utils/runner'); + +var _runner2 = _interopRequireDefault(_runner); + +var _index = require('../index'); + +var _express = require('express'); + +var _express2 = _interopRequireDefault(_express); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +(0, _runner2.default)({ + definitions: _parseLiveQueryServer2.default, + start: function start(program, options, logOptions) { + logOptions(); + var app = (0, _express2.default)(); + var httpServer = require('http').createServer(app); + httpServer.listen(options.port); + _index.ParseServer.createLiveQueryServer(httpServer, options); + } +}); \ No newline at end of file diff --git a/lib/cli/parse-server.js b/lib/cli/parse-server.js new file mode 100755 index 0000000000..ec671e97fc --- /dev/null +++ b/lib/cli/parse-server.js @@ -0,0 +1,169 @@ +'use strict'; + +var _express = require('express'); + +var _express2 = _interopRequireDefault(_express); + +var _index = require('../index'); + +var _index2 = _interopRequireDefault(_index); + +var _parseServer = require('./definitions/parse-server'); + +var _parseServer2 = _interopRequireDefault(_parseServer); + +var _cluster = require('cluster'); + +var _cluster2 = _interopRequireDefault(_cluster); + +var _os = require('os'); + +var _os2 = _interopRequireDefault(_os); + +var _runner = require('./utils/runner'); + +var _runner2 = _interopRequireDefault(_runner); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +/* eslint-disable no-console */ +var path = require("path"); + +var help = function help() { + console.log(' Get Started guide:'); + console.log(''); + console.log(' Please have a look at the get started guide!'); + console.log(' https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide'); + console.log(''); + console.log(''); + 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(''); + 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(''); +}; + +function startServer(options, callback) { + var app = (0, _express2.default)(); + if (options.middleware) { + var middleware = void 0; + if (typeof options.middleware == 'function') { + middleware = options.middleware; + }if (typeof options.middleware == 'string') { + middleware = require(path.resolve(process.cwd(), options.middleware)); + } else { + throw "middleware should be a string or a function"; + } + app.use(middleware); + } + + var parseServer = new _index2.default(options); + var sockets = {}; + app.use(options.mountPath, parseServer.app); + + var server = app.listen(options.port, options.host, callback); + server.on('connection', initializeConnections); + + if (options.startLiveQueryServer || options.liveQueryServerOptions) { + var liveQueryServer = server; + if (options.liveQueryPort) { + liveQueryServer = (0, _express2.default)().listen(options.liveQueryPort, function () { + console.log('ParseLiveQuery listening on ' + options.liveQueryPort); + }); + } + _index2.default.createLiveQueryServer(liveQueryServer, options.liveQueryServerOptions); + } + + function initializeConnections(socket) { + /* 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. */ + + var socketId = socket.remoteAddress + ':' + socket.remotePort; + sockets[socketId] = socket; + + socket.on('close', function () { + delete sockets[socketId]; + }); + } + + function destroyAliveConnections() { + for (var socketId in sockets) { + try { + sockets[socketId].destroy(); + } catch (e) {/* */} + } + } + + var handleShutdown = function handleShutdown() { + console.log('Termination signal received. Shutting down.'); + destroyAliveConnections(); + server.close(); + parseServer.handleShutdown(); + }; + process.on('SIGTERM', handleShutdown); + process.on('SIGINT', handleShutdown); +} + +(0, _runner2.default)({ + definitions: _parseServer2.default, + help: help, + usage: '[options] ', + start: function start(program, options, logOptions) { + if (!options.serverURL) { + options.serverURL = 'http://localhost:' + options.port + options.mountPath; + } + + if (!options.appId || !options.masterKey || !options.serverURL) { + program.outputHelp(); + console.error(""); + console.error('\x1B[31mERROR: appId and masterKey are required\x1B[0m'); + console.error(""); + process.exit(1); + } + + if (options["liveQuery.classNames"]) { + options.liveQuery = options.liveQuery || {}; + options.liveQuery.classNames = options["liveQuery.classNames"]; + delete options["liveQuery.classNames"]; + } + if (options["liveQuery.redisURL"]) { + options.liveQuery = options.liveQuery || {}; + options.liveQuery.redisURL = options["liveQuery.redisURL"]; + delete options["liveQuery.redisURL"]; + } + + if (options.cluster) { + var numCPUs = typeof options.cluster === 'number' ? options.cluster : _os2.default.cpus().length; + if (_cluster2.default.isMaster) { + logOptions(); + for (var i = 0; i < numCPUs; i++) { + _cluster2.default.fork(); + } + _cluster2.default.on('exit', function (worker, code) { + console.log('worker ' + worker.process.pid + ' died (' + code + ')... Restarting'); + _cluster2.default.fork(); + }); + } else { + startServer(options, function () { + console.log('[' + process.pid + '] parse-server running on ' + options.serverURL); + }); + } + } else { + startServer(options, function () { + logOptions(); + console.log(''); + console.log('[' + process.pid + '] parse-server running on ' + options.serverURL); + }); + } + } +}); + +/* eslint-enable no-console */ \ No newline at end of file diff --git a/lib/cli/utils/commander.js b/lib/cli/utils/commander.js new file mode 100644 index 0000000000..daded29687 --- /dev/null +++ b/lib/cli/utils/commander.js @@ -0,0 +1,150 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /* eslint-disable no-console */ + + +var _commander = require('commander'); + +var _path = require('path'); + +var _path2 = _interopRequireDefault(_path); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var _definitions = void 0; +var _reverseDefinitions = void 0; +var _defaults = void 0; + +_commander.Command.prototype.loadDefinitions = function (definitions) { + _definitions = definitions; + + Object.keys(definitions).reduce(function (program, opt) { + if (_typeof(definitions[opt]) == "object") { + var additionalOptions = definitions[opt]; + if (additionalOptions.required === true) { + return program.option('--' + opt + ' <' + opt + '>', additionalOptions.help, additionalOptions.action); + } else { + return program.option('--' + opt + ' [' + opt + ']', additionalOptions.help, additionalOptions.action); + } + } + return program.option('--' + opt + ' [' + opt + ']'); + }, this); + + _defaults = Object.keys(definitions).reduce(function (defs, opt) { + if (_definitions[opt].default) { + defs[opt] = _definitions[opt].default; + } + return defs; + }, {}); + + _reverseDefinitions = Object.keys(definitions).reduce(function (object, key) { + var value = definitions[key]; + if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) == "object") { + value = value.env; + } + if (value) { + object[value] = key; + } + return object; + }, {}); + + /* istanbul ignore next */ + this.on('--help', function () { + console.log(' Configure From Environment:'); + console.log(''); + Object.keys(_reverseDefinitions).forEach(function (key) { + console.log(' $ ' + key + '=\'' + _reverseDefinitions[key] + '\''); + }); + console.log(''); + }); +}; + +function parseEnvironment() { + var env = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; + + return Object.keys(_reverseDefinitions).reduce(function (options, key) { + if (env[key]) { + var originalKey = _reverseDefinitions[key]; + var action = function action(option) { + return option; + }; + if (_typeof(_definitions[originalKey]) === "object") { + action = _definitions[originalKey].action || action; + } + options[_reverseDefinitions[key]] = action(env[key]); + } + return options; + }, {}); +} + +function parseConfigFile(program) { + var options = {}; + if (program.args.length > 0) { + var jsonPath = program.args[0]; + jsonPath = _path2.default.resolve(jsonPath); + var jsonConfig = require(jsonPath); + if (jsonConfig.apps) { + if (jsonConfig.apps.length > 1) { + throw 'Multiple apps are not supported'; + } + options = jsonConfig.apps[0]; + } else { + options = jsonConfig; + } + Object.keys(options).forEach(function (key) { + var value = options[key]; + if (!_definitions[key]) { + throw 'error: unknown option ' + key; + } + var action = _definitions[key].action; + if (action) { + options[key] = action(value); + } + }); + console.log('Configuration loaded from ' + jsonPath); + } + return options; +} + +_commander.Command.prototype.setValuesIfNeeded = function (options) { + var _this = this; + + Object.keys(options).forEach(function (key) { + if (!_this.hasOwnProperty(key)) { + _this[key] = options[key]; + } + }); +}; + +_commander.Command.prototype._parse = _commander.Command.prototype.parse; + +_commander.Command.prototype.parse = function (args, env) { + this._parse(args); + // Parse the environment first + var envOptions = parseEnvironment(env); + var fromFile = parseConfigFile(this); + // Load the env if not passed from command line + this.setValuesIfNeeded(envOptions); + // Load from file to override + this.setValuesIfNeeded(fromFile); + // Last set the defaults + this.setValuesIfNeeded(_defaults); +}; + +_commander.Command.prototype.getOptions = function () { + var _this2 = this; + + return Object.keys(_definitions).reduce(function (options, key) { + if (typeof _this2[key] !== 'undefined') { + options[key] = _this2[key]; + } + return options; + }, {}); +}; + +exports.default = new _commander.Command(); +/* eslint-enable no-console */ \ No newline at end of file diff --git a/lib/cli/utils/parsers.js b/lib/cli/utils/parsers.js new file mode 100644 index 0000000000..fabcec314e --- /dev/null +++ b/lib/cli/utils/parsers.js @@ -0,0 +1,80 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +exports.numberParser = numberParser; +exports.numberOrBoolParser = numberOrBoolParser; +exports.objectParser = objectParser; +exports.arrayParser = arrayParser; +exports.moduleOrObjectParser = moduleOrObjectParser; +exports.booleanParser = booleanParser; +exports.nullParser = nullParser; +function numberParser(key) { + return function (opt) { + var intOpt = parseInt(opt); + if (!Number.isInteger(intOpt)) { + throw new Error('Key ' + key + ' has invalid value ' + opt); + } + return intOpt; + }; +} + +function numberOrBoolParser(key) { + return function (opt) { + if (typeof opt === 'boolean') { + return opt; + } + if (opt === 'true') { + return true; + } + if (opt === 'false') { + return false; + } + return numberParser(key)(opt); + }; +} + +function objectParser(opt) { + if ((typeof opt === 'undefined' ? 'undefined' : _typeof(opt)) == 'object') { + return opt; + } + return JSON.parse(opt); +} + +function arrayParser(opt) { + if (Array.isArray(opt)) { + return opt; + } else if (typeof opt === 'string') { + return opt.split(','); + } else { + throw new Error(opt + ' should be a comma separated string or an array'); + } +} + +function moduleOrObjectParser(opt) { + if ((typeof opt === 'undefined' ? 'undefined' : _typeof(opt)) == 'object') { + return opt; + } + try { + return JSON.parse(opt); + } catch (e) {/* */} + return opt; +} + +function booleanParser(opt) { + if (opt == true || opt == 'true' || opt == '1') { + return true; + } + return false; +} + +function nullParser(opt) { + if (opt == 'null') { + return null; + } + return opt; +} \ No newline at end of file diff --git a/lib/cli/utils/runner.js b/lib/cli/utils/runner.js new file mode 100644 index 0000000000..f4371a242e --- /dev/null +++ b/lib/cli/utils/runner.js @@ -0,0 +1,49 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +exports.default = function (_ref) { + var definitions = _ref.definitions, + help = _ref.help, + usage = _ref.usage, + start = _ref.start; + + _commander2.default.loadDefinitions(definitions); + if (usage) { + _commander2.default.usage(usage); + } + if (help) { + _commander2.default.on('--help', help); + } + _commander2.default.parse(process.argv, process.env); + + var options = _commander2.default.getOptions(); + start(_commander2.default, options, function () { + logStartupOptions(options); + }); +}; + +var _commander = require("./commander"); + +var _commander2 = _interopRequireDefault(_commander); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function logStartupOptions(options) { + for (var key in options) { + var value = options[key]; + if (key == "masterKey") { + value = "***REDACTED***"; + } + if ((typeof value === "undefined" ? "undefined" : _typeof(value)) === 'object') { + value = JSON.stringify(value); + } + /* eslint-disable no-console */ + console.log(key + ": " + value); + /* eslint-enable no-console */ + } +} \ No newline at end of file diff --git a/lib/cloud-code/HTTPResponse.js b/lib/cloud-code/HTTPResponse.js new file mode 100644 index 0000000000..7eb5b66d1d --- /dev/null +++ b/lib/cloud-code/HTTPResponse.js @@ -0,0 +1,65 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + +var HTTPResponse = function HTTPResponse(response, body) { + var _this = this; + + _classCallCheck(this, HTTPResponse); + + var _text = void 0, + _data = void 0; + this.status = response.statusCode; + this.headers = response.headers || {}; + this.cookies = this.headers["set-cookie"]; + + if (typeof body == 'string') { + _text = body; + } else if (Buffer.isBuffer(body)) { + this.buffer = body; + } else if ((typeof body === 'undefined' ? 'undefined' : _typeof(body)) == 'object') { + _data = body; + } + + var getText = function getText() { + if (!_text && _this.buffer) { + _text = _this.buffer.toString('utf-8'); + } else if (!_text && _data) { + _text = JSON.stringify(_data); + } + return _text; + }; + + var getData = function getData() { + if (!_data) { + try { + _data = JSON.parse(getText()); + } catch (e) {/* */} + } + return _data; + }; + + Object.defineProperty(this, 'body', { + get: function get() { + return body; + } + }); + + Object.defineProperty(this, 'text', { + enumerable: true, + get: getText + }); + + Object.defineProperty(this, 'data', { + enumerable: true, + get: getData + }); +}; + +exports.default = HTTPResponse; \ No newline at end of file diff --git a/lib/cloud-code/Parse.Cloud.js b/lib/cloud-code/Parse.Cloud.js new file mode 100644 index 0000000000..7aa74f4b9f --- /dev/null +++ b/lib/cloud-code/Parse.Cloud.js @@ -0,0 +1,76 @@ +'use strict'; + +var _node = require('parse/node'); + +var _triggers = require('../triggers'); + +var triggers = _interopRequireWildcard(_triggers); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function validateClassNameForTriggers(className) { + var restrictedClassNames = ['_Session']; + if (restrictedClassNames.indexOf(className) != -1) { + throw 'Triggers are not supported for ' + className + ' class.'; + } + return className; +} + +function getClassName(parseClass) { + if (parseClass && parseClass.className) { + return validateClassNameForTriggers(parseClass.className); + } + return validateClassNameForTriggers(parseClass); +} + +var ParseCloud = {}; +ParseCloud.define = function (functionName, handler, validationHandler) { + triggers.addFunction(functionName, handler, validationHandler, _node.Parse.applicationId); +}; + +ParseCloud.job = function (functionName, handler) { + triggers.addJob(functionName, handler, _node.Parse.applicationId); +}; + +ParseCloud.beforeSave = function (parseClass, handler) { + var className = getClassName(parseClass); + triggers.addTrigger(triggers.Types.beforeSave, className, handler, _node.Parse.applicationId); +}; + +ParseCloud.beforeDelete = function (parseClass, handler) { + var className = getClassName(parseClass); + triggers.addTrigger(triggers.Types.beforeDelete, className, handler, _node.Parse.applicationId); +}; + +ParseCloud.afterSave = function (parseClass, handler) { + var className = getClassName(parseClass); + triggers.addTrigger(triggers.Types.afterSave, className, handler, _node.Parse.applicationId); +}; + +ParseCloud.afterDelete = function (parseClass, handler) { + var className = getClassName(parseClass); + triggers.addTrigger(triggers.Types.afterDelete, className, handler, _node.Parse.applicationId); +}; + +ParseCloud.beforeFind = function (parseClass, handler) { + var className = getClassName(parseClass); + triggers.addTrigger(triggers.Types.beforeFind, className, handler, _node.Parse.applicationId); +}; + +ParseCloud.afterFind = function (parseClass, handler) { + var className = getClassName(parseClass); + triggers.addTrigger(triggers.Types.afterFind, className, handler, _node.Parse.applicationId); +}; + +ParseCloud._removeAllHooks = function () { + triggers._unregisterAll(); +}; + +ParseCloud.useMasterKey = function () { + // 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: https://github.com/ParsePlatform/parse-server/wiki/Compatibility-with-Hosted-Parse#cloud-code"); +}; + +ParseCloud.httpRequest = require("./httpRequest"); + +module.exports = ParseCloud; \ No newline at end of file diff --git a/lib/cloud-code/httpRequest.js b/lib/cloud-code/httpRequest.js new file mode 100644 index 0000000000..869ebec3e1 --- /dev/null +++ b/lib/cloud-code/httpRequest.js @@ -0,0 +1,107 @@ +'use strict'; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _request = require('request'); + +var _request2 = _interopRequireDefault(_request); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _HTTPResponse = require('./HTTPResponse'); + +var _HTTPResponse2 = _interopRequireDefault(_HTTPResponse); + +var _querystring = require('querystring'); + +var _querystring2 = _interopRequireDefault(_querystring); + +var _logger = require('../logger'); + +var _logger2 = _interopRequireDefault(_logger); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var encodeBody = function encodeBody(_ref) { + var body = _ref.body, + _ref$headers = _ref.headers, + headers = _ref$headers === undefined ? {} : _ref$headers; + + if ((typeof body === 'undefined' ? 'undefined' : _typeof(body)) !== 'object') { + return { body: body, headers: headers }; + } + var contentTypeKeys = Object.keys(headers).filter(function (key) { + return key.match(/content-type/i) != null; + }); + + if (contentTypeKeys.length == 0) { + // no content type + // As per https://parse.com/docs/cloudcode/guide#cloud-code-advanced-sending-a-post-request the default encoding is supposedly x-www-form-urlencoded + + body = _querystring2.default.stringify(body); + headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } else { + /* istanbul ignore next */ + if (contentTypeKeys.length > 1) { + _logger2.default.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)) { + body = _querystring2.default.stringify(body); + } + } + return { body: body, headers: headers }; +}; + +module.exports = function (options) { + var promise = new _node2.default.Promise(); + var callbacks = { + success: options.success, + error: options.error + }; + delete options.success; + delete options.error; + delete options.uri; // not supported + options = Object.assign(options, encodeBody(options)); + // set follow redirects to false by default + options.followRedirect = options.followRedirects == true; + // support params options + if (_typeof(options.params) === 'object') { + options.qs = options.params; + } else if (typeof options.params === 'string') { + options.qs = _querystring2.default.parse(options.params); + } + // force the response as a buffer + options.encoding = null; + + (0, _request2.default)(options, function (error, response, body) { + if (error) { + if (callbacks.error) { + callbacks.error(error); + } + return promise.reject(error); + } + var httpResponse = new _HTTPResponse2.default(response, body); + + // Consider <200 && >= 400 as errors + if (httpResponse.status < 200 || httpResponse.status >= 400) { + if (callbacks.error) { + callbacks.error(httpResponse); + } + return promise.reject(httpResponse); + } else { + if (callbacks.success) { + callbacks.success(httpResponse); + } + return promise.resolve(httpResponse); + } + }); + return promise; +}; + +module.exports.encodeBody = encodeBody; \ No newline at end of file diff --git a/lib/cryptoUtils.js b/lib/cryptoUtils.js new file mode 100644 index 0000000000..ae36817935 --- /dev/null +++ b/lib/cryptoUtils.js @@ -0,0 +1,59 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.randomHexString = randomHexString; +exports.randomString = randomString; +exports.newObjectId = newObjectId; +exports.newToken = newToken; +exports.md5Hash = md5Hash; + +var _crypto = require('crypto'); + +// Returns a new random hex string of the given even size. +function randomHexString(size) { + if (size === 0) { + throw new Error('Zero-length randomHexString is useless.'); + } + if (size % 2 !== 0) { + throw new Error('randomHexString size must be divisible by 2.'); + } + return (0, _crypto.randomBytes)(size / 2).toString('hex'); +} + +// Returns a new random alphanumeric string of the given size. +// +// Note: to simplify implementation, the result has slight modulo bias, +// because chars length of 62 doesn't divide the number of all bytes +// (256) evenly. Such bias is acceptable for most cases when the output +// length is long enough and doesn't need to be uniform. + + +function randomString(size) { + if (size === 0) { + throw new Error('Zero-length randomString is useless.'); + } + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789'; + var objectId = ''; + var bytes = (0, _crypto.randomBytes)(size); + for (var i = 0; i < bytes.length; ++i) { + objectId += chars[bytes.readUInt8(i) % chars.length]; + } + return objectId; +} + +// Returns a new random alphanumeric string suitable for object ID. +function newObjectId() { + //TODO: increase length to better protect against collisions. + return randomString(10); +} + +// Returns a new random hex string suitable for secure tokens. +function newToken() { + return randomHexString(32); +} + +function md5Hash(string) { + return (0, _crypto.createHash)('md5').update(string).digest('hex'); +} \ No newline at end of file diff --git a/lib/defaults.js b/lib/defaults.js new file mode 100644 index 0000000000..148f6cf533 --- /dev/null +++ b/lib/defaults.js @@ -0,0 +1,44 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +var _parsers = require('./cli/utils/parsers'); + +var logsFolder = function () { + var folder = './logs/'; + if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { + folder = './test_logs/'; + } + if (process.env.PARSE_SERVER_LOGS_FOLDER) { + folder = (0, _parsers.nullParser)(process.env.PARSE_SERVER_LOGS_FOLDER); + } + return folder; +}(); + +var _ref = function () { + var verbose = process.env.VERBOSE ? true : false; + return { verbose: verbose, level: verbose ? 'verbose' : undefined }; +}(), + verbose = _ref.verbose, + level = _ref.level; + +exports.default = { + DefaultMongoURI: 'mongodb://localhost:27017/parse', + jsonLogs: process.env.JSON_LOGS || false, + logsFolder: logsFolder, + verbose: verbose, + level: level, + silent: false, + enableAnonymousUsers: true, + allowClientClassCreation: true, + maxUploadSize: '20mb', + verifyUserEmails: false, + preventLoginWithUnverifiedEmail: false, + sessionLength: 31536000, + expireInactiveSessions: true, + revokeSessionOnPasswordReset: true, + schemaCacheTTL: 5000, // in ms + userSensitiveFields: ['email'] +}; \ No newline at end of file diff --git a/lib/deprecated.js b/lib/deprecated.js new file mode 100644 index 0000000000..3371f7cc51 --- /dev/null +++ b/lib/deprecated.js @@ -0,0 +1,11 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.useExternal = useExternal; +function useExternal(name, moduleName) { + return function () { + throw name + " is not provided by parse-server anymore; please install " + moduleName; + }; +} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000000..f9fc68faa0 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,69 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.ParseServer = exports.PushWorker = exports.TestUtils = exports.RedisCacheAdapter = exports.NullCacheAdapter = exports.InMemoryCacheAdapter = exports.FileSystemAdapter = exports.GCSAdapter = exports.S3Adapter = undefined; + +var _ParseServer2 = require('./ParseServer'); + +var _ParseServer3 = _interopRequireDefault(_ParseServer2); + +var _parseServerS3Adapter = require('parse-server-s3-adapter'); + +var _parseServerS3Adapter2 = _interopRequireDefault(_parseServerS3Adapter); + +var _parseServerFsAdapter = require('parse-server-fs-adapter'); + +var _parseServerFsAdapter2 = _interopRequireDefault(_parseServerFsAdapter); + +var _InMemoryCacheAdapter = require('./Adapters/Cache/InMemoryCacheAdapter'); + +var _InMemoryCacheAdapter2 = _interopRequireDefault(_InMemoryCacheAdapter); + +var _NullCacheAdapter = require('./Adapters/Cache/NullCacheAdapter'); + +var _NullCacheAdapter2 = _interopRequireDefault(_NullCacheAdapter); + +var _RedisCacheAdapter = require('./Adapters/Cache/RedisCacheAdapter'); + +var _RedisCacheAdapter2 = _interopRequireDefault(_RedisCacheAdapter); + +var _TestUtils = require('./TestUtils'); + +var TestUtils = _interopRequireWildcard(_TestUtils); + +var _deprecated = require('./deprecated'); + +var _logger = require('./logger'); + +var _PushWorker = require('./Push/PushWorker'); + +function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// Factory function +var _ParseServer = function _ParseServer(options) { + var server = new _ParseServer3.default(options); + return server.app; +}; +// Mount the create liveQueryServer +_ParseServer.createLiveQueryServer = _ParseServer3.default.createLiveQueryServer; + +var GCSAdapter = (0, _deprecated.useExternal)('GCSAdapter', 'parse-server-gcs-adapter'); + +Object.defineProperty(module.exports, 'logger', { + get: _logger.getLogger +}); + +exports.default = _ParseServer3.default; +exports.S3Adapter = _parseServerS3Adapter2.default; +exports.GCSAdapter = GCSAdapter; +exports.FileSystemAdapter = _parseServerFsAdapter2.default; +exports.InMemoryCacheAdapter = _InMemoryCacheAdapter2.default; +exports.NullCacheAdapter = _NullCacheAdapter2.default; +exports.RedisCacheAdapter = _RedisCacheAdapter2.default; +exports.TestUtils = TestUtils; +exports.PushWorker = _PushWorker.PushWorker; +exports.ParseServer = _ParseServer; \ No newline at end of file diff --git a/lib/logger.js b/lib/logger.js new file mode 100644 index 0000000000..641704b2da --- /dev/null +++ b/lib/logger.js @@ -0,0 +1,46 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.setLogger = setLogger; +exports.getLogger = getLogger; + +var _defaults = require('./defaults'); + +var _defaults2 = _interopRequireDefault(_defaults); + +var _WinstonLoggerAdapter = require('./Adapters/Logger/WinstonLoggerAdapter'); + +var _LoggerController = require('./Controllers/LoggerController'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +function defaultLogger() { + var adapter = new _WinstonLoggerAdapter.WinstonLoggerAdapter({ + logsFolder: _defaults2.default.logsFolder, + jsonLogs: _defaults2.default.jsonLogs, + verbose: _defaults2.default.verbose, + silent: _defaults2.default.silent }); + return new _LoggerController.LoggerController(adapter); +} + +var logger = defaultLogger(); + +function setLogger(aLogger) { + logger = aLogger; +} + +function getLogger() { + return logger; +} + +// for: `import logger from './logger'` +Object.defineProperty(module.exports, 'default', { + get: getLogger +}); + +// for: `import { logger } from './logger'` +Object.defineProperty(module.exports, 'logger', { + get: getLogger +}); \ No newline at end of file diff --git a/lib/middlewares.js b/lib/middlewares.js new file mode 100644 index 0000000000..75bca9cb31 --- /dev/null +++ b/lib/middlewares.js @@ -0,0 +1,310 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.handleParseHeaders = handleParseHeaders; +exports.allowCrossDomain = allowCrossDomain; +exports.allowMethodOverride = allowMethodOverride; +exports.handleParseErrors = handleParseErrors; +exports.enforceMasterKeyAccess = enforceMasterKeyAccess; +exports.promiseEnforceMasterKeyAccess = promiseEnforceMasterKeyAccess; + +var _cache = require('./cache'); + +var _cache2 = _interopRequireDefault(_cache); + +var _logger = require('./logger'); + +var _logger2 = _interopRequireDefault(_logger); + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _Auth = require('./Auth'); + +var _Auth2 = _interopRequireDefault(_Auth); + +var _Config = require('./Config'); + +var _Config2 = _interopRequireDefault(_Config); + +var _ClientSDK = require('./ClientSDK'); + +var _ClientSDK2 = _interopRequireDefault(_ClientSDK); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// Checks that the request is authorized for this app and checks user +// auth too. +// The bodyparser should run before this middleware. +// Adds info to the request: +// req.config - the Config for this app +// req.auth - the Auth for this request +function handleParseHeaders(req, res, next) { + var mountPathLength = req.originalUrl.length - req.url.length; + var mountPath = req.originalUrl.slice(0, mountPathLength); + var mount = req.protocol + '://' + req.get('host') + mountPath; + + var info = { + appId: req.get('X-Parse-Application-Id'), + sessionToken: req.get('X-Parse-Session-Token'), + masterKey: req.get('X-Parse-Master-Key'), + installationId: req.get('X-Parse-Installation-Id'), + clientKey: req.get('X-Parse-Client-Key'), + 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') + }; + + var basicAuth = httpAuth(req); + + if (basicAuth) { + var basicAuthAppId = basicAuth.appId; + if (_cache2.default.get(basicAuthAppId)) { + info.appId = basicAuthAppId; + info.masterKey = basicAuth.masterKey || info.masterKey; + info.javascriptKey = basicAuth.javascriptKey || info.javascriptKey; + } + } + + if (req.body) { + // Unity SDK sends a _noBody key which needs to be removed. + // Unclear at this point if action needs to be taken. + delete req.body._noBody; + } + + var fileViaJSON = false; + + if (!info.appId || !_cache2.default.get(info.appId)) { + // See if we can find the app id on the body. + if (req.body instanceof Buffer) { + // The only chance to find the app id is if this is a file + // upload that actually is a JSON body. So try to parse it. + req.body = JSON.parse(req.body); + fileViaJSON = true; + } + + if (req.body) { + delete req.body._RevocableSession; + } + + if (req.body && req.body._ApplicationId && _cache2.default.get(req.body._ApplicationId) && (!info.masterKey || _cache2.default.get(req.body._ApplicationId).masterKey === info.masterKey)) { + info.appId = req.body._ApplicationId; + info.javascriptKey = req.body._JavaScriptKey || ''; + delete req.body._ApplicationId; + delete req.body._JavaScriptKey; + // TODO: test that the REST API formats generated by the other + // SDKs are handled ok + if (req.body._ClientVersion) { + info.clientVersion = req.body._ClientVersion; + delete req.body._ClientVersion; + } + if (req.body._InstallationId) { + info.installationId = req.body._InstallationId; + delete req.body._InstallationId; + } + if (req.body._SessionToken) { + info.sessionToken = req.body._SessionToken; + delete req.body._SessionToken; + } + if (req.body._MasterKey) { + info.masterKey = req.body._MasterKey; + delete req.body._MasterKey; + } + if (req.body._ContentType) { + req.headers['content-type'] = req.body._ContentType; + delete req.body._ContentType; + } + } else { + return invalidRequest(req, res); + } + } + + if (info.clientVersion) { + info.clientSDK = _ClientSDK2.default.fromString(info.clientVersion); + } + + if (fileViaJSON) { + // We need to repopulate req.body with a buffer + var base64 = req.body.base64; + req.body = new Buffer(base64, 'base64'); + } + + info.app = _cache2.default.get(info.appId); + req.config = new _Config2.default(info.appId, mount); + req.info = info; + + var isMaster = info.masterKey === req.config.masterKey; + + if (isMaster) { + req.auth = new _Auth2.default.Auth({ config: req.config, installationId: info.installationId, isMaster: 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. + var keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"]; + var oneKeyConfigured = keys.some(function (key) { + return req.config[key] !== undefined; + }); + var oneKeyMatches = keys.some(function (key) { + return req.config[key] !== undefined && info[key] === req.config[key]; + }); + + if (oneKeyConfigured && !oneKeyMatches) { + return invalidRequest(req, res); + } + + if (req.url == "/login") { + delete info.sessionToken; + } + + if (!info.sessionToken) { + req.auth = new _Auth2.default.Auth({ config: req.config, installationId: info.installationId, isMaster: false }); + next(); + return; + } + + return Promise.resolve().then(function () { + // handle the upgradeToRevocableSession path on it's own + if (info.sessionToken && req.url === '/upgradeToRevocableSession' && info.sessionToken.indexOf('r:') != 0) { + return _Auth2.default.getAuthForLegacySessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }); + } else { + return _Auth2.default.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }); + } + }).then(function (auth) { + if (auth) { + req.auth = auth; + next(); + } + }).catch(function (error) { + if (error instanceof _node2.default.Error) { + next(error); + return; + } else { + // TODO: Determine the correct error scenario. + _logger2.default.error('error getting auth for sessionToken', error); + throw new _node2.default.Error(_node2.default.Error.UNKNOWN_ERROR, error); + } + }); +} + +function httpAuth(req) { + if (!(req.req || req).headers.authorization) return; + + var header = (req.req || req).headers.authorization; + var appId, masterKey, javascriptKey; + + // parse header + var authPrefix = 'basic '; + + var match = header.toLowerCase().indexOf(authPrefix); + + if (match == 0) { + var encodedAuth = header.substring(authPrefix.length, header.length); + var credentials = decodeBase64(encodedAuth).split(':'); + + if (credentials.length == 2) { + appId = credentials[0]; + var key = credentials[1]; + + var jsKeyPrefix = 'javascript-key='; + + var matchKey = key.indexOf(jsKeyPrefix); + if (matchKey == 0) { + javascriptKey = key.substring(jsKeyPrefix.length, key.length); + } else { + masterKey = key; + } + } + } + + return { appId: appId, masterKey: masterKey, javascriptKey: javascriptKey }; +} + +function decodeBase64(str) { + return new Buffer(str, 'base64').toString(); +} + +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'); + + // intercept OPTIONS method + if ('OPTIONS' == req.method) { + res.sendStatus(200); + } else { + next(); + } +} + +function allowMethodOverride(req, res, next) { + if (req.method === 'POST' && req.body._method) { + req.originalMethod = req.method; + req.method = req.body._method; + delete req.body._method; + } + next(); +} + +function handleParseErrors(err, req, res, next) { + if (err instanceof _node2.default.Error) { + var httpStatus = void 0; + // TODO: fill out this mapping + switch (err.code) { + case _node2.default.Error.INTERNAL_SERVER_ERROR: + httpStatus = 500; + break; + case _node2.default.Error.OBJECT_NOT_FOUND: + httpStatus = 404; + break; + default: + httpStatus = 400; + } + + res.status(httpStatus); + res.json({ code: err.code, error: err.message }); + _logger2.default.error(err.message, err); + } else if (err.status && err.message) { + res.status(err.status); + res.json({ error: err.message }); + next(err); + } else { + _logger2.default.error('Uncaught internal server error.', err, err.stack); + res.status(500); + res.json({ + code: _node2.default.Error.INTERNAL_SERVER_ERROR, + message: 'Internal server error.' + }); + next(err); + } +} + +function enforceMasterKeyAccess(req, res, next) { + if (!req.auth.isMaster) { + res.status(403); + res.end('{"error":"unauthorized: master key is required"}'); + return; + } + next(); +} + +function promiseEnforceMasterKeyAccess(request) { + if (!request.auth.isMaster) { + var error = new Error(); + error.status = 403; + error.message = "unauthorized: master key is required"; + throw error; + } + return Promise.resolve(); +} + +function invalidRequest(req, res) { + res.status(403); + res.end('{"error":"unauthorized"}'); +} \ No newline at end of file diff --git a/lib/password.js b/lib/password.js new file mode 100644 index 0000000000..729a07b272 --- /dev/null +++ b/lib/password.js @@ -0,0 +1,29 @@ +'use strict'; + +// Tools for encrypting and decrypting passwords. +// Basically promise-friendly wrappers for bcrypt. +var bcrypt = require('bcryptjs'); + +try { + bcrypt = require('bcrypt'); +} catch (e) {} /* */ + +// Returns a promise for a hashed password string. +function hash(password) { + return bcrypt.hash(password, 10); +} + +// Returns a promise for whether this password compares to equal this +// hashed password. +function compare(password, hashedPassword) { + // Cannot bcrypt compare when one is undefined + if (!password || !hashedPassword) { + return Promise.resolve(false); + } + return bcrypt.compare(password, hashedPassword); +} + +module.exports = { + hash: hash, + compare: compare +}; \ No newline at end of file diff --git a/lib/requiredParameter.js b/lib/requiredParameter.js new file mode 100644 index 0000000000..6b8272e016 --- /dev/null +++ b/lib/requiredParameter.js @@ -0,0 +1,9 @@ +"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); + +exports.default = function (errorMessage) { + throw errorMessage; +}; \ No newline at end of file diff --git a/lib/rest.js b/lib/rest.js new file mode 100644 index 0000000000..dea46ad966 --- /dev/null +++ b/lib/rest.js @@ -0,0 +1,157 @@ +'use strict'; + +var _Auth = require('./Auth'); + +var _Auth2 = _interopRequireDefault(_Auth); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +// This file contains helpers for running operations in REST format. +// The goal is that handlers that explicitly handle an express route +// should just be shallow wrappers around things in this file, but +// these functions should not explicitly depend on the request +// object. +// This means that one of these handlers can support multiple +// routes. That's useful for the routes that do really similar +// things. + +var Parse = require('parse/node').Parse; + + +var RestQuery = require('./RestQuery'); +var RestWrite = require('./RestWrite'); +var triggers = require('./triggers'); + +function checkTriggers(className, config, types) { + return types.some(function (triggerType) { + return triggers.getTrigger(className, triggers.Types[triggerType], config.applicationId); + }); +} + +function checkLiveQuery(className, config) { + return config.liveQueryController && config.liveQueryController.hasLiveQuery(className); +} + +// Returns a promise for an object with optional keys 'results' and 'count'. +function find(config, auth, className, restWhere, restOptions, clientSDK) { + enforceRoleSecurity('find', className, auth); + return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth).then(function (result) { + restWhere = result.restWhere || restWhere; + restOptions = result.restOptions || restOptions; + var query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK); + return query.execute(); + }); +} + +// get is just like find but only queries an objectId. +var get = function get(config, auth, className, objectId, restOptions, clientSDK) { + enforceRoleSecurity('get', className, auth); + var query = new RestQuery(config, auth, className, { objectId: objectId }, 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'); + } + + if (className === '_User' && !auth.couldUpdateUserId(objectId)) { + throw new Parse.Error(Parse.Error.SESSION_MISSING, 'insufficient auth to delete user'); + } + + enforceRoleSecurity('delete', className, auth); + + var inflatedObject; + + return Promise.resolve().then(function () { + var hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']); + var hasLiveQuery = checkLiveQuery(className, config); + if (hasTriggers || hasLiveQuery || className == '_Session') { + return find(config, _Auth2.default.master(config), className, { objectId: objectId }).then(function (response) { + if (response && response.results && response.results.length) { + response.results[0].className = className; + + var cacheAdapter = config.cacheController; + cacheAdapter.user.del(response.results[0].sessionToken); + inflatedObject = Parse.Object.fromJSON(response.results[0]); + // 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(function () { + if (!auth.isMaster) { + return auth.getUserRoles(); + } else { + return; + } + }).then(function () { + 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(function () { + return triggers.maybeRunTrigger(triggers.Types.afterDelete, auth, inflatedObject, null, config); + }); +} + +// 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); + return write.execute(); +} + +// Returns a promise that contains the fields of the update that the +// REST API is supposed to return. +// Usually, this is just updatedAt. +function update(config, auth, className, objectId, restObject, clientSDK) { + enforceRoleSecurity('update', className, auth); + + return Promise.resolve().then(function () { + var hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']); + var hasLiveQuery = checkLiveQuery(className, config); + if (hasTriggers || hasLiveQuery) { + return find(config, _Auth2.default.master(config), className, { objectId: objectId }); + } + return Promise.resolve({}); + }).then(function (response) { + var originalRestObject; + if (response && response.results && response.results.length) { + originalRestObject = response.results[0]; + } + + var write = new RestWrite(config, auth, className, { objectId: objectId }, restObject, originalRestObject, clientSDK); + return write.execute(); + }); +} + +// 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') { + var error = 'Clients aren\'t allowed to perform the ' + method + ' operation on the installation collection.'; + throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); + } + } +} + +module.exports = { + create: create, + del: del, + find: find, + get: get, + update: update +}; \ No newline at end of file diff --git a/lib/triggers.js b/lib/triggers.js new file mode 100644 index 0000000000..e483034bcf --- /dev/null +++ b/lib/triggers.js @@ -0,0 +1,437 @@ +'use strict'; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.Types = undefined; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; // triggers.js + + +exports.addFunction = addFunction; +exports.addJob = addJob; +exports.addTrigger = addTrigger; +exports.removeFunction = removeFunction; +exports.removeJob = removeJob; +exports.removeTrigger = removeTrigger; +exports._unregister = _unregister; +exports._unregisterAll = _unregisterAll; +exports.getTrigger = getTrigger; +exports.triggerExists = triggerExists; +exports.getFunction = getFunction; +exports.getJob = getJob; +exports.getJobs = getJobs; +exports.getValidator = getValidator; +exports.getRequestObject = getRequestObject; +exports.getRequestQueryObject = getRequestQueryObject; +exports.getResponseObject = getResponseObject; +exports.maybeRunAfterFindTrigger = maybeRunAfterFindTrigger; +exports.maybeRunQueryTrigger = maybeRunQueryTrigger; +exports.maybeRunTrigger = maybeRunTrigger; +exports.inflate = inflate; + +var _node = require('parse/node'); + +var _node2 = _interopRequireDefault(_node); + +var _logger = require('./logger'); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + +var Types = exports.Types = { + beforeSave: 'beforeSave', + afterSave: 'afterSave', + beforeDelete: 'beforeDelete', + afterDelete: 'afterDelete', + beforeFind: 'beforeFind', + afterFind: 'afterFind' +}; + +var baseStore = function baseStore() { + var Validators = {}; + var Functions = {}; + var Jobs = {}; + var Triggers = Object.keys(Types).reduce(function (base, key) { + base[key] = {}; + return base; + }, {}); + + return Object.freeze({ + Functions: Functions, + Jobs: Jobs, + Validators: Validators, + Triggers: Triggers + }); +}; + +var _triggerStore = {}; + +function addFunction(functionName, handler, validationHandler, applicationId) { + applicationId = applicationId || _node2.default.applicationId; + _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); + _triggerStore[applicationId].Functions[functionName] = handler; + _triggerStore[applicationId].Validators[functionName] = validationHandler; +} + +function addJob(jobName, handler, applicationId) { + applicationId = applicationId || _node2.default.applicationId; + _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); + _triggerStore[applicationId].Jobs[jobName] = handler; +} + +function addTrigger(type, className, handler, applicationId) { + applicationId = applicationId || _node2.default.applicationId; + _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); + _triggerStore[applicationId].Triggers[type][className] = handler; +} + +function removeFunction(functionName, applicationId) { + applicationId = applicationId || _node2.default.applicationId; + delete _triggerStore[applicationId].Functions[functionName]; +} + +function removeJob(jobName, applicationId) { + applicationId = applicationId || _node2.default.applicationId; + delete _triggerStore[applicationId].Jobs[jobName]; +} + +function removeTrigger(type, className, applicationId) { + applicationId = applicationId || _node2.default.applicationId; + delete _triggerStore[applicationId].Triggers[type][className]; +} + +function _unregister(appId, category, className, type) { + if (type) { + removeTrigger(className, type, appId); + delete _triggerStore[appId][category][className][type]; + } else { + delete _triggerStore[appId][category][className]; + } +} + +function _unregisterAll() { + Object.keys(_triggerStore).forEach(function (appId) { + return delete _triggerStore[appId]; + }); +} + +function getTrigger(className, triggerType, applicationId) { + if (!applicationId) { + throw "Missing ApplicationID"; + } + var manager = _triggerStore[applicationId]; + if (manager && manager.Triggers && manager.Triggers[triggerType] && manager.Triggers[triggerType][className]) { + return manager.Triggers[triggerType][className]; + } + return undefined; +} + +function triggerExists(className, type, applicationId) { + return getTrigger(className, type, applicationId) != undefined; +} + +function getFunction(functionName, applicationId) { + var manager = _triggerStore[applicationId]; + if (manager && manager.Functions) { + return manager.Functions[functionName]; + } + return undefined; +} + +function getJob(jobName, applicationId) { + var manager = _triggerStore[applicationId]; + if (manager && manager.Jobs) { + return manager.Jobs[jobName]; + } + return undefined; +} + +function getJobs(applicationId) { + var manager = _triggerStore[applicationId]; + if (manager && manager.Jobs) { + return manager.Jobs; + } + return undefined; +} + +function getValidator(functionName, applicationId) { + var manager = _triggerStore[applicationId]; + if (manager && manager.Validators) { + return manager.Validators[functionName]; + } + return undefined; +} + +function getRequestObject(triggerType, auth, parseObject, originalParseObject, config) { + var request = { + triggerName: triggerType, + object: parseObject, + master: false, + log: config.loggerController + }; + + if (originalParseObject) { + request.original = originalParseObject; + } + + if (!auth) { + return request; + } + if (auth.isMaster) { + request['master'] = true; + } + if (auth.user) { + request['user'] = auth.user; + } + if (auth.installationId) { + request['installationId'] = auth.installationId; + } + return request; +} + +function getRequestQueryObject(triggerType, auth, query, count, config) { + var request = { + triggerName: triggerType, + query: query, + master: false, + count: count, + log: config.loggerController + }; + + if (!auth) { + return request; + } + if (auth.isMaster) { + request['master'] = true; + } + if (auth.user) { + request['user'] = auth.user; + } + if (auth.installationId) { + request['installationId'] = auth.installationId; + } + return request; +} + +// Creates the response object, and uses the request object to pass data +// The API will call this with REST API formatted objects, this will +// transform them to Parse.Object instances expected by Cloud Code. +// Any changes made to the object in a beforeSave will be included. +function getResponseObject(request, resolve, reject) { + return { + success: function success(response) { + if (request.triggerName === Types.afterFind) { + if (!response) { + response = request.objects; + } + response = response.map(function (object) { + return object.toJSON(); + }); + return resolve(response); + } + // Use the JSON response + if (response && !request.object.equals(response) && request.triggerName === Types.beforeSave) { + return resolve(response); + } + response = {}; + if (request.triggerName === Types.beforeSave) { + response['object'] = request.object._getSaveJSON(); + } + return resolve(response); + }, + error: function error(code, message) { + if (!message) { + message = code; + code = _node2.default.Error.SCRIPT_FAILED; + } + var scriptError = new _node2.default.Error(code, message); + return reject(scriptError); + } + }; +} + +function userIdForLog(auth) { + return auth && auth.user ? auth.user.id : undefined; +} + +function logTriggerAfterHook(triggerType, className, input, auth) { + var cleanInput = _logger.logger.truncateLogMessage(JSON.stringify(input)); + _logger.logger.info(triggerType + ' triggered for ' + className + ' for user ' + userIdForLog(auth) + ':\n Input: ' + cleanInput, { + className: className, + triggerType: triggerType, + user: userIdForLog(auth) + }); +} + +function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth) { + var cleanInput = _logger.logger.truncateLogMessage(JSON.stringify(input)); + var cleanResult = _logger.logger.truncateLogMessage(JSON.stringify(result)); + _logger.logger.info(triggerType + ' triggered for ' + className + ' for user ' + userIdForLog(auth) + ':\n Input: ' + cleanInput + '\n Result: ' + cleanResult, { + className: className, + triggerType: triggerType, + user: userIdForLog(auth) + }); +} + +function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) { + var cleanInput = _logger.logger.truncateLogMessage(JSON.stringify(input)); + _logger.logger.error(triggerType + ' failed for ' + className + ' for user ' + userIdForLog(auth) + ':\n Input: ' + cleanInput + '\n Error: ' + JSON.stringify(error), { + className: className, + triggerType: triggerType, + error: error, + user: userIdForLog(auth) + }); +} + +function maybeRunAfterFindTrigger(triggerType, auth, className, objects, config) { + return new Promise(function (resolve, reject) { + var trigger = getTrigger(className, triggerType, config.applicationId); + if (!trigger) { + return resolve(); + } + var request = getRequestObject(triggerType, auth, null, null, config); + var response = getResponseObject(request, function (object) { + resolve(object); + }, function (error) { + reject(error); + }); + logTriggerSuccessBeforeHook(triggerType, className, 'AfterFind', JSON.stringify(objects), auth); + request.objects = objects.map(function (object) { + //setting the class name to transform into parse object + object.className = className; + return _node2.default.Object.fromJSON(object); + }); + var triggerPromise = trigger(request, response); + if (triggerPromise && typeof triggerPromise.then === "function") { + return triggerPromise.then(function (promiseResults) { + if (promiseResults) { + resolve(promiseResults); + } else { + return reject(new _node2.default.Error(_node2.default.Error.SCRIPT_FAILED, "AfterFind expect results to be returned in the promise")); + } + }); + } + }).then(function (results) { + logTriggerAfterHook(triggerType, className, JSON.stringify(results), auth); + return results; + }); +} + +function maybeRunQueryTrigger(triggerType, className, restWhere, restOptions, config, auth) { + var trigger = getTrigger(className, triggerType, config.applicationId); + if (!trigger) { + return Promise.resolve({ + restWhere: restWhere, + restOptions: restOptions + }); + } + + var parseQuery = new _node2.default.Query(className); + if (restWhere) { + parseQuery._where = restWhere; + } + var count = false; + if (restOptions) { + if (restOptions.include && restOptions.include.length > 0) { + parseQuery._include = restOptions.include.split(','); + } + if (restOptions.skip) { + parseQuery._skip = restOptions.skip; + } + if (restOptions.limit) { + parseQuery._limit = restOptions.limit; + } + count = !!restOptions.count; + } + var requestObject = getRequestQueryObject(triggerType, auth, parseQuery, count, config); + return Promise.resolve().then(function () { + return trigger(requestObject); + }).then(function (result) { + var queryResult = parseQuery; + if (result && result instanceof _node2.default.Query) { + queryResult = result; + } + var 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; + } + return { + restWhere: restWhere, + restOptions: restOptions + }; + }, function (err) { + if (typeof err === 'string') { + throw new _node2.default.Error(1, err); + } else { + throw err; + } + }); +} + +// To be used as part of the promise chain when saving/deleting an object +// Will resolve successfully if no trigger is configured +// 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 +function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, config) { + if (!parseObject) { + return Promise.resolve({}); + } + 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); + var response = getResponseObject(request, function (object) { + logTriggerSuccessBeforeHook(triggerType, parseObject.className, parseObject.toJSON(), object, auth); + resolve(object); + }, function (error) { + logTriggerErrorBeforeHook(triggerType, parseObject.className, parseObject.toJSON(), auth, error); + reject(error); + }); + // Force the current Parse app before the trigger + _node2.default.applicationId = config.applicationId; + _node2.default.javascriptKey = config.javascriptKey || ''; + _node2.default.masterKey = config.masterKey; + + // 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. + var triggerPromise = trigger(request, response); + if (triggerType === Types.afterSave || triggerType === Types.afterDelete) { + logTriggerAfterHook(triggerType, parseObject.className, parseObject.toJSON(), auth); + if (triggerPromise && typeof triggerPromise.then === "function") { + return triggerPromise.then(resolve, resolve); + } else { + return resolve(); + } + } + }); +} + +// Converts a REST-format object to a Parse.Object +// data is either className or an object +function inflate(data, restObject) { + var copy = (typeof data === 'undefined' ? 'undefined' : _typeof(data)) == 'object' ? data : { className: data }; + for (var key in restObject) { + copy[key] = restObject[key]; + } + return _node2.default.Object.fromJSON(copy); +} \ No newline at end of file diff --git a/lib/vendor/README.md b/lib/vendor/README.md new file mode 100644 index 0000000000..d51e8ea4ec --- /dev/null +++ b/lib/vendor/README.md @@ -0,0 +1,8 @@ +# mongoUrl + +A fork of node's `url` module, with the modification that commas and colons are +allowed in hostnames. While this results in a slightly incorrect parsed result, +as the hostname field for a mongodb should be an array of replica sets, it's +good enough to let us pull out and escape the auth portion of the URL. + +See also: https://github.com/ParsePlatform/parse-server/pull/986 diff --git a/lib/vendor/mongodbUrl.js b/lib/vendor/mongodbUrl.js new file mode 100644 index 0000000000..5b0e3b2db9 --- /dev/null +++ b/lib/vendor/mongodbUrl.js @@ -0,0 +1,928 @@ +// A slightly patched version of node's url module, with support for mongodb:// +// uris. +// +// See https://github.com/nodejs/node/blob/master/LICENSE for licensing +// information + +'use strict'; + +var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var punycode = require('punycode'); + +exports.parse = urlParse; +exports.resolve = urlResolve; +exports.resolveObject = urlResolveObject; +exports.format = urlFormat; + +exports.Url = Url; + +function Url() { + this.protocol = null; + this.slashes = null; + this.auth = null; + this.host = null; + this.port = null; + this.hostname = null; + this.hash = null; + this.search = null; + this.query = null; + this.pathname = null; + this.path = null; + this.href = null; +} + +// Reference: RFC 3986, RFC 1808, RFC 2396 + +// define these here so at least they only have to be +// compiled once on the first module load. +var protocolPattern = /^([a-z0-9.+-]+:)/i; +var portPattern = /:[0-9]*$/; + +// Special case for a simple path URL +var simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/; + +var hostnameMaxLen = 255; +// protocols that can allow "unsafe" and "unwise" chars. +var unsafeProtocol = { + 'javascript': true, + 'javascript:': true +}; +// protocols that never have a hostname. +var hostlessProtocol = { + 'javascript': true, + 'javascript:': true +}; +// protocols that always contain a // bit. +var slashedProtocol = { + 'http': true, + 'http:': true, + 'https': true, + 'https:': true, + 'ftp': true, + 'ftp:': true, + 'gopher': true, + 'gopher:': true, + 'file': true, + 'file:': true +}; +var querystring = require('querystring'); + +/* istanbul ignore next: improve coverage */ +function urlParse(url, parseQueryString, slashesDenoteHost) { + if (url instanceof Url) return url; + + var u = new Url(); + u.parse(url, parseQueryString, slashesDenoteHost); + return u; +} + +/* istanbul ignore next: improve coverage */ +Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { + if (typeof url !== 'string') { + throw new TypeError('Parameter "url" must be a string, not ' + (typeof url === 'undefined' ? 'undefined' : _typeof(url))); + } + + // Copy chrome, IE, opera backslash-handling behavior. + // Back slashes before the query string get converted to forward slashes + // See: https://code.google.com/p/chromium/issues/detail?id=25916 + var hasHash = false; + var start = -1; + var end = -1; + var rest = ''; + var lastPos = 0; + var i = 0; + for (var inWs = false, split = false; i < url.length; ++i) { + var code = url.charCodeAt(i); + + // Find first and last non-whitespace characters for trimming + var 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; + lastPos = start = i; + } else { + if (inWs) { + if (!isWs) { + end = -1; + inWs = false; + } + } else if (isWs) { + end = i; + inWs = true; + } + } + + // Only convert backslashes while we haven't seen a split character + if (!split) { + switch (code) { + 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; + } + } else if (!hasHash && code === 35 /*#*/) { + hasHash = true; + } + } + + // Check if string was non-empty (including strings with only whitespace) + if (start !== -1) { + if (lastPos === start) { + // We didn't convert any backslashes + + if (end === -1) { + if (start === 0) rest = url;else rest = url.slice(start); + } else { + rest = url.slice(start, end); + } + } else if (end === -1 && lastPos < url.length) { + // We converted some backslashes and have only part of the entire string + rest += url.slice(lastPos); + } else if (end !== -1 && lastPos < end) { + // We converted some backslashes and have only part of the entire string + rest += url.slice(lastPos, end); + } + } + + if (!slashesDenoteHost && !hasHash) { + // Try fast path regexp + var simplePath = simplePathPattern.exec(rest); + if (simplePath) { + this.path = rest; + this.href = rest; + this.pathname = simplePath[1]; + if (simplePath[2]) { + this.search = simplePath[2]; + if (parseQueryString) { + this.query = querystring.parse(this.search.slice(1)); + } else { + this.query = this.search.slice(1); + } + } else if (parseQueryString) { + this.search = ''; + this.query = {}; + } + return this; + } + } + + var proto = protocolPattern.exec(rest); + if (proto) { + proto = proto[0]; + var lowerProto = proto.toLowerCase(); + this.protocol = lowerProto; + rest = rest.slice(proto.length); + } + + // figure out if it's got a host + // user@server is *always* interpreted as a hostname, and url + // 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 /*/*/; + if (slashes && !(proto && hostlessProtocol[proto])) { + rest = rest.slice(2); + this.slashes = true; + } + } + + if (!hostlessProtocol[proto] && (slashes || proto && !slashedProtocol[proto])) { + + // there's a hostname. + // the first instance of /, ?, ;, or # ends the host. + // + // If there is an @ in the hostname, then non-host chars *are* allowed + // to the left of the last @ sign, unless some host-ending character + // comes *before* the @-sign. + // URLs are obnoxious. + // + // ex: + // http://a@b@c/ => user:a@b host:c + // http://a@b?@c => user:a host:b path:/?@c + + // v0.12 TODO(isaacs): This is not quite how Chrome does things. + // Review our test case against browsers more comprehensively. + + var hostEnd = -1; + var atSign = -1; + 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; + } + if (hostEnd !== -1) break; + } + start = 0; + if (atSign !== -1) { + this.auth = decodeURIComponent(rest.slice(0, atSign)); + start = atSign + 1; + } + if (nonHost === -1) { + this.host = rest.slice(start); + rest = ''; + } else { + this.host = rest.slice(start, nonHost); + rest = rest.slice(nonHost); + } + + // pull out port. + this.parseHost(); + + // 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 = ''; + + 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 /*]*/; + + // validate a little. + if (!ipv6Hostname) { + var result = validateHostname(this, rest, hostname); + if (result !== undefined) rest = result; + } + + if (this.hostname.length > hostnameMaxLen) { + this.hostname = ''; + } else { + // hostnames are always lower case. + this.hostname = this.hostname.toLowerCase(); + } + + if (!ipv6Hostname) { + // IDNA Support: Returns a punycoded representation of "domain". + // It only converts parts of the domain name that + // have non-ASCII characters, i.e. it doesn't matter if + // you call it with a domain that already is ASCII-only. + this.hostname = punycode.toASCII(this.hostname); + } + + var p = this.port ? ':' + this.port : ''; + var h = this.hostname || ''; + this.host = h + p; + + // strip [ and ] from the hostname + // the host field still retains them, though + if (ipv6Hostname) { + this.hostname = this.hostname.slice(1, -1); + if (rest[0] !== '/') { + rest = '/' + rest; + } + } + } + + // now rest is set to the post-host stuff. + // chop off any delim chars. + if (!unsafeProtocol[lowerProto]) { + // First, make 100% sure that any "autoEscape" chars get + // escaped, even if encodeURIComponent doesn't think they + // need to be. + var _result = autoEscapeStr(rest); + if (_result !== undefined) rest = _result; + } + + var questionIdx = -1; + var hashIdx = -1; + for (i = 0; i < rest.length; ++i) { + var _code = rest.charCodeAt(i); + if (_code === 35 /*#*/) { + this.hash = rest.slice(i); + hashIdx = i; + break; + } else if (_code === 63 /*?*/ && questionIdx === -1) { + questionIdx = i; + } + } + + if (questionIdx !== -1) { + if (hashIdx === -1) { + this.search = rest.slice(questionIdx); + this.query = rest.slice(questionIdx + 1); + } else { + this.search = rest.slice(questionIdx, hashIdx); + this.query = rest.slice(questionIdx + 1, hashIdx); + } + if (parseQueryString) { + this.query = querystring.parse(this.query); + } + } else if (parseQueryString) { + // no query string, but parseQueryString still requested + this.search = ''; + this.query = {}; + } + + var firstIdx = questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx) ? questionIdx : hashIdx; + if (firstIdx === -1) { + if (rest.length > 0) this.pathname = rest; + } else if (firstIdx > 0) { + this.pathname = rest.slice(0, firstIdx); + } + if (slashedProtocol[lowerProto] && this.hostname && !this.pathname) { + this.pathname = '/'; + } + + // to support http.request + if (this.pathname || this.search) { + var _p = this.pathname || ''; + var s = this.search || ''; + this.path = _p + s; + } + + // finally, reconstruct the href based on what has been validated. + this.href = this.format(); + return this; +}; + +/* istanbul ignore next: improve coverage */ +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 - lastPos > 0) { + if (i - lastPos > 63) { + self.hostname = hostname.slice(0, lastPos + 63); + return '/' + hostname.slice(lastPos + 63) + rest; + } + } + 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) { + continue; + } + // Invalid host character + self.hostname = hostname.slice(0, i); + if (i < hostname.length) return '/' + hostname.slice(i) + rest; + break; + } +} + +/* istanbul ignore next: improve coverage */ +function autoEscapeStr(rest) { + var newRest = ''; + var lastPos = 0; + for (var i = 0; i < rest.length; ++i) { + // 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; + } + } + if (lastPos === 0) return; + if (lastPos < rest.length) return newRest + rest.slice(lastPos);else return newRest; +} + +// format a parsed object into a url string +/* istanbul ignore next: improve coverage */ +function urlFormat(obj) { + // ensure it's an object, and not a string url. + // If it's an obj, this is a no-op. + // 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 === 'undefined' ? 'undefined' : _typeof(obj)) !== 'object' || obj === null) throw new TypeError('Parameter "urlObj" must be an object, not ' + obj === null ? 'null' : typeof obj === 'undefined' ? 'undefined' : _typeof(obj));else if (!(obj instanceof Url)) return Url.prototype.format.call(obj); + + return obj.format(); +} + +/* istanbul ignore next: improve coverage */ +Url.prototype.format = function () { + var auth = this.auth || ''; + if (auth) { + auth = encodeAuth(auth); + auth += '@'; + } + + var protocol = this.protocol || ''; + var pathname = this.pathname || ''; + var hash = this.hash || ''; + var host = false; + var query = ''; + + if (this.host) { + host = auth + this.host; + } else if (this.hostname) { + host = auth + (this.hostname.indexOf(':') === -1 ? this.hostname : '[' + this.hostname + ']'); + if (this.port) { + host += ':' + this.port; + } + } + + if (this.query !== null && _typeof(this.query) === 'object') query = querystring.stringify(this.query); + + var search = this.search || query && '?' + query || ''; + + 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; + } + } + if (lastPos > 0) { + if (lastPos !== pathname.length) pathname = newPathname + pathname.slice(lastPos);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) { + host = '//' + (host || ''); + if (pathname && pathname.charCodeAt(0) !== 47 /*/*/) pathname = '/' + pathname; + } else if (!host) { + host = ''; + } + + search = search.replace('#', '%23'); + + if (hash && hash.charCodeAt(0) !== 35 /*#*/) hash = '#' + hash; + if (search && search.charCodeAt(0) !== 63 /*?*/) search = '?' + search; + + return protocol + host + pathname + search + hash; +}; + +/* istanbul ignore next: improve coverage */ +function urlResolve(source, relative) { + return urlParse(source, false, true).resolve(relative); +} + +/* istanbul ignore next: improve coverage */ +Url.prototype.resolve = function (relative) { + return this.resolveObject(urlParse(relative, false, true)).format(); +}; + +/* istanbul ignore next: improve coverage */ +function urlResolveObject(source, relative) { + if (!source) return relative; + return urlParse(source, false, true).resolveObject(relative); +} + +/* istanbul ignore next: improve coverage */ +Url.prototype.resolveObject = function (relative) { + if (typeof relative === 'string') { + var rel = new Url(); + rel.parse(relative, false, true); + relative = rel; + } + + var result = new Url(); + var tkeys = Object.keys(this); + for (var tk = 0; tk < tkeys.length; tk++) { + var tkey = tkeys[tk]; + result[tkey] = this[tkey]; + } + + // hash is always overridden, no matter what. + // even href="" will remove it. + result.hash = relative.hash; + + // if the relative url is empty, then there's nothing left to do here. + if (relative.href === '') { + result.href = result.format(); + return result; + } + + // hrefs like //foo/bar always cut to the protocol. + if (relative.slashes && !relative.protocol) { + // take everything except the protocol from 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]; + } + + //urlParse appends trailing / to urls like http://www.example.com + if (slashedProtocol[result.protocol] && result.hostname && !result.pathname) { + result.path = result.pathname = '/'; + } + + result.href = result.format(); + return result; + } + + if (relative.protocol && relative.protocol !== result.protocol) { + // if it's a known url protocol, then changing + // the protocol does weird things + // first, if it's not file:, then we MUST have a host, + // and if there was a path + // to begin with, then we MUST have a path. + // if it is file:, then the host is dropped, + // because that's known to be hostless. + // anything else is assumed to be absolute. + if (!slashedProtocol[relative.protocol]) { + var keys = Object.keys(relative); + for (var v = 0; v < keys.length; v++) { + var k = keys[v]; + result[k] = relative[k]; + } + result.href = result.format(); + return result; + } + + result.protocol = relative.protocol; + if (!relative.host && !/^file:?$/.test(relative.protocol) && !hostlessProtocol[relative.protocol]) { + var _relPath = (relative.pathname || '').split('/'); + while (_relPath.length && !(relative.host = _relPath.shift())) {} + if (!relative.host) relative.host = ''; + if (!relative.hostname) relative.hostname = ''; + if (_relPath[0] !== '') _relPath.unshift(''); + if (_relPath.length < 2) _relPath.unshift(''); + result.pathname = _relPath.join('/'); + } else { + result.pathname = relative.pathname; + } + result.search = relative.search; + result.query = relative.query; + result.host = relative.host || ''; + result.auth = relative.auth; + result.hostname = relative.hostname || relative.host; + result.port = relative.port; + // to support http.request + if (result.pathname || result.search) { + var p = result.pathname || ''; + var s = result.search || ''; + result.path = p + s; + } + result.slashes = result.slashes || relative.slashes; + result.href = result.format(); + 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 removeAllDots = mustEndAbs; + 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 + // links like ../.. should be able + // to crawl up to the hostname, as well. This is strange. + // result.protocol has already been set by now. + // Later on, put the first path part into the host field. + if (psychotic) { + result.hostname = ''; + result.port = null; + if (result.host) { + if (srcPath[0] === '') srcPath[0] = result.host;else srcPath.unshift(result.host); + } + result.host = ''; + if (relative.protocol) { + relative.hostname = null; + relative.port = null; + if (relative.host) { + if (relPath[0] === '') relPath[0] = relative.host;else relPath.unshift(relative.host); + } + relative.host = null; + } + mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === ''); + } + + 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.search = relative.search; + result.query = relative.query; + srcPath = relPath; + // fall through to the dot-handling below. + } else if (relPath.length) { + // it's relative + // throw away the existing file, and take the new path instead. + if (!srcPath) srcPath = []; + srcPath.pop(); + srcPath = srcPath.concat(relPath); + result.search = relative.search; + result.query = relative.query; + } else if (relative.search !== null && relative.search !== undefined) { + // just pull out the search. + // like href='?foo'. + // Put this after the other two cases because it simplifies the booleans + if (psychotic) { + result.hostname = result.host = srcPath.shift(); + //occasionally the auth can get stuck only in host + //this especially happens in cases like + //url.resolveObject('mailto:local1@domain1', 'local2@domain2') + var authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false; + if (authInHost) { + result.auth = authInHost.shift(); + result.host = result.hostname = authInHost.shift(); + } + } + result.search = relative.search; + 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.href = result.format(); + return result; + } + + if (!srcPath.length) { + // no path at all. easy. + // we've already handled the other stuff above. + result.pathname = null; + //to support http.request + if (result.search) { + result.path = '/' + result.search; + } else { + result.path = null; + } + result.href = result.format(); + return result; + } + + // if a url ENDs in . or .., then it must get a trailing slash. + // 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 === ''; + + // strip single dots, resolve double dots to parent dir + // if the path tries to go above the root, `up` ends up > 0 + var up = 0; + for (var i = srcPath.length; i >= 0; i--) { + last = srcPath[i]; + if (last === '.') { + spliceOne(srcPath, i); + } else if (last === '..') { + spliceOne(srcPath, i); + up++; + } else if (up) { + spliceOne(srcPath, i); + up--; + } + } + + // if the path is allowed to go above the root, restore leading ..s + if (!mustEndAbs && !removeAllDots) { + for (; up--; up) { + srcPath.unshift('..'); + } + } + + if (mustEndAbs && srcPath[0] !== '' && (!srcPath[0] || srcPath[0].charAt(0) !== '/')) { + srcPath.unshift(''); + } + + if (hasTrailingSlash && srcPath.join('/').substr(-1) !== '/') { + srcPath.push(''); + } + + 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() : ''; + //occasionally the auth can get stuck only in host + //this especially happens in cases like + //url.resolveObject('mailto:local1@domain1', 'local2@domain2') + var _authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false; + if (_authInHost) { + result.auth = _authInHost.shift(); + result.host = result.hostname = _authInHost.shift(); + } + } + + mustEndAbs = mustEndAbs || result.host && srcPath.length; + + if (mustEndAbs && !isAbsolute) { + srcPath.unshift(''); + } + + if (!srcPath.length) { + result.pathname = null; + result.path = null; + } else { + result.pathname = srcPath.join('/'); + } + + //to support request.http + if (result.pathname !== null || result.search !== null) { + result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : ''); + } + result.auth = relative.auth || result.auth; + result.slashes = result.slashes || relative.slashes; + result.href = result.format(); + return result; +}; + +/* istanbul ignore next: improve coverage */ +Url.prototype.parseHost = function () { + var host = this.host; + var port = portPattern.exec(host); + if (port) { + port = port[0]; + if (port !== ':') { + this.port = port.slice(1); + } + host = host.slice(0, host.length - port.length); + } + if (host) this.hostname = host; +}; + +// About 1.5x faster than the two-arg version of Array#splice(). +/* istanbul ignore next: improve coverage */ +function spliceOne(list, index) { + for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) { + list[i] = list[k]; + }list.pop(); +} + +var hexTable = new Array(256); +for (var i = 0; i < 256; ++i) { + hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); +} /* istanbul ignore next: improve coverage */ +function encodeAuth(str) { + // faster encodeURIComponent alternative for encoding auth uri components + var out = ''; + var lastPos = 0; + for (var i = 0; i < str.length; ++i) { + var c = str.charCodeAt(i); + + // These characters do not need escaping: + // ! - . _ ~ + // ' ( ) * : + // 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) { + continue; + } + + if (i - lastPos > 0) out += str.slice(lastPos, i); + + lastPos = i + 1; + + // Other ASCII characters + if (c < 0x80) { + out += hexTable[c]; + continue; + } + + // Multi-byte characters ... + if (c < 0x800) { + 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]; + 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); + return out; +} \ No newline at end of file From 77580a0f7180f9118c5ab4faa236c371a4e5ada6 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Fri, 26 May 2017 09:04:59 +0200 Subject: [PATCH 03/29] Revert "feat: Add lib folder" This reverts commit c9dfbcbf699ff220baeb2df5586a944d19808e5e. --- .gitignore | 2 +- lib/AccountLockout.js | 199 --- lib/Adapters/AdapterLoader.js | 44 - lib/Adapters/Analytics/AnalyticsAdapter.js | 45 - lib/Adapters/Auth/AuthAdapter.js | 45 - lib/Adapters/Auth/OAuth1Client.js | 222 --- lib/Adapters/Auth/facebook.js | 56 - lib/Adapters/Auth/github.js | 54 - lib/Adapters/Auth/google.js | 70 - lib/Adapters/Auth/index.js | 123 -- lib/Adapters/Auth/instagram.js | 43 - lib/Adapters/Auth/janraincapture.js | 55 - lib/Adapters/Auth/janrainengage.js | 73 - lib/Adapters/Auth/linkedin.js | 60 - lib/Adapters/Auth/meetup.js | 53 - lib/Adapters/Auth/qq.js | 53 - lib/Adapters/Auth/spotify.js | 62 - lib/Adapters/Auth/twitter.js | 56 - lib/Adapters/Auth/vkontakte.js | 66 - lib/Adapters/Auth/wechat.js | 47 - lib/Adapters/Auth/weibo.js | 66 - lib/Adapters/Cache/CacheAdapter.js | 57 - lib/Adapters/Cache/InMemoryCache.js | 88 - lib/Adapters/Cache/InMemoryCacheAdapter.js | 58 - lib/Adapters/Cache/NullCacheAdapter.js | 43 - lib/Adapters/Cache/RedisCacheAdapter.js | 123 -- lib/Adapters/Email/MailAdapter.js | 44 - lib/Adapters/Files/FilesAdapter.js | 58 - lib/Adapters/Files/GridStoreAdapter.js | 114 -- lib/Adapters/Logger/LoggerAdapter.js | 34 - lib/Adapters/Logger/WinstonLogger.js | 131 -- lib/Adapters/Logger/WinstonLoggerAdapter.js | 95 -- lib/Adapters/MessageQueue/EventEmitterMQ.js | 99 -- lib/Adapters/PubSub/EventEmitterPubSub.js | 92 -- lib/Adapters/PubSub/RedisPubSub.js | 31 - lib/Adapters/Push/PushAdapter.js | 47 - lib/Adapters/Storage/Mongo/MongoCollection.js | 155 -- .../Storage/Mongo/MongoSchemaCollection.js | 244 --- .../Storage/Mongo/MongoStorageAdapter.js | 536 ------- lib/Adapters/Storage/Mongo/MongoTransform.js | 1022 ------------ .../Storage/Postgres/PostgresClient.js | 33 - .../Storage/Postgres/PostgresConfigParser.js | 46 - .../Postgres/PostgresStorageAdapter.js | 1426 ----------------- .../Storage/Postgres/sql/array/add-unique.sql | 11 - .../Storage/Postgres/sql/array/add.sql | 11 - .../Postgres/sql/array/contains-all.sql | 11 - .../Storage/Postgres/sql/array/contains.sql | 11 - .../Storage/Postgres/sql/array/remove.sql | 11 - lib/Adapters/Storage/Postgres/sql/index.js | 32 - .../sql/misc/json-object-set-keys.sql | 19 - lib/Auth.js | 241 --- lib/ClientSDK.js | 42 - lib/Config.js | 324 ---- lib/Controllers/AdaptableController.js | 101 -- lib/Controllers/AnalyticsController.js | 69 - lib/Controllers/CacheController.js | 129 -- lib/Controllers/DatabaseController.js | 1167 -------------- lib/Controllers/FilesController.js | 142 -- lib/Controllers/HooksController.js | 268 ---- lib/Controllers/LiveQueryController.js | 68 - lib/Controllers/LoggerController.js | 276 ---- lib/Controllers/PushController.js | 167 -- lib/Controllers/SchemaCache.js | 121 -- lib/Controllers/SchemaController.js | 1114 ------------- lib/Controllers/UserController.js | 318 ---- lib/LiveQuery/Client.js | 158 -- lib/LiveQuery/Id.js | 34 - lib/LiveQuery/ParseCloudCodePublisher.js | 67 - lib/LiveQuery/ParseLiveQueryServer.js | 821 ---------- lib/LiveQuery/ParsePubSub.js | 45 - lib/LiveQuery/ParseWebSocketServer.js | 72 - lib/LiveQuery/QueryTools.js | 298 ---- lib/LiveQuery/RequestSchema.js | 145 -- lib/LiveQuery/SessionTokenCache.js | 78 - lib/LiveQuery/Subscription.js | 68 - lib/LiveQuery/equalObjects.js | 52 - lib/ParseMessageQueue.js | 30 - lib/ParseServer.js | 514 ------ lib/ParseServerRESTController.js | 109 -- lib/PromiseRouter.js | 302 ---- lib/Push/PushQueue.js | 92 -- lib/Push/PushWorker.js | 144 -- lib/Push/utils.js | 41 - lib/RestQuery.js | 989 ------------ lib/RestWrite.js | 1104 ------------- lib/Routers/AnalyticsRouter.js | 51 - lib/Routers/ClassesRouter.js | 281 ---- lib/Routers/CloudCodeRouter.js | 54 - lib/Routers/FeaturesRouter.js | 93 -- lib/Routers/FilesRouter.js | 234 --- lib/Routers/FunctionsRouter.js | 205 --- lib/Routers/GlobalConfigRouter.js | 79 - lib/Routers/HooksRouter.js | 155 -- lib/Routers/IAPValidationRouter.js | 147 -- lib/Routers/InstallationsRouter.js | 113 -- lib/Routers/LogsRouter.js | 96 -- lib/Routers/PublicAPIRouter.js | 285 ---- lib/Routers/PurgeRouter.js | 64 - lib/Routers/PushRouter.js | 104 -- lib/Routers/RolesRouter.js | 89 - lib/Routers/SchemasRouter.js | 125 -- lib/Routers/SessionsRouter.js | 167 -- lib/Routers/UsersRouter.js | 367 ----- lib/StatusHandler.js | 263 --- lib/TestUtils.js | 27 - lib/batch.js | 98 -- lib/cache.js | 11 - .../definitions/parse-live-query-server.js | 45 - lib/cli/definitions/parse-server.js | 257 --- lib/cli/parse-live-query-server.js | 28 - lib/cli/parse-server.js | 169 -- lib/cli/utils/commander.js | 150 -- lib/cli/utils/parsers.js | 80 - lib/cli/utils/runner.js | 49 - lib/cloud-code/HTTPResponse.js | 65 - lib/cloud-code/Parse.Cloud.js | 76 - lib/cloud-code/httpRequest.js | 107 -- lib/cryptoUtils.js | 59 - lib/defaults.js | 44 - lib/deprecated.js | 11 - lib/index.js | 69 - lib/logger.js | 46 - lib/middlewares.js | 310 ---- lib/password.js | 29 - lib/requiredParameter.js | 9 - lib/rest.js | 157 -- lib/triggers.js | 437 ----- lib/vendor/README.md | 8 - lib/vendor/mongodbUrl.js | 928 ----------- 129 files changed, 1 insertion(+), 22126 deletions(-) delete mode 100644 lib/AccountLockout.js delete mode 100644 lib/Adapters/AdapterLoader.js delete mode 100644 lib/Adapters/Analytics/AnalyticsAdapter.js delete mode 100644 lib/Adapters/Auth/AuthAdapter.js delete mode 100644 lib/Adapters/Auth/OAuth1Client.js delete mode 100644 lib/Adapters/Auth/facebook.js delete mode 100644 lib/Adapters/Auth/github.js delete mode 100644 lib/Adapters/Auth/google.js delete mode 100755 lib/Adapters/Auth/index.js delete mode 100644 lib/Adapters/Auth/instagram.js delete mode 100644 lib/Adapters/Auth/janraincapture.js delete mode 100644 lib/Adapters/Auth/janrainengage.js delete mode 100644 lib/Adapters/Auth/linkedin.js delete mode 100644 lib/Adapters/Auth/meetup.js delete mode 100644 lib/Adapters/Auth/qq.js delete mode 100644 lib/Adapters/Auth/spotify.js delete mode 100644 lib/Adapters/Auth/twitter.js delete mode 100644 lib/Adapters/Auth/vkontakte.js delete mode 100644 lib/Adapters/Auth/wechat.js delete mode 100644 lib/Adapters/Auth/weibo.js delete mode 100644 lib/Adapters/Cache/CacheAdapter.js delete mode 100644 lib/Adapters/Cache/InMemoryCache.js delete mode 100644 lib/Adapters/Cache/InMemoryCacheAdapter.js delete mode 100644 lib/Adapters/Cache/NullCacheAdapter.js delete mode 100644 lib/Adapters/Cache/RedisCacheAdapter.js delete mode 100644 lib/Adapters/Email/MailAdapter.js delete mode 100644 lib/Adapters/Files/FilesAdapter.js delete mode 100644 lib/Adapters/Files/GridStoreAdapter.js delete mode 100644 lib/Adapters/Logger/LoggerAdapter.js delete mode 100644 lib/Adapters/Logger/WinstonLogger.js delete mode 100644 lib/Adapters/Logger/WinstonLoggerAdapter.js delete mode 100644 lib/Adapters/MessageQueue/EventEmitterMQ.js delete mode 100644 lib/Adapters/PubSub/EventEmitterPubSub.js delete mode 100644 lib/Adapters/PubSub/RedisPubSub.js delete mode 100644 lib/Adapters/Push/PushAdapter.js delete mode 100644 lib/Adapters/Storage/Mongo/MongoCollection.js delete mode 100644 lib/Adapters/Storage/Mongo/MongoSchemaCollection.js delete mode 100644 lib/Adapters/Storage/Mongo/MongoStorageAdapter.js delete mode 100644 lib/Adapters/Storage/Mongo/MongoTransform.js delete mode 100644 lib/Adapters/Storage/Postgres/PostgresClient.js delete mode 100644 lib/Adapters/Storage/Postgres/PostgresConfigParser.js delete mode 100644 lib/Adapters/Storage/Postgres/PostgresStorageAdapter.js delete mode 100644 lib/Adapters/Storage/Postgres/sql/array/add-unique.sql delete mode 100644 lib/Adapters/Storage/Postgres/sql/array/add.sql delete mode 100644 lib/Adapters/Storage/Postgres/sql/array/contains-all.sql delete mode 100644 lib/Adapters/Storage/Postgres/sql/array/contains.sql delete mode 100644 lib/Adapters/Storage/Postgres/sql/array/remove.sql delete mode 100644 lib/Adapters/Storage/Postgres/sql/index.js delete mode 100644 lib/Adapters/Storage/Postgres/sql/misc/json-object-set-keys.sql delete mode 100644 lib/Auth.js delete mode 100644 lib/ClientSDK.js delete mode 100644 lib/Config.js delete mode 100644 lib/Controllers/AdaptableController.js delete mode 100644 lib/Controllers/AnalyticsController.js delete mode 100644 lib/Controllers/CacheController.js delete mode 100644 lib/Controllers/DatabaseController.js delete mode 100644 lib/Controllers/FilesController.js delete mode 100644 lib/Controllers/HooksController.js delete mode 100644 lib/Controllers/LiveQueryController.js delete mode 100644 lib/Controllers/LoggerController.js delete mode 100644 lib/Controllers/PushController.js delete mode 100644 lib/Controllers/SchemaCache.js delete mode 100644 lib/Controllers/SchemaController.js delete mode 100644 lib/Controllers/UserController.js delete mode 100644 lib/LiveQuery/Client.js delete mode 100644 lib/LiveQuery/Id.js delete mode 100644 lib/LiveQuery/ParseCloudCodePublisher.js delete mode 100644 lib/LiveQuery/ParseLiveQueryServer.js delete mode 100644 lib/LiveQuery/ParsePubSub.js delete mode 100644 lib/LiveQuery/ParseWebSocketServer.js delete mode 100644 lib/LiveQuery/QueryTools.js delete mode 100644 lib/LiveQuery/RequestSchema.js delete mode 100644 lib/LiveQuery/SessionTokenCache.js delete mode 100644 lib/LiveQuery/Subscription.js delete mode 100644 lib/LiveQuery/equalObjects.js delete mode 100644 lib/ParseMessageQueue.js delete mode 100644 lib/ParseServer.js delete mode 100644 lib/ParseServerRESTController.js delete mode 100644 lib/PromiseRouter.js delete mode 100644 lib/Push/PushQueue.js delete mode 100644 lib/Push/PushWorker.js delete mode 100644 lib/Push/utils.js delete mode 100644 lib/RestQuery.js delete mode 100644 lib/RestWrite.js delete mode 100644 lib/Routers/AnalyticsRouter.js delete mode 100644 lib/Routers/ClassesRouter.js delete mode 100644 lib/Routers/CloudCodeRouter.js delete mode 100644 lib/Routers/FeaturesRouter.js delete mode 100644 lib/Routers/FilesRouter.js delete mode 100644 lib/Routers/FunctionsRouter.js delete mode 100644 lib/Routers/GlobalConfigRouter.js delete mode 100644 lib/Routers/HooksRouter.js delete mode 100644 lib/Routers/IAPValidationRouter.js delete mode 100644 lib/Routers/InstallationsRouter.js delete mode 100644 lib/Routers/LogsRouter.js delete mode 100644 lib/Routers/PublicAPIRouter.js delete mode 100644 lib/Routers/PurgeRouter.js delete mode 100644 lib/Routers/PushRouter.js delete mode 100644 lib/Routers/RolesRouter.js delete mode 100644 lib/Routers/SchemasRouter.js delete mode 100644 lib/Routers/SessionsRouter.js delete mode 100644 lib/Routers/UsersRouter.js delete mode 100644 lib/StatusHandler.js delete mode 100644 lib/TestUtils.js delete mode 100644 lib/batch.js delete mode 100644 lib/cache.js delete mode 100644 lib/cli/definitions/parse-live-query-server.js delete mode 100644 lib/cli/definitions/parse-server.js delete mode 100644 lib/cli/parse-live-query-server.js delete mode 100755 lib/cli/parse-server.js delete mode 100644 lib/cli/utils/commander.js delete mode 100644 lib/cli/utils/parsers.js delete mode 100644 lib/cli/utils/runner.js delete mode 100644 lib/cloud-code/HTTPResponse.js delete mode 100644 lib/cloud-code/Parse.Cloud.js delete mode 100644 lib/cloud-code/httpRequest.js delete mode 100644 lib/cryptoUtils.js delete mode 100644 lib/defaults.js delete mode 100644 lib/deprecated.js delete mode 100644 lib/index.js delete mode 100644 lib/logger.js delete mode 100644 lib/middlewares.js delete mode 100644 lib/password.js delete mode 100644 lib/requiredParameter.js delete mode 100644 lib/rest.js delete mode 100644 lib/triggers.js delete mode 100644 lib/vendor/README.md delete mode 100644 lib/vendor/mongodbUrl.js diff --git a/.gitignore b/.gitignore index 8f1edaabbf..4e4ee21cae 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,7 @@ node_modules .vscode # Babel.js -#lib/ +lib/ # cache folder .cache diff --git a/lib/AccountLockout.js b/lib/AccountLockout.js deleted file mode 100644 index 6dc8a469d9..0000000000 --- a/lib/AccountLockout.js +++ /dev/null @@ -1,199 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.AccountLockout = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // This class handles the Account Lockout Policy settings. - - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var AccountLockout = exports.AccountLockout = function () { - function AccountLockout(user, config) { - _classCallCheck(this, AccountLockout); - - this._user = user; - this._config = config; - } - - /** - * set _failed_login_count to value - */ - - - _createClass(AccountLockout, [{ - key: '_setFailedLoginCount', - value: function _setFailedLoginCount(value) { - var query = { - username: this._user.username - }; - - var updateFields = { - _failed_login_count: value - }; - - return this._config.database.update('_User', query, updateFields); - } - - /** - * check if the _failed_login_count field has been set - */ - - }, { - key: '_isFailedLoginCountSet', - value: function _isFailedLoginCountSet() { - var query = { - username: this._user.username, - _failed_login_count: { $exists: true } - }; - - return this._config.database.find('_User', query).then(function (users) { - if (Array.isArray(users) && users.length > 0) { - return true; - } else { - return false; - } - }); - } - - /** - * if _failed_login_count is NOT set then set it to 0 - * else do nothing - */ - - }, { - key: '_initFailedLoginCount', - value: function _initFailedLoginCount() { - var _this = this; - - return this._isFailedLoginCountSet().then(function (failedLoginCountIsSet) { - if (!failedLoginCountIsSet) { - return _this._setFailedLoginCount(0); - } - }); - } - - /** - * increment _failed_login_count by 1 - */ - - }, { - key: '_incrementFailedLoginCount', - value: function _incrementFailedLoginCount() { - var query = { - username: this._user.username - }; - - var updateFields = { _failed_login_count: { __op: 'Increment', amount: 1 } }; - - return this._config.database.update('_User', query, updateFields); - } - - /** - * if the failed login count is greater than the threshold - * then sets lockout expiration to 'currenttime + accountPolicy.duration', i.e., account is locked out for the next 'accountPolicy.duration' minutes - * else do nothing - */ - - }, { - key: '_setLockoutExpiration', - value: function _setLockoutExpiration() { - var query = { - username: this._user.username, - _failed_login_count: { $gte: this._config.accountLockout.threshold } - }; - - var now = new Date(); - - var updateFields = { - _account_lockout_expires_at: _node2.default._encode(new Date(now.getTime() + this._config.accountLockout.duration * 60 * 1000)) - }; - - return this._config.database.update('_User', query, updateFields).catch(function (err) { - 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 - } - }); - } - - /** - * if _account_lockout_expires_at > current_time and _failed_login_count > threshold - * reject with account locked error - * else - * resolve - */ - - }, { - key: '_notLocked', - value: function _notLocked() { - var _this2 = this; - - var query = { - username: this._user.username, - _account_lockout_expires_at: { $gt: _node2.default._encode(new Date()) }, - _failed_login_count: { $gte: this._config.accountLockout.threshold } - }; - - return this._config.database.find('_User', query).then(function (users) { - if (Array.isArray(users) && users.length > 0) { - throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Your account is locked due to multiple failed login attempts. Please try again after ' + _this2._config.accountLockout.duration + ' minute(s)'); - } - }); - } - - /** - * set and/or increment _failed_login_count - * if _failed_login_count > threshold - * set the _account_lockout_expires_at to current_time + accountPolicy.duration - * else - * do nothing - */ - - }, { - key: '_handleFailedLoginAttempt', - value: function _handleFailedLoginAttempt() { - var _this3 = this; - - return this._initFailedLoginCount().then(function () { - return _this3._incrementFailedLoginCount(); - }).then(function () { - return _this3._setLockoutExpiration(); - }); - } - - /** - * handle login attempt if the Account Lockout Policy is enabled - */ - - }, { - key: 'handleLoginAttempt', - value: function handleLoginAttempt(loginSuccessful) { - var _this4 = this; - - if (!this._config.accountLockout) { - return Promise.resolve(); - } - return this._notLocked().then(function () { - if (loginSuccessful) { - return _this4._setFailedLoginCount(0); - } else { - return _this4._handleFailedLoginAttempt(); - } - }); - } - }]); - - return AccountLockout; -}(); - -exports.default = AccountLockout; \ No newline at end of file diff --git a/lib/Adapters/AdapterLoader.js b/lib/Adapters/AdapterLoader.js deleted file mode 100644 index ffdf98fd95..0000000000 --- a/lib/Adapters/AdapterLoader.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.loadAdapter = loadAdapter; -function loadAdapter(adapter, defaultAdapter, options) { - if (!adapter) { - if (!defaultAdapter) { - return options; - } - // Load from the default adapter when no adapter is set - return loadAdapter(defaultAdapter, undefined, options); - } else if (typeof adapter === "function") { - try { - return adapter(options); - } catch (e) { - if (e.name === 'TypeError') { - var Adapter = adapter; - return new Adapter(options); - } else { - throw e; - } - } - } else if (typeof adapter === "string") { - /* eslint-disable */ - adapter = require(adapter); - // If it's define as a module, get the default - if (adapter.default) { - adapter = adapter.default; - } - return loadAdapter(adapter, undefined, options); - } else if (adapter.module) { - return loadAdapter(adapter.module, undefined, adapter.options); - } else if (adapter.class) { - return loadAdapter(adapter.class, undefined, adapter.options); - } else if (adapter.adapter) { - return loadAdapter(adapter.adapter, undefined, adapter.options); - } - // return the adapter as provided - return adapter; -} - -exports.default = loadAdapter; \ No newline at end of file diff --git a/lib/Adapters/Analytics/AnalyticsAdapter.js b/lib/Adapters/Analytics/AnalyticsAdapter.js deleted file mode 100644 index b3d965a47e..0000000000 --- a/lib/Adapters/Analytics/AnalyticsAdapter.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -/*eslint no-unused-vars: "off"*/ -var AnalyticsAdapter = exports.AnalyticsAdapter = function () { - function AnalyticsAdapter() { - _classCallCheck(this, AnalyticsAdapter); - } - - _createClass(AnalyticsAdapter, [{ - key: "appOpened", - - - /* - @param parameters: the analytics request body, analytics info will be in the dimensions property - @param req: the original http request - */ - value: function appOpened(parameters, req) { - return Promise.resolve({}); - } - - /* - @param eventName: the name of the custom eventName - @param parameters: the analytics request body, analytics info will be in the dimensions property - @param req: the original http request - */ - - }, { - key: "trackEvent", - value: function trackEvent(eventName, parameters, req) { - return Promise.resolve({}); - } - }]); - - return AnalyticsAdapter; -}(); - -exports.default = AnalyticsAdapter; \ No newline at end of file diff --git a/lib/Adapters/Auth/AuthAdapter.js b/lib/Adapters/Auth/AuthAdapter.js deleted file mode 100644 index 5685bd02c0..0000000000 --- a/lib/Adapters/Auth/AuthAdapter.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -/*eslint no-unused-vars: "off"*/ -var AuthAdapter = exports.AuthAdapter = function () { - function AuthAdapter() { - _classCallCheck(this, AuthAdapter); - } - - _createClass(AuthAdapter, [{ - key: "validateAppId", - - - /* - @param appIds: the specified app ids in the configuration - @param authData: the client provided authData - @returns a promise that resolves if the applicationId is valid - */ - value: function validateAppId(appIds, authData) { - return Promise.resolve({}); - } - - /* - @param authData: the client provided authData - @param options: additional options - */ - - }, { - key: "validateAuthData", - value: function validateAuthData(authData, options) { - return Promise.resolve({}); - } - }]); - - return AuthAdapter; -}(); - -exports.default = AuthAdapter; \ No newline at end of file diff --git a/lib/Adapters/Auth/OAuth1Client.js b/lib/Adapters/Auth/OAuth1Client.js deleted file mode 100644 index 4938a47fcb..0000000000 --- a/lib/Adapters/Auth/OAuth1Client.js +++ /dev/null @@ -1,222 +0,0 @@ -'use strict'; - -var https = require('https'), - crypto = require('crypto'); -var Parse = require('parse/node').Parse; - -var OAuth = function OAuth(options) { - if (!options) { - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'No options passed to OAuth'); - } - this.consumer_key = options.consumer_key; - this.consumer_secret = options.consumer_secret; - this.auth_token = options.auth_token; - this.auth_token_secret = options.auth_token_secret; - this.host = options.host; - this.oauth_params = options.oauth_params || {}; -}; - -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); - }); - }).on('error', function () { - reject('Failed to make an OAuth request'); - }); - if (request.body) { - httpRequest.write(request.body); - } - httpRequest.end(); - }); -}; - -OAuth.prototype.buildRequest = function (method, path, params, body) { - if (path.indexOf("/") != 0) { - path = "/" + path; - } - if (params && Object.keys(params).length > 0) { - path += "?" + OAuth.buildParameterString(params); - } - - var request = { - 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; - } - - 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); -}; - -OAuth.prototype.post = function (path, params, body) { - return this.send("POST", path, params, body); -}; - -/* - Proper string %escape encoding -*/ -OAuth.encode = function (str) { - // discuss at: http://phpjs.org/functions/rawurlencode/ - // original by: Brett Zamir (http://brett-zamir.me) - // input by: travc - // input by: Brett Zamir (http://brett-zamir.me) - // input by: Michael Grier - // input by: Ratheous - // bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) - // bugfixed by: Brett Zamir (http://brett-zamir.me) - // bugfixed by: Joris - // reimplemented by: Brett Zamir (http://brett-zamir.me) - // reimplemented by: Brett Zamir (http://brett-zamir.me) - // note: This reflects PHP 5.3/6.0+ behavior - // note: Please be aware that this function expects to encode into UTF-8 encoded strings, as found on - // note: pages served as UTF-8 - // example 1: rawurlencode('Kevin van Zonneveld!'); - // returns 1: 'Kevin%20van%20Zonneveld%21' - // example 2: rawurlencode('http://kevin.vanzonneveld.net/'); - // returns 2: 'http%3A%2F%2Fkevin.vanzonneveld.net%2F' - // 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(); - - // 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. - return encodeURIComponent(str).replace(/!/g, '%21').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/\*/g, '%2A'); -}; - -OAuth.signatureMethod = "HMAC-SHA1"; -OAuth.version = "1.0"; - -/* - Generate a nonce -*/ -OAuth.nonce = function () { - var text = ""; - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - - for (var i = 0; i < 30; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - }return text; -}; - -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 ""; -}; - -/* - Build the signature string from the object -*/ - -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.signRequest = function (request, oauth_parameters, consumer_secret, auth_token_secret) { - oauth_parameters = oauth_parameters || {}; - - // Set default values - if (!oauth_parameters.oauth_nonce) { - oauth_parameters.oauth_nonce = OAuth.nonce(); - } - if (!oauth_parameters.oauth_timestamp) { - oauth_parameters.oauth_timestamp = Math.floor(new Date().getTime() / 1000); - } - if (!oauth_parameters.oauth_signature_method) { - oauth_parameters.oauth_signature_method = OAuth.signatureMethod; - } - if (!oauth_parameters.oauth_version) { - oauth_parameters.oauth_version = OAuth.version; - } - - if (!auth_token_secret) { - auth_token_secret = ""; - } - // Force GET method if unset - if (!request.method) { - 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) { - var parameters = parametersToMerge[i]; - for (var k in parameters) { - signatureParams[k] = parameters[k]; - } - } - - // Create a string based on the parameters - var parameterString = OAuth.buildParameterString(signatureParams); - - // Build the signature string - var url = "https://" + request.host + "" + request.path; - - 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 signature = OAuth.signature(signatureString, signatureKey); - - // Set the signature in the params - oauth_parameters.oauth_signature = signature; - 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(", "); - - request.headers.Authorization = 'OAuth ' + authHeader; - - // Set the content type header - request.headers["Content-Type"] = "application/x-www-form-urlencoded"; - return request; -}; - -module.exports = OAuth; \ No newline at end of file diff --git a/lib/Adapters/Auth/facebook.js b/lib/Adapters/Auth/facebook.js deleted file mode 100644 index d08c6bb43d..0000000000 --- a/lib/Adapters/Auth/facebook.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -// Helper functions for accessing the Facebook Graph API. -var https = require('https'); -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(function (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. -function validateAppId(appIds, authData) { - var access_token = authData.access_token; - if (!appIds.length) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Facebook auth is not configured.'); - } - return graphRequest('app?access_token=' + access_token).then(function (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. -function graphRequest(path) { - return new Promise(function (resolve, reject) { - https.get('https://graph.facebook.com/v2.5/' + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with Facebook.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/github.js b/lib/Adapters/Auth/github.js deleted file mode 100644 index 495958c6ac..0000000000 --- a/lib/Adapters/Auth/github.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -// Helper functions for accessing the github API. -var https = require('https'); -var Parse = require('parse/node').Parse; - -// Returns a promise that fulfills iff this user id is valid. -function validateAuthData(authData) { - return request('user', authData.access_token).then(function (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. -function validateAppId() { - return Promise.resolve(); -} - -// A promisey wrapper for api requests -function request(path, access_token) { - return new Promise(function (resolve, reject) { - https.get({ - host: 'api.github.com', - path: '/' + path, - headers: { - 'Authorization': 'bearer ' + access_token, - 'User-Agent': 'parse-server' - } - }, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with Github.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/google.js b/lib/Adapters/Auth/google.js deleted file mode 100644 index 1a43b2d355..0000000000 --- a/lib/Adapters/Auth/google.js +++ /dev/null @@ -1,70 +0,0 @@ -'use strict'; - -// Helper functions for accessing the google API. -var https = require('https'); -var Parse = require('parse/node').Parse; - -function validateIdToken(id, token) { - return request("tokeninfo?id_token=" + token).then(function (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 request("tokeninfo?access_token=" + token).then(function (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. -function validateAuthData(authData) { - if (authData.id_token) { - return validateIdToken(authData.id, authData.id_token); - } else { - return validateAuthToken(authData.id, authData.access_token).then(function () { - // Validation with auth token worked - return; - }, function () { - // Try with the id_token param - return validateIdToken(authData.id, authData.access_token); - }); - } -} - -// Returns a promise that fulfills if this app id is valid. -function validateAppId() { - return Promise.resolve(); -} - -// A promisey wrapper for api requests -function request(path) { - return new Promise(function (resolve, reject) { - https.get("https://www.googleapis.com/oauth2/v3/" + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with Google.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/index.js b/lib/Adapters/Auth/index.js deleted file mode 100755 index 84d5b12bf3..0000000000 --- a/lib/Adapters/Auth/index.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict'; - -var _AdapterLoader = require('../AdapterLoader'); - -var _AdapterLoader2 = _interopRequireDefault(_AdapterLoader); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var facebook = require('./facebook'); -var instagram = require("./instagram"); -var linkedin = require("./linkedin"); -var meetup = require("./meetup"); -var google = require("./google"); -var github = require("./github"); -var twitter = require("./twitter"); -var spotify = require("./spotify"); -var digits = require("./twitter"); // digits tokens are validated by twitter -var janrainengage = require("./janrainengage"); -var janraincapture = require("./janraincapture"); -var vkontakte = require("./vkontakte"); -var qq = require("./qq"); -var wechat = require("./wechat"); -var weibo = require("./weibo"); - -var anonymous = { - validateAuthData: function validateAuthData() { - return Promise.resolve(); - }, - validateAppId: function validateAppId() { - return Promise.resolve(); - } -}; - -var providers = { - facebook: facebook, - instagram: instagram, - linkedin: linkedin, - meetup: meetup, - google: google, - github: github, - twitter: twitter, - spotify: spotify, - anonymous: anonymous, - digits: digits, - janrainengage: janrainengage, - janraincapture: janraincapture, - vkontakte: vkontakte, - qq: qq, - wechat: wechat, - weibo: weibo -}; - -function authDataValidator(adapter, appIds, options) { - return function (authData) { - return adapter.validateAuthData(authData, options).then(function () { - if (appIds) { - return adapter.validateAppId(appIds, authData, options); - } - return Promise.resolve(); - }); - }; -} - -function loadAuthAdapter(provider, authOptions) { - var defaultAdapter = providers[provider]; - var adapter = Object.assign({}, defaultAdapter); - var providerOptions = authOptions[provider]; - - if (!defaultAdapter && !providerOptions) { - return; - } - - var appIds = providerOptions ? providerOptions.appIds : undefined; - - // Try the configuration methods - if (providerOptions) { - var optionalAdapter = (0, _AdapterLoader2.default)(providerOptions, undefined, providerOptions); - if (optionalAdapter) { - ['validateAuthData', 'validateAppId'].forEach(function (key) { - if (optionalAdapter[key]) { - adapter[key] = optionalAdapter[key]; - } - }); - } - } - - if (!adapter.validateAuthData || !adapter.validateAppId) { - return; - } - - return { adapter: adapter, appIds: appIds, providerOptions: providerOptions }; -} - -module.exports = function () { - var authOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - var enableAnonymousUsers = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; - - var _enableAnonymousUsers = enableAnonymousUsers; - var setEnableAnonymousUsers = function setEnableAnonymousUsers(enable) { - _enableAnonymousUsers = enable; - }; - // To handle the test cases on configuration - var getValidatorForProvider = function getValidatorForProvider(provider) { - - if (provider === 'anonymous' && !_enableAnonymousUsers) { - return; - } - - var _loadAuthAdapter = loadAuthAdapter(provider, authOptions), - adapter = _loadAuthAdapter.adapter, - appIds = _loadAuthAdapter.appIds, - providerOptions = _loadAuthAdapter.providerOptions; - - return authDataValidator(adapter, appIds, providerOptions); - }; - - return Object.freeze({ - getValidatorForProvider: getValidatorForProvider, - setEnableAnonymousUsers: setEnableAnonymousUsers - }); -}; - -module.exports.loadAuthAdapter = loadAuthAdapter; \ No newline at end of file diff --git a/lib/Adapters/Auth/instagram.js b/lib/Adapters/Auth/instagram.js deleted file mode 100644 index 3bf9e5ff56..0000000000 --- a/lib/Adapters/Auth/instagram.js +++ /dev/null @@ -1,43 +0,0 @@ -'use strict'; - -// Helper functions for accessing the instagram API. -var https = require('https'); -var Parse = require('parse/node').Parse; - -// Returns a promise that fulfills iff this user id is valid. -function validateAuthData(authData) { - return request("users/self/?access_token=" + authData.access_token).then(function (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.'); - }); -} - -// Returns a promise that fulfills iff this app id is valid. -function validateAppId() { - return Promise.resolve(); -} - -// A promisey wrapper for api requests -function request(path) { - return new Promise(function (resolve, reject) { - https.get("https://api.instagram.com/v1/" + path, 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 validate this access token with Instagram.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/janraincapture.js b/lib/Adapters/Auth/janraincapture.js deleted file mode 100644 index 34828ba097..0000000000 --- a/lib/Adapters/Auth/janraincapture.js +++ /dev/null @@ -1,55 +0,0 @@ -'use strict'; - -// Helper functions for accessing the Janrain Capture API. -var https = require('https'); -var Parse = require('parse/node').Parse; -var querystring = require('querystring'); - -// 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(function (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.'); - }); -} - -// Returns a promise that fulfills iff this app id is valid. -function validateAppId() { - //no-op - return Promise.resolve(); -} - -// 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 - }); - - return new Promise(function (resolve, reject) { - https.get({ - host: host, - path: '/entity?' + query_string_data - }, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - resolve(JSON.parse(data)); - }); - }).on('error', function () { - reject('Failed to validate this access token with Janrain capture.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/janrainengage.js b/lib/Adapters/Auth/janrainengage.js deleted file mode 100644 index 494c5160fa..0000000000 --- a/lib/Adapters/Auth/janrainengage.js +++ /dev/null @@ -1,73 +0,0 @@ -'use strict'; - -// Helper functions for accessing the Janrain Engage API. -var https = require('https'); -var Parse = require('parse/node').Parse; -var querystring = require('querystring'); - -// Returns a promise that fulfills iff this user id is valid. -function validateAuthData(authData, options) { - return request(options.api_key, authData.auth_token).then(function (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. -function validateAppId() { - //no-op - return Promise.resolve(); -} - -// A promisey wrapper for api requests -function request(api_key, auth_token) { - - var post_data = querystring.stringify({ - 'token': auth_token, - 'apiKey': api_key, - 'format': 'json' - }); - - var post_options = { - host: 'rpxnow.com', - path: '/api/v2/auth_info', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': post_data.length - } - }; - - return new Promise(function (resolve, reject) { - // Create the post request. - var post_req = https.request(post_options, function (res) { - var data = ''; - res.setEncoding('utf8'); - // Append data as we receive it from the Janrain engage server. - res.on('data', function (d) { - data += d; - }); - // Once we have all the data, we can parse it and return the data we want. - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }); - - post_req.write(post_data); - post_req.end(); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/linkedin.js b/lib/Adapters/Auth/linkedin.js deleted file mode 100644 index e2182baee8..0000000000 --- a/lib/Adapters/Auth/linkedin.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -// Helper functions for accessing the linkedin API. -var https = require('https'); -var Parse = require('parse/node').Parse; - -// 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(function (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. -function validateAppId() { - return Promise.resolve(); -} - -// A promisey wrapper for api requests -function request(path, access_token, is_mobile_sdk) { - var headers = { - 'Authorization': 'Bearer ' + access_token, - 'x-li-format': 'json' - }; - - if (is_mobile_sdk) { - headers['x-li-src'] = 'msdk'; - } - - return new Promise(function (resolve, reject) { - https.get({ - host: 'api.linkedin.com', - path: '/v1/' + path, - headers: headers - }, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with Linkedin.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/meetup.js b/lib/Adapters/Auth/meetup.js deleted file mode 100644 index d78b974081..0000000000 --- a/lib/Adapters/Auth/meetup.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -// Helper functions for accessing the meetup API. -var https = require('https'); -var Parse = require('parse/node').Parse; - -// Returns a promise that fulfills iff this user id is valid. -function validateAuthData(authData) { - return request('member/self', authData.access_token).then(function (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. -function validateAppId() { - return Promise.resolve(); -} - -// A promisey wrapper for api requests -function request(path, access_token) { - return new Promise(function (resolve, reject) { - https.get({ - host: 'api.meetup.com', - path: '/2/' + path, - headers: { - 'Authorization': 'bearer ' + access_token - } - }, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with Meetup.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/qq.js b/lib/Adapters/Auth/qq.js deleted file mode 100644 index 34f0781f1f..0000000000 --- a/lib/Adapters/Auth/qq.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; - -// Helper functions for accessing the qq Graph API. -var https = require('https'); -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) { - if (data && data.openid == authData.id) { - return; - } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); - }); -} - -// Returns a promise that fulfills if this app id is valid. -function validateAppId() { - return Promise.resolve(); -} - -// A promisey wrapper for qq graph requests. -function graphRequest(path) { - return new Promise(function (resolve, reject) { - https.get('https://graph.qq.com/oauth2.0/' + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - var starPos = data.indexOf("("); - var endPos = data.indexOf(")"); - if (starPos == -1 || endPos == -1) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); - } - data = data.substring(starPos + 1, endPos - 1); - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with qq.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/spotify.js b/lib/Adapters/Auth/spotify.js deleted file mode 100644 index d508f39727..0000000000 --- a/lib/Adapters/Auth/spotify.js +++ /dev/null @@ -1,62 +0,0 @@ -'use strict'; - -// Helper functions for accessing the Spotify API. -var https = require('https'); -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(function (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. -function validateAppId(appIds, authData) { - var access_token = authData.access_token; - if (!appIds.length) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Spotify auth is not configured.'); - } - return request('me', access_token).then(function (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. -function request(path, access_token) { - return new Promise(function (resolve, reject) { - https.get({ - host: 'api.spotify.com', - path: '/v1/' + path, - headers: { - 'Authorization': 'Bearer ' + access_token - } - }, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with Spotify.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/twitter.js b/lib/Adapters/Auth/twitter.js deleted file mode 100644 index 47f618a905..0000000000 --- a/lib/Adapters/Auth/twitter.js +++ /dev/null @@ -1,56 +0,0 @@ -'use strict'; - -// Helper functions for accessing the twitter API. -var OAuth = require('./OAuth1Client'); -var Parse = require('parse/node').Parse; -var logger = require('../../logger').default; - -// Returns a promise that fulfills iff this user id is valid. -function validateAuthData(authData, options) { - if (!options) { - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Twitter auth configuration missing'); - } - options = handleMultipleConfigurations(authData, options); - var client = new OAuth(options); - client.host = "api.twitter.com"; - client.auth_token = authData.auth_token; - client.auth_token_secret = authData.auth_token_secret; - - return client.get("/1.1/account/verify_credentials.json").then(function (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.'); - }); -} - -// Returns a promise that fulfills iff this app id is valid. -function validateAppId() { - return Promise.resolve(); -} - -function handleMultipleConfigurations(authData, options) { - if (Array.isArray(options)) { - var 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.'); - } - options = options.filter(function (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.'); - } - options = options[0]; - } - return options; -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData, - handleMultipleConfigurations: handleMultipleConfigurations -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/vkontakte.js b/lib/Adapters/Auth/vkontakte.js deleted file mode 100644 index 7f91583d82..0000000000 --- a/lib/Adapters/Auth/vkontakte.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -// Helper functions for accessing the vkontakte API. - -var https = require('https'); -var Parse = require('parse/node').Parse; -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) { - if (response && response && response.access_token) { - return request("api.vk.com", "method/secure.checkToken?token=" + authData.access_token + "&client_secret=" + params.appSecret + "&access_token=" + response.access_token).then(function (response) { - if (response && response.response && response.response.user_id == authData.id) { - return; - } - 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.'); - }); -} - -function vkOAuth2Request(params) { - var promise = new Parse.Promise(); - return promise.then(function () { - 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 request("oauth.vk.com", "access_token?client_id=" + params.appIds + "&client_secret=" + params.appSecret + "&v=5.59&grant_type=client_credentials"); - }); -} - -// Returns a promise that fulfills iff this app id is valid. -function validateAppId() { - return Promise.resolve(); -} - -// A promisey wrapper for api requests -function request(host, path) { - return new Promise(function (resolve, reject) { - https.get("https://" + host + "/" + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with Vk.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/wechat.js b/lib/Adapters/Auth/wechat.js deleted file mode 100644 index a414dfaf8b..0000000000 --- a/lib/Adapters/Auth/wechat.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -// Helper functions for accessing the WeChat Graph API. -var https = require('https'); -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) { - if (data.errcode == 0) { - return; - } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'qq auth is invalid for this user.'); - }); -} - -// Returns a promise that fulfills if this app id is valid. -function validateAppId() { - return Promise.resolve(); -} - -// A promisey wrapper for WeChat graph requests. -function graphRequest(path) { - return new Promise(function (resolve, reject) { - https.get('https://api.weixin.qq.com/sns/' + path, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - }).on('error', function () { - reject('Failed to validate this access token with weixin.'); - }); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Auth/weibo.js b/lib/Adapters/Auth/weibo.js deleted file mode 100644 index f4adddbfb0..0000000000 --- a/lib/Adapters/Auth/weibo.js +++ /dev/null @@ -1,66 +0,0 @@ -'use strict'; - -// Helper functions for accessing the weibo Graph API. -var https = require('https'); -var Parse = require('parse/node').Parse; -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) { - if (data && data.uid == authData.id) { - return; - } - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'weibo auth is invalid for this user.'); - }); -} - -// Returns a promise that fulfills if this app id is valid. -function validateAppId() { - return Promise.resolve(); -} - -// A promisey wrapper for weibo graph requests. -function graphRequest(access_token) { - return new Promise(function (resolve, reject) { - var postData = querystring.stringify({ - "access_token": access_token - }); - var options = { - hostname: 'api.weibo.com', - path: '/oauth2/get_token_info', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(postData) - } - }; - var req = https.request(options, function (res) { - var data = ''; - res.on('data', function (chunk) { - data += chunk; - }); - res.on('end', function () { - try { - data = JSON.parse(data); - } catch (e) { - return reject(e); - } - resolve(data); - }); - res.on('error', function () { - reject('Failed to validate this access token with weibo.'); - }); - }); - req.on('error', function () { - reject('Failed to validate this access token with weibo.'); - }); - req.write(postData); - req.end(); - }); -} - -module.exports = { - validateAppId: validateAppId, - validateAuthData: validateAuthData -}; \ No newline at end of file diff --git a/lib/Adapters/Cache/CacheAdapter.js b/lib/Adapters/Cache/CacheAdapter.js deleted file mode 100644 index 343697dac0..0000000000 --- a/lib/Adapters/Cache/CacheAdapter.js +++ /dev/null @@ -1,57 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -/*eslint no-unused-vars: "off"*/ -var CacheAdapter = exports.CacheAdapter = function () { - function CacheAdapter() { - _classCallCheck(this, CacheAdapter); - } - - _createClass(CacheAdapter, [{ - key: "get", - - /** - * Get a value in the cache - * @param key Cache key to get - * @return Promise that will eventually resolve to the value in the cache. - */ - value: function get(key) {} - - /** - * Set a value in the cache - * @param key Cache key to set - * @param value Value to set the key - * @param ttl Optional TTL - */ - - }, { - key: "put", - value: function put(key, value, ttl) {} - - /** - * Remove a value from the cache. - * @param key Cache key to remove - */ - - }, { - key: "del", - value: function del(key) {} - - /** - * Empty a cache - */ - - }, { - key: "clear", - value: function clear() {} - }]); - - return CacheAdapter; -}(); \ No newline at end of file diff --git a/lib/Adapters/Cache/InMemoryCache.js b/lib/Adapters/Cache/InMemoryCache.js deleted file mode 100644 index 6852a37aaf..0000000000 --- a/lib/Adapters/Cache/InMemoryCache.js +++ /dev/null @@ -1,88 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var DEFAULT_CACHE_TTL = 5 * 1000; - -var InMemoryCache = exports.InMemoryCache = function () { - function InMemoryCache(_ref) { - var _ref$ttl = _ref.ttl, - ttl = _ref$ttl === undefined ? DEFAULT_CACHE_TTL : _ref$ttl; - - _classCallCheck(this, InMemoryCache); - - this.ttl = ttl; - this.cache = Object.create(null); - } - - _createClass(InMemoryCache, [{ - key: "get", - value: function get(key) { - var record = this.cache[key]; - if (record == null) { - return null; - } - - // Has Record and isnt expired - if (isNaN(record.expire) || record.expire >= Date.now()) { - return record.value; - } - - // Record has expired - delete this.cache[key]; - return null; - } - }, { - key: "put", - value: function put(key, value) { - var _this = this; - - var ttl = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.ttl; - - if (ttl < 0 || isNaN(ttl)) { - ttl = NaN; - } - - var record = { - value: value, - expire: ttl + Date.now() - }; - - if (!isNaN(record.expire)) { - record.timeout = setTimeout(function () { - _this.del(key); - }, ttl); - } - - this.cache[key] = record; - } - }, { - key: "del", - value: function del(key) { - var record = this.cache[key]; - if (record == null) { - return; - } - - if (record.timeout) { - clearTimeout(record.timeout); - } - delete this.cache[key]; - } - }, { - key: "clear", - value: function clear() { - this.cache = Object.create(null); - } - }]); - - return InMemoryCache; -}(); - -exports.default = InMemoryCache; \ No newline at end of file diff --git a/lib/Adapters/Cache/InMemoryCacheAdapter.js b/lib/Adapters/Cache/InMemoryCacheAdapter.js deleted file mode 100644 index 3b04e8a332..0000000000 --- a/lib/Adapters/Cache/InMemoryCacheAdapter.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.InMemoryCacheAdapter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _InMemoryCache = require('./InMemoryCache'); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var InMemoryCacheAdapter = exports.InMemoryCacheAdapter = function () { - function InMemoryCacheAdapter(ctx) { - _classCallCheck(this, InMemoryCacheAdapter); - - this.cache = new _InMemoryCache.InMemoryCache(ctx); - } - - _createClass(InMemoryCacheAdapter, [{ - key: 'get', - value: function get(key) { - var _this = this; - - return new Promise(function (resolve) { - var record = _this.cache.get(key); - if (record == null) { - return resolve(null); - } - - return resolve(JSON.parse(record)); - }); - } - }, { - key: 'put', - value: function put(key, value, ttl) { - this.cache.put(key, JSON.stringify(value), ttl); - return Promise.resolve(); - } - }, { - key: 'del', - value: function del(key) { - this.cache.del(key); - return Promise.resolve(); - } - }, { - key: 'clear', - value: function clear() { - this.cache.clear(); - return Promise.resolve(); - } - }]); - - return InMemoryCacheAdapter; -}(); - -exports.default = InMemoryCacheAdapter; \ No newline at end of file diff --git a/lib/Adapters/Cache/NullCacheAdapter.js b/lib/Adapters/Cache/NullCacheAdapter.js deleted file mode 100644 index 6eca7d8001..0000000000 --- a/lib/Adapters/Cache/NullCacheAdapter.js +++ /dev/null @@ -1,43 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var NullCacheAdapter = exports.NullCacheAdapter = function () { - function NullCacheAdapter() { - _classCallCheck(this, NullCacheAdapter); - } - - _createClass(NullCacheAdapter, [{ - key: "get", - value: function get() { - return new Promise(function (resolve) { - return resolve(null); - }); - } - }, { - key: "put", - value: function put() { - return Promise.resolve(); - } - }, { - key: "del", - value: function del() { - return Promise.resolve(); - } - }, { - key: "clear", - value: function clear() { - return Promise.resolve(); - } - }]); - - return NullCacheAdapter; -}(); - -exports.default = NullCacheAdapter; \ No newline at end of file diff --git a/lib/Adapters/Cache/RedisCacheAdapter.js b/lib/Adapters/Cache/RedisCacheAdapter.js deleted file mode 100644 index a01463a03d..0000000000 --- a/lib/Adapters/Cache/RedisCacheAdapter.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.RedisCacheAdapter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _redis = require('redis'); - -var _redis2 = _interopRequireDefault(_redis); - -var _logger = require('../../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var DEFAULT_REDIS_TTL = 30 * 1000; // 30 seconds in milliseconds - -function debug() { - _logger2.default.debug.apply(_logger2.default, ['RedisCacheAdapter'].concat(Array.prototype.slice.call(arguments))); -} - -var RedisCacheAdapter = exports.RedisCacheAdapter = function () { - function RedisCacheAdapter(redisCtx) { - var ttl = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : DEFAULT_REDIS_TTL; - - _classCallCheck(this, RedisCacheAdapter); - - this.client = _redis2.default.createClient(redisCtx); - this.p = Promise.resolve(); - this.ttl = ttl; - } - - _createClass(RedisCacheAdapter, [{ - key: 'get', - value: function get(key) { - var _this = this; - - debug('get', key); - this.p = this.p.then(function () { - return new Promise(function (resolve) { - _this.client.get(key, function (err, res) { - debug('-> get', key, res); - if (!res) { - return resolve(null); - } - resolve(JSON.parse(res)); - }); - }); - }); - return this.p; - } - }, { - key: 'put', - value: function put(key, value) { - var _this2 = this; - - var ttl = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.ttl; - - value = JSON.stringify(value); - debug('put', key, value, ttl); - if (ttl === 0) { - return this.p; // ttl of zero is a logical no-op, but redis cannot set expire time of zero - } - if (ttl < 0 || isNaN(ttl)) { - ttl = DEFAULT_REDIS_TTL; - } - this.p = this.p.then(function () { - return new Promise(function (resolve) { - if (ttl === Infinity) { - _this2.client.set(key, value, function () { - resolve(); - }); - } else { - _this2.client.psetex(key, ttl, value, function () { - resolve(); - }); - } - }); - }); - return this.p; - } - }, { - key: 'del', - value: function del(key) { - var _this3 = this; - - debug('del', key); - this.p = this.p.then(function () { - return new Promise(function (resolve) { - _this3.client.del(key, function () { - resolve(); - }); - }); - }); - return this.p; - } - }, { - key: 'clear', - value: function clear() { - var _this4 = this; - - debug('clear'); - this.p = this.p.then(function () { - return new Promise(function (resolve) { - _this4.client.flushdb(function () { - resolve(); - }); - }); - }); - return this.p; - } - }]); - - return RedisCacheAdapter; -}(); - -exports.default = RedisCacheAdapter; \ No newline at end of file diff --git a/lib/Adapters/Email/MailAdapter.js b/lib/Adapters/Email/MailAdapter.js deleted file mode 100644 index 589aa3564c..0000000000 --- a/lib/Adapters/Email/MailAdapter.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -/*eslint no-unused-vars: "off"*/ -/* - Mail Adapter prototype - A MailAdapter should implement at least sendMail() - */ -var MailAdapter = exports.MailAdapter = function () { - function MailAdapter() { - _classCallCheck(this, MailAdapter); - } - - _createClass(MailAdapter, [{ - key: "sendMail", - - /* - * A method for sending mail - * @param options would have the parameters - * - to: the recipient - * - text: the raw text of the message - * - subject: the subject of the email - */ - value: function sendMail(options) {} - - /* You can implement those methods if you want - * to provide HTML templates etc... - */ - // sendVerificationEmail({ link, appName, user }) {} - // sendPasswordResetEmail({ link, appName, user }) {} - - }]); - - return MailAdapter; -}(); - -exports.default = MailAdapter; \ No newline at end of file diff --git a/lib/Adapters/Files/FilesAdapter.js b/lib/Adapters/Files/FilesAdapter.js deleted file mode 100644 index fc1db39af1..0000000000 --- a/lib/Adapters/Files/FilesAdapter.js +++ /dev/null @@ -1,58 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -/*eslint no-unused-vars: "off"*/ -// Files Adapter -// -// Allows you to change the file storage mechanism. -// -// Adapter classes must implement the following functions: -// * createFile(config, filename, data) -// * getFileData(config, filename) -// * getFileLocation(config, request, filename) -// -// Default is GridStoreAdapter, which requires mongo -// and for the API server to be using the DatabaseController with Mongo -// database adapter. - -var FilesAdapter = exports.FilesAdapter = function () { - function FilesAdapter() { - _classCallCheck(this, FilesAdapter); - } - - _createClass(FilesAdapter, [{ - key: "createFile", - - /* This method is responsible to store the file in order to be retrieved later by its file name - * - * @param filename the filename to save - * @param data the buffer of data from the file - * @param contentType the supposed contentType - * @discussion the contentType can be undefined if the controller was not able to determine it - * - * @return a promise that should fail if the storage didn't succeed - * - */ - value: function createFile(filename, data, contentType) {} - }, { - key: "deleteFile", - value: function deleteFile(filename) {} - }, { - key: "getFileData", - value: function getFileData(filename) {} - }, { - key: "getFileLocation", - value: function getFileLocation(config, filename) {} - }]); - - return FilesAdapter; -}(); - -exports.default = FilesAdapter; \ No newline at end of file diff --git a/lib/Adapters/Files/GridStoreAdapter.js b/lib/Adapters/Files/GridStoreAdapter.js deleted file mode 100644 index 478844771c..0000000000 --- a/lib/Adapters/Files/GridStoreAdapter.js +++ /dev/null @@ -1,114 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.GridStoreAdapter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _mongodb = require('mongodb'); - -var _FilesAdapter2 = require('./FilesAdapter'); - -var _defaults = require('../../defaults'); - -var _defaults2 = _interopRequireDefault(_defaults); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** - GridStoreAdapter - Stores files in Mongo using GridStore - Requires the database adapter to be based on mongoclient - - weak - */ - -var GridStoreAdapter = exports.GridStoreAdapter = function (_FilesAdapter) { - _inherits(GridStoreAdapter, _FilesAdapter); - - function GridStoreAdapter() { - var mongoDatabaseURI = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _defaults2.default.DefaultMongoURI; - - _classCallCheck(this, GridStoreAdapter); - - var _this = _possibleConstructorReturn(this, (GridStoreAdapter.__proto__ || Object.getPrototypeOf(GridStoreAdapter)).call(this)); - - _this._databaseURI = mongoDatabaseURI; - return _this; - } - - _createClass(GridStoreAdapter, [{ - key: '_connect', - value: function _connect() { - if (!this._connectionPromise) { - this._connectionPromise = _mongodb.MongoClient.connect(this._databaseURI); - } - return this._connectionPromise; - } - - // For a given config object, filename, and data, store a file - // Returns a promise - - }, { - key: 'createFile', - value: function createFile(filename, data) { - return this._connect().then(function (database) { - var gridStore = new _mongodb.GridStore(database, filename, 'w'); - return gridStore.open(); - }).then(function (gridStore) { - return gridStore.write(data); - }).then(function (gridStore) { - return gridStore.close(); - }); - } - }, { - key: 'deleteFile', - value: function deleteFile(filename) { - return this._connect().then(function (database) { - var gridStore = new _mongodb.GridStore(database, filename, 'r'); - return gridStore.open(); - }).then(function (gridStore) { - return gridStore.unlink(); - }).then(function (gridStore) { - return gridStore.close(); - }); - } - }, { - key: 'getFileData', - value: function getFileData(filename) { - return this._connect().then(function (database) { - return _mongodb.GridStore.exist(database, filename).then(function () { - var gridStore = new _mongodb.GridStore(database, filename, 'r'); - return gridStore.open(); - }); - }).then(function (gridStore) { - return gridStore.read(); - }); - } - }, { - key: 'getFileLocation', - value: function getFileLocation(config, filename) { - return config.mount + '/files/' + config.applicationId + '/' + encodeURIComponent(filename); - } - }, { - key: 'getFileStream', - value: function getFileStream(filename) { - return this._connect().then(function (database) { - return _mongodb.GridStore.exist(database, filename).then(function () { - var gridStore = new _mongodb.GridStore(database, filename, 'r'); - return gridStore.open(); - }); - }); - } - }]); - - return GridStoreAdapter; -}(_FilesAdapter2.FilesAdapter); - -exports.default = GridStoreAdapter; \ No newline at end of file diff --git a/lib/Adapters/Logger/LoggerAdapter.js b/lib/Adapters/Logger/LoggerAdapter.js deleted file mode 100644 index 099b1a7510..0000000000 --- a/lib/Adapters/Logger/LoggerAdapter.js +++ /dev/null @@ -1,34 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -/*eslint no-unused-vars: "off"*/ -// Logger Adapter -// -// Allows you to change the logger mechanism -// -// Adapter classes must implement the following functions: -// * log() {} -// * query(options, callback) /* optional */ -// Default is WinstonLoggerAdapter.js - -var LoggerAdapter = exports.LoggerAdapter = function () { - function LoggerAdapter(options) { - _classCallCheck(this, LoggerAdapter); - } - - _createClass(LoggerAdapter, [{ - key: "log", - value: function log(level, message) /* meta */{} - }]); - - return LoggerAdapter; -}(); - -exports.default = LoggerAdapter; \ No newline at end of file diff --git a/lib/Adapters/Logger/WinstonLogger.js b/lib/Adapters/Logger/WinstonLogger.js deleted file mode 100644 index cffce8584a..0000000000 --- a/lib/Adapters/Logger/WinstonLogger.js +++ /dev/null @@ -1,131 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.logger = undefined; -exports.configureLogger = configureLogger; -exports.addTransport = addTransport; -exports.removeTransport = removeTransport; - -var _winston = require('winston'); - -var _winston2 = _interopRequireDefault(_winston); - -var _fs = require('fs'); - -var _fs2 = _interopRequireDefault(_fs); - -var _path = require('path'); - -var _path2 = _interopRequireDefault(_path); - -var _winstonDailyRotateFile = require('winston-daily-rotate-file'); - -var _winstonDailyRotateFile2 = _interopRequireDefault(_winstonDailyRotateFile); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _defaults = require('../../defaults'); - -var _defaults2 = _interopRequireDefault(_defaults); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var logger = new _winston2.default.Logger(); -var additionalTransports = []; - -function updateTransports(options) { - var transports = Object.assign({}, logger.transports); - if (options) { - var silent = options.silent; - delete options.silent; - if (_lodash2.default.isNull(options.dirname)) { - delete transports['parse-server']; - delete transports['parse-server-error']; - } else if (!_lodash2.default.isUndefined(options.dirname)) { - transports['parse-server'] = new _winstonDailyRotateFile2.default(Object.assign({}, { - filename: 'parse-server.info', - name: 'parse-server' - }, options, { timestamp: true })); - transports['parse-server-error'] = new _winstonDailyRotateFile2.default(Object.assign({}, { - filename: 'parse-server.err', - name: 'parse-server-error' - }, options, { level: 'error', timestamp: true })); - } - - transports.console = new _winston2.default.transports.Console(Object.assign({ - colorize: true, - name: 'console', - silent: silent - }, options)); - } - // Mount the additional transports - additionalTransports.forEach(function (transport) { - transports[transport.name] = transport; - }); - logger.configure({ - transports: _lodash2.default.values(transports) - }); -} - -function configureLogger() { - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, - _ref$logsFolder = _ref.logsFolder, - logsFolder = _ref$logsFolder === undefined ? _defaults2.default.logsFolder : _ref$logsFolder, - _ref$jsonLogs = _ref.jsonLogs, - jsonLogs = _ref$jsonLogs === undefined ? _defaults2.default.jsonLogs : _ref$jsonLogs, - _ref$logLevel = _ref.logLevel, - logLevel = _ref$logLevel === undefined ? _winston2.default.level : _ref$logLevel, - _ref$verbose = _ref.verbose, - verbose = _ref$verbose === undefined ? _defaults2.default.verbose : _ref$verbose, - _ref$silent = _ref.silent, - silent = _ref$silent === undefined ? _defaults2.default.silent : _ref$silent; - - if (verbose) { - logLevel = 'verbose'; - } - - _winston2.default.level = logLevel; - var options = {}; - - if (logsFolder) { - if (!_path2.default.isAbsolute(logsFolder)) { - logsFolder = _path2.default.resolve(process.cwd(), logsFolder); - } - try { - _fs2.default.mkdirSync(logsFolder); - } catch (e) {/* */} - } - options.dirname = logsFolder; - options.level = logLevel; - options.silent = silent; - - if (jsonLogs) { - options.json = true; - options.stringify = true; - } - updateTransports(options); -} - -function addTransport(transport) { - additionalTransports.push(transport); - updateTransports(); -} - -function removeTransport(transport) { - var transportName = typeof transport == 'string' ? transport : transport.name; - var transports = Object.assign({}, logger.transports); - delete transports[transportName]; - logger.configure({ - transports: _lodash2.default.values(transports) - }); - _lodash2.default.remove(additionalTransports, function (transport) { - return transport.name === transportName; - }); -} - -exports.logger = logger; -exports.default = logger; \ No newline at end of file diff --git a/lib/Adapters/Logger/WinstonLoggerAdapter.js b/lib/Adapters/Logger/WinstonLoggerAdapter.js deleted file mode 100644 index f01d9e5145..0000000000 --- a/lib/Adapters/Logger/WinstonLoggerAdapter.js +++ /dev/null @@ -1,95 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.WinstonLoggerAdapter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _LoggerAdapter2 = require('./LoggerAdapter'); - -var _WinstonLogger = require('./WinstonLogger'); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; - -var WinstonLoggerAdapter = exports.WinstonLoggerAdapter = function (_LoggerAdapter) { - _inherits(WinstonLoggerAdapter, _LoggerAdapter); - - function WinstonLoggerAdapter(options) { - _classCallCheck(this, WinstonLoggerAdapter); - - var _this = _possibleConstructorReturn(this, (WinstonLoggerAdapter.__proto__ || Object.getPrototypeOf(WinstonLoggerAdapter)).call(this)); - - if (options) { - (0, _WinstonLogger.configureLogger)(options); - } - return _this; - } - - _createClass(WinstonLoggerAdapter, [{ - key: 'log', - value: function log() { - return _WinstonLogger.logger.log.apply(_WinstonLogger.logger, arguments); - } - }, { - key: 'addTransport', - value: function addTransport(transport) { - // Note that this is calling addTransport - // from logger. See import - confusing. - // but this is not recursive. - (0, _WinstonLogger.addTransport)(transport); - } - - // custom query as winston is currently limited - - }, { - key: 'query', - value: function query(options) { - var callback = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; - - if (!options) { - options = {}; - } - // defaults to 7 days prior - var from = options.from || new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); - var until = options.until || new Date(); - var limit = options.size || 10; - var order = options.order || 'desc'; - var level = options.level || 'info'; - - var queryOptions = { - from: from, - until: until, - limit: limit, - order: order - }; - - return new Promise(function (resolve, reject) { - _WinstonLogger.logger.query(queryOptions, function (err, res) { - if (err) { - callback(err); - return reject(err); - } - if (level == 'error') { - callback(res['parse-server-error']); - resolve(res['parse-server-error']); - } else { - callback(res['parse-server']); - resolve(res['parse-server']); - } - }); - }); - } - }]); - - return WinstonLoggerAdapter; -}(_LoggerAdapter2.LoggerAdapter); - -exports.default = WinstonLoggerAdapter; \ No newline at end of file diff --git a/lib/Adapters/MessageQueue/EventEmitterMQ.js b/lib/Adapters/MessageQueue/EventEmitterMQ.js deleted file mode 100644 index 4a7fe3b63e..0000000000 --- a/lib/Adapters/MessageQueue/EventEmitterMQ.js +++ /dev/null @@ -1,99 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.EventEmitterMQ = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _events = require('events'); - -var _events2 = _interopRequireDefault(_events); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var emitter = new _events2.default.EventEmitter(); -var subscriptions = new Map(); - -function _unsubscribe(channel) { - if (!subscriptions.has(channel)) { - //console.log('No channel to unsub from'); - return; - } - //console.log('unsub ', channel); - emitter.removeListener(channel, subscriptions.get(channel)); - subscriptions.delete(channel); -} - -var Publisher = function () { - function Publisher(emitter) { - _classCallCheck(this, Publisher); - - this.emitter = emitter; - } - - _createClass(Publisher, [{ - key: 'publish', - value: function publish(channel, message) { - this.emitter.emit(channel, message); - } - }]); - - return Publisher; -}(); - -var Consumer = function (_events$EventEmitter) { - _inherits(Consumer, _events$EventEmitter); - - function Consumer(emitter) { - _classCallCheck(this, Consumer); - - var _this = _possibleConstructorReturn(this, (Consumer.__proto__ || Object.getPrototypeOf(Consumer)).call(this)); - - _this.emitter = emitter; - return _this; - } - - _createClass(Consumer, [{ - key: 'subscribe', - value: function subscribe(channel) { - var _this2 = this; - - _unsubscribe(channel); - var handler = function handler(message) { - _this2.emit('message', channel, message); - }; - subscriptions.set(channel, handler); - this.emitter.on(channel, handler); - } - }, { - key: 'unsubscribe', - value: function unsubscribe(channel) { - _unsubscribe(channel); - } - }]); - - return Consumer; -}(_events2.default.EventEmitter); - -function createPublisher() { - return new Publisher(emitter); -} - -function createSubscriber() { - return new Consumer(emitter); -} - -var EventEmitterMQ = { - createPublisher: createPublisher, - createSubscriber: createSubscriber -}; - -exports.EventEmitterMQ = EventEmitterMQ; \ No newline at end of file diff --git a/lib/Adapters/PubSub/EventEmitterPubSub.js b/lib/Adapters/PubSub/EventEmitterPubSub.js deleted file mode 100644 index b767a0ad1b..0000000000 --- a/lib/Adapters/PubSub/EventEmitterPubSub.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.EventEmitterPubSub = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _events = require('events'); - -var _events2 = _interopRequireDefault(_events); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var emitter = new _events2.default.EventEmitter(); - -var Publisher = function () { - function Publisher(emitter) { - _classCallCheck(this, Publisher); - - this.emitter = emitter; - } - - _createClass(Publisher, [{ - key: 'publish', - value: function publish(channel, message) { - this.emitter.emit(channel, message); - } - }]); - - return Publisher; -}(); - -var Subscriber = function (_events$EventEmitter) { - _inherits(Subscriber, _events$EventEmitter); - - function Subscriber(emitter) { - _classCallCheck(this, Subscriber); - - var _this = _possibleConstructorReturn(this, (Subscriber.__proto__ || Object.getPrototypeOf(Subscriber)).call(this)); - - _this.emitter = emitter; - _this.subscriptions = new Map(); - return _this; - } - - _createClass(Subscriber, [{ - key: 'subscribe', - value: function subscribe(channel) { - var _this2 = this; - - var handler = function handler(message) { - _this2.emit('message', channel, message); - }; - this.subscriptions.set(channel, handler); - this.emitter.on(channel, handler); - } - }, { - key: 'unsubscribe', - value: function unsubscribe(channel) { - if (!this.subscriptions.has(channel)) { - return; - } - this.emitter.removeListener(channel, this.subscriptions.get(channel)); - this.subscriptions.delete(channel); - } - }]); - - return Subscriber; -}(_events2.default.EventEmitter); - -function createPublisher() { - return new Publisher(emitter); -} - -function createSubscriber() { - return new Subscriber(emitter); -} - -var EventEmitterPubSub = { - createPublisher: createPublisher, - createSubscriber: createSubscriber -}; - -exports.EventEmitterPubSub = EventEmitterPubSub; \ No newline at end of file diff --git a/lib/Adapters/PubSub/RedisPubSub.js b/lib/Adapters/PubSub/RedisPubSub.js deleted file mode 100644 index e2c8b8549c..0000000000 --- a/lib/Adapters/PubSub/RedisPubSub.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.RedisPubSub = undefined; - -var _redis = require('redis'); - -var _redis2 = _interopRequireDefault(_redis); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function createPublisher(_ref) { - var redisURL = _ref.redisURL; - - return _redis2.default.createClient(redisURL, { no_ready_check: true }); -} - -function createSubscriber(_ref2) { - var redisURL = _ref2.redisURL; - - return _redis2.default.createClient(redisURL, { no_ready_check: true }); -} - -var RedisPubSub = { - createPublisher: createPublisher, - createSubscriber: createSubscriber -}; - -exports.RedisPubSub = RedisPubSub; \ No newline at end of file diff --git a/lib/Adapters/Push/PushAdapter.js b/lib/Adapters/Push/PushAdapter.js deleted file mode 100644 index 2f51d31691..0000000000 --- a/lib/Adapters/Push/PushAdapter.js +++ /dev/null @@ -1,47 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -/*eslint no-unused-vars: "off"*/ -// Push Adapter -// -// Allows you to change the push notification mechanism. -// -// Adapter classes must implement the following functions: -// * getValidPushTypes() -// * send(devices, installations, pushStatus) -// -// Default is ParsePushAdapter, which uses GCM for -// android push and APNS for ios push. - -var PushAdapter = exports.PushAdapter = function () { - function PushAdapter() { - _classCallCheck(this, PushAdapter); - } - - _createClass(PushAdapter, [{ - key: "send", - value: function send(body, installations, pushStatus) {} - - /** - * Get an array of valid push types. - * @returns {Array} An array of valid push types - */ - - }, { - key: "getValidPushTypes", - value: function getValidPushTypes() { - return []; - } - }]); - - return PushAdapter; -}(); - -exports.default = PushAdapter; \ No newline at end of file diff --git a/lib/Adapters/Storage/Mongo/MongoCollection.js b/lib/Adapters/Storage/Mongo/MongoCollection.js deleted file mode 100644 index a10aaceeaf..0000000000 --- a/lib/Adapters/Storage/Mongo/MongoCollection.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var mongodb = require('mongodb'); -var Collection = mongodb.Collection; - -var MongoCollection = function () { - function MongoCollection(mongoCollection) { - _classCallCheck(this, MongoCollection); - - this._mongoCollection = mongoCollection; - } - - // Does a find with "smart indexing". - // Currently this just means, if it needs a geoindex and there is - // none, then build the geoindex. - // This could be improved a lot but it's not clear if that's a good - // idea. Or even if this behavior is a good idea. - - - _createClass(MongoCollection, [{ - key: 'find', - value: function find(query) { - var _this = this; - - var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, - skip = _ref.skip, - limit = _ref.limit, - sort = _ref.sort, - keys = _ref.keys, - maxTimeMS = _ref.maxTimeMS; - - return this._rawFind(query, { skip: skip, limit: limit, sort: sort, keys: keys, maxTimeMS: maxTimeMS }).catch(function (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 - var 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(function () { - return _this._rawFind(query, { skip: skip, limit: limit, sort: sort, keys: keys, maxTimeMS: maxTimeMS }); - }); - }); - } - }, { - key: '_rawFind', - value: function _rawFind(query) { - var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, - skip = _ref2.skip, - limit = _ref2.limit, - sort = _ref2.sort, - keys = _ref2.keys, - maxTimeMS = _ref2.maxTimeMS; - - var findOperation = this._mongoCollection.find(query, { skip: skip, limit: limit, sort: sort }); - - if (keys) { - findOperation = findOperation.project(keys); - } - - if (maxTimeMS) { - findOperation = findOperation.maxTimeMS(maxTimeMS); - } - - return findOperation.toArray(); - } - }, { - key: 'count', - value: function count(query) { - var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, - skip = _ref3.skip, - limit = _ref3.limit, - sort = _ref3.sort, - maxTimeMS = _ref3.maxTimeMS; - - var countOperation = this._mongoCollection.count(query, { skip: skip, limit: limit, sort: sort, maxTimeMS: maxTimeMS }); - - return countOperation; - } - }, { - key: 'insertOne', - value: function insertOne(object) { - return this._mongoCollection.insertOne(object); - } - - // Atomically updates data in the database for a single (first) object that matched the query - // If there is nothing that matches the query - does insert - // Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5. - - }, { - key: 'upsertOne', - value: function upsertOne(query, update) { - return this._mongoCollection.update(query, update, { upsert: true }); - } - }, { - key: 'updateOne', - value: function updateOne(query, update) { - return this._mongoCollection.updateOne(query, update); - } - }, { - key: 'updateMany', - value: function updateMany(query, update) { - return this._mongoCollection.updateMany(query, update); - } - }, { - key: 'deleteOne', - value: function deleteOne(query) { - return this._mongoCollection.deleteOne(query); - } - }, { - key: 'deleteMany', - value: function deleteMany(query) { - return this._mongoCollection.deleteMany(query); - } - }, { - key: '_ensureSparseUniqueIndexInBackground', - value: function _ensureSparseUniqueIndexInBackground(indexRequest) { - var _this2 = this; - - return new Promise(function (resolve, reject) { - _this2._mongoCollection.ensureIndex(indexRequest, { unique: true, background: true, sparse: true }, function (error) { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); - } - }, { - key: 'drop', - value: function drop() { - return this._mongoCollection.drop(); - } - }]); - - return MongoCollection; -}(); - -exports.default = MongoCollection; \ No newline at end of file diff --git a/lib/Adapters/Storage/Mongo/MongoSchemaCollection.js b/lib/Adapters/Storage/Mongo/MongoSchemaCollection.js deleted file mode 100644 index b7565fadf9..0000000000 --- a/lib/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ /dev/null @@ -1,244 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _MongoCollection = require('./MongoCollection'); - -var _MongoCollection2 = _interopRequireDefault(_MongoCollection); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function mongoFieldToParseSchemaField(type) { - if (type[0] === '*') { - return { - type: 'Pointer', - targetClass: type.slice(1) - }; - } - if (type.startsWith('relation<')) { - return { - type: 'Relation', - targetClass: type.slice('relation<'.length, type.length - 1) - }; - } - switch (type) { - case 'number': - return { type: 'Number' }; - case 'string': - return { type: 'String' }; - case 'boolean': - return { type: 'Boolean' }; - case 'date': - return { type: 'Date' }; - case 'map': - case 'object': - return { type: 'Object' }; - case 'array': - return { type: 'Array' }; - case 'geopoint': - return { type: 'GeoPoint' }; - case 'file': - return { type: 'File' }; - case 'bytes': - return { type: 'Bytes' }; - } -} - -var nonFieldSchemaKeys = ['_id', '_metadata', '_client_permissions']; -function mongoSchemaFieldsToParseSchemaFields(schema) { - var fieldNames = Object.keys(schema).filter(function (key) { - return nonFieldSchemaKeys.indexOf(key) === -1; - }); - var response = fieldNames.reduce(function (obj, fieldName) { - obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName]); - return obj; - }, {}); - response.ACL = { type: 'ACL' }; - response.createdAt = { type: 'Date' }; - response.updatedAt = { type: 'Date' }; - response.objectId = { type: 'String' }; - return response; -} - -var emptyCLPS = Object.freeze({ - find: {}, - get: {}, - create: {}, - update: {}, - delete: {}, - addField: {} -}); - -var defaultCLPS = Object.freeze({ - find: { '*': true }, - get: { '*': true }, - create: { '*': true }, - update: { '*': true }, - delete: { '*': true }, - addField: { '*': true } -}); - -function mongoSchemaToParseSchema(mongoSchema) { - var clps = defaultCLPS; - if (mongoSchema._metadata && mongoSchema._metadata.class_permissions) { - clps = _extends({}, emptyCLPS, mongoSchema._metadata.class_permissions); - } - return { - className: mongoSchema._id, - fields: mongoSchemaFieldsToParseSchemaFields(mongoSchema), - classLevelPermissions: clps - }; -} - -function _mongoSchemaQueryFromNameQuery(name, query) { - var object = { _id: name }; - if (query) { - Object.keys(query).forEach(function (key) { - object[key] = query[key]; - }); - } - 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(_ref) { - var type = _ref.type, - targetClass = _ref.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'; - } -} - -var MongoSchemaCollection = function () { - function MongoSchemaCollection(collection) { - _classCallCheck(this, MongoSchemaCollection); - - this._collection = collection; - } - - _createClass(MongoSchemaCollection, [{ - key: '_fetchAllSchemasFrom_SCHEMA', - value: function _fetchAllSchemasFrom_SCHEMA() { - return this._collection._rawFind({}).then(function (schemas) { - return schemas.map(mongoSchemaToParseSchema); - }); - } - }, { - key: '_fechOneSchemaFrom_SCHEMA', - value: function _fechOneSchemaFrom_SCHEMA(name) { - return this._collection._rawFind(_mongoSchemaQueryFromNameQuery(name), { limit: 1 }).then(function (results) { - if (results.length === 1) { - return mongoSchemaToParseSchema(results[0]); - } else { - throw undefined; - } - }); - } - - // Atomically find and delete an object based on query. - - }, { - key: 'findAndDeleteSchema', - value: function findAndDeleteSchema(name) { - return this._collection._mongoCollection.findAndRemove(_mongoSchemaQueryFromNameQuery(name), []); - } - }, { - key: 'updateSchema', - value: function updateSchema(name, update) { - return this._collection.updateOne(_mongoSchemaQueryFromNameQuery(name), update); - } - }, { - key: 'upsertSchema', - value: function upsertSchema(name, query, update) { - return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update); - } - - // Add a field to the schema. If database does not support the field - // type (e.g. mongo doesn't support more than one GeoPoint in a class) reject with an "Incorrect Type" - // Parse error with a desciptive message. If the field already exists, this function must - // not modify the schema, and must reject with DUPLICATE_VALUE error. - // If this is called for a class that doesn't exist, this function must create that class. - - // TODO: throw an error if an unsupported field type is passed. Deciding whether a type is supported - // should be the job of the adapter. Some adapters may not support GeoPoint at all. Others may - // Support additional types that Mongo doesn't, like Money, or something. - - // TODO: don't spend an extra query on finding the schema if the type we are trying to add isn't a GeoPoint. - - }, { - key: 'addFieldIfNotExists', - value: function addFieldIfNotExists(className, fieldName, type) { - var _this = this; - - return this._fechOneSchemaFrom_SCHEMA(className).then(function (schema) { - // The schema exists. Check for existing GeoPoints. - if (type.type === 'GeoPoint') { - // Make sure there are not other geopoint fields - if (Object.keys(schema.fields).some(function (existingField) { - return schema.fields[existingField].type === 'GeoPoint'; - })) { - throw new _node2.default.Error(_node2.default.Error.INCORRECT_TYPE, 'MongoDB only supports one GeoPoint field in a class.'); - } - } - return; - }, function (error) { - // If error is undefined, the schema doesn't exist, and we can create the schema with the field. - // If some other error, reject with it. - if (error === undefined) { - return; - } - throw error; - }).then(function () { - // 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, _defineProperty({}, fieldName, { '$exists': false }), { '$set': _defineProperty({}, fieldName, parseFieldTypeToMongoFieldType(type)) }); - }); - } - }]); - - return 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; - -exports.default = MongoSchemaCollection; \ No newline at end of file diff --git a/lib/Adapters/Storage/Mongo/MongoStorageAdapter.js b/lib/Adapters/Storage/Mongo/MongoStorageAdapter.js deleted file mode 100644 index d40ad4c11a..0000000000 --- a/lib/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ /dev/null @@ -1,536 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.MongoStorageAdapter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _MongoCollection = require('./MongoCollection'); - -var _MongoCollection2 = _interopRequireDefault(_MongoCollection); - -var _MongoSchemaCollection = require('./MongoSchemaCollection'); - -var _MongoSchemaCollection2 = _interopRequireDefault(_MongoSchemaCollection); - -var _mongodbUrl = require('../../../vendor/mongodbUrl'); - -var _MongoTransform = require('./MongoTransform'); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _defaults = require('../../../defaults'); - -var _defaults2 = _interopRequireDefault(_defaults); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } - -var mongodb = require('mongodb'); -var MongoClient = mongodb.MongoClient; - -var MongoSchemaCollectionName = '_SCHEMA'; - -var storageAdapterAllCollections = function storageAdapterAllCollections(mongoAdapter) { - return mongoAdapter.connect().then(function () { - return mongoAdapter.database.collections(); - }).then(function (collections) { - return collections.filter(function (collection) { - if (collection.namespace.match(/\.system\./)) { - return false; - } - // TODO: If you have one app with a collection prefix that happens to be a prefix of another - // apps prefix, this will go very very badly. We should fix that somehow. - return collection.collectionName.indexOf(mongoAdapter._collectionPrefix) == 0; - }); - }); -}; - -var convertParseSchemaToMongoSchema = function convertParseSchemaToMongoSchema(_ref) { - var schema = _objectWithoutProperties(_ref, []); - - delete schema.fields._rperm; - delete schema.fields._wperm; - - if (schema.className === '_User') { - // Legacy mongo adapter knows about the difference between password and _hashed_password. - // Future database adapters will only know about _hashed_password. - // Note: Parse Server will bring back password with injectDefaultSchema, so we don't need - // to add _hashed_password back ever. - delete schema.fields._hashed_password; - } - - return schema; -}; - -// Returns { code, error } if invalid, or { result }, an object -// suitable for inserting into _SCHEMA collection, otherwise. -var mongoSchemaFromFieldsAndClassNameAndCLP = function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) { - var mongoObject = { - _id: className, - objectId: 'string', - updatedAt: 'string', - createdAt: 'string' - }; - - for (var fieldName in fields) { - mongoObject[fieldName] = _MongoSchemaCollection2.default.parseFieldTypeToMongoFieldType(fields[fieldName]); - } - - if (typeof classLevelPermissions !== 'undefined') { - mongoObject._metadata = mongoObject._metadata || {}; - if (!classLevelPermissions) { - delete mongoObject._metadata.class_permissions; - } else { - mongoObject._metadata.class_permissions = classLevelPermissions; - } - } - - return mongoObject; -}; - -var MongoStorageAdapter = exports.MongoStorageAdapter = function () { - // Public - function MongoStorageAdapter(_ref2) { - var _ref2$uri = _ref2.uri, - uri = _ref2$uri === undefined ? _defaults2.default.DefaultMongoURI : _ref2$uri, - _ref2$collectionPrefi = _ref2.collectionPrefix, - collectionPrefix = _ref2$collectionPrefi === undefined ? '' : _ref2$collectionPrefi, - _ref2$mongoOptions = _ref2.mongoOptions, - mongoOptions = _ref2$mongoOptions === undefined ? {} : _ref2$mongoOptions; - - _classCallCheck(this, MongoStorageAdapter); - - this._uri = uri; - this._collectionPrefix = collectionPrefix; - this._mongoOptions = mongoOptions; - - // MaxTimeMS is not a global MongoDB client option, it is applied per operation. - this._maxTimeMS = mongoOptions.maxTimeMS; - } - // Private - - - _createClass(MongoStorageAdapter, [{ - key: 'connect', - value: function connect() { - var _this = this; - - if (this.connectionPromise) { - return this.connectionPromise; - } - - // parsing and re-formatting causes the auth value (if there) to get URI - // encoded - var encodedUri = (0, _mongodbUrl.format)((0, _mongodbUrl.parse)(this._uri)); - - this.connectionPromise = MongoClient.connect(encodedUri, this._mongoOptions).then(function (database) { - if (!database) { - delete _this.connectionPromise; - return; - } - database.on('error', function () { - delete _this.connectionPromise; - }); - database.on('close', function () { - delete _this.connectionPromise; - }); - _this.database = database; - }).catch(function (err) { - delete _this.connectionPromise; - return Promise.reject(err); - }); - - return this.connectionPromise; - } - }, { - key: 'handleShutdown', - value: function handleShutdown() { - if (!this.database) { - return; - } - this.database.close(false); - } - }, { - key: '_adaptiveCollection', - value: function _adaptiveCollection(name) { - var _this2 = this; - - return this.connect().then(function () { - return _this2.database.collection(_this2._collectionPrefix + name); - }).then(function (rawCollection) { - return new _MongoCollection2.default(rawCollection); - }); - } - }, { - key: '_schemaCollection', - value: function _schemaCollection() { - var _this3 = this; - - return this.connect().then(function () { - return _this3._adaptiveCollection(MongoSchemaCollectionName); - }).then(function (collection) { - return new _MongoSchemaCollection2.default(collection); - }); - } - }, { - key: 'classExists', - value: function classExists(name) { - var _this4 = this; - - return this.connect().then(function () { - return _this4.database.listCollections({ name: _this4._collectionPrefix + name }).toArray(); - }).then(function (collections) { - return collections.length > 0; - }); - } - }, { - key: 'setClassLevelPermissions', - value: function setClassLevelPermissions(className, CLPs) { - return this._schemaCollection().then(function (schemaCollection) { - return schemaCollection.updateSchema(className, { - $set: { _metadata: { class_permissions: CLPs } } - }); - }); - } - }, { - key: 'createClass', - value: function createClass(className, schema) { - schema = convertParseSchemaToMongoSchema(schema); - var mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(schema.fields, className, schema.classLevelPermissions); - mongoObject._id = className; - return this._schemaCollection().then(function (schemaCollection) { - return schemaCollection._collection.insertOne(mongoObject); - }).then(function (result) { - return _MongoSchemaCollection2.default._TESTmongoSchemaToParseSchema(result.ops[0]); - }).catch(function (error) { - if (error.code === 11000) { - //Mongo's duplicate key error - throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'Class already exists.'); - } else { - throw error; - } - }); - } - }, { - key: 'addFieldIfNotExists', - value: function addFieldIfNotExists(className, fieldName, type) { - return this._schemaCollection().then(function (schemaCollection) { - return schemaCollection.addFieldIfNotExists(className, fieldName, type); - }); - } - - // Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.) - // and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible. - - }, { - key: 'deleteClass', - value: function deleteClass(className) { - var _this5 = this; - - return this._adaptiveCollection(className).then(function (collection) { - return collection.drop(); - }).catch(function (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(function () { - return _this5._schemaCollection(); - }).then(function (schemaCollection) { - return schemaCollection.findAndDeleteSchema(className); - }); - } - - // Delete all data known to this adatper. Used for testing. - - }, { - key: 'deleteAllClasses', - value: function deleteAllClasses() { - return storageAdapterAllCollections(this).then(function (collections) { - return Promise.all(collections.map(function (collection) { - return collection.drop(); - })); - }); - } - - // Remove the column and all the data. For Relations, the _Join collection is handled - // specially, this function does not delete _Join columns. It should, however, indicate - // that the relation fields does not exist anymore. In mongo, this means removing it from - // the _SCHEMA collection. There should be no actual data in the collection under the same name - // as the relation column, so it's fine to attempt to delete it. If the fields listed to be - // deleted do not exist, this function should return successfully anyways. Checking for - // attempts to delete non-existent fields is the responsibility of Parse Server. - - // Pointer field names are passed for legacy reasons: the original mongo - // format stored pointer field names differently in the database, and therefore - // needed to know the type of the field before it could delete it. Future database - // adatpers should ignore the pointerFieldNames argument. All the field names are in - // fieldNames, they show up additionally in the pointerFieldNames database for use - // by the mongo adapter, which deals with the legacy mongo format. - - // This function is not obligated to delete fields atomically. It is given the field - // names in a list so that databases that are capable of deleting fields atomically - // may do so. - - // Returns a Promise. - - }, { - key: 'deleteFields', - value: function deleteFields(className, schema, fieldNames) { - var _this6 = this; - - var mongoFormatNames = fieldNames.map(function (fieldName) { - if (schema.fields[fieldName].type === 'Pointer') { - return '_p_' + fieldName; - } else { - return fieldName; - } - }); - var collectionUpdate = { '$unset': {} }; - mongoFormatNames.forEach(function (name) { - collectionUpdate['$unset'][name] = null; - }); - - var schemaUpdate = { '$unset': {} }; - fieldNames.forEach(function (name) { - schemaUpdate['$unset'][name] = null; - }); - - return this._adaptiveCollection(className).then(function (collection) { - return collection.updateMany({}, collectionUpdate); - }).then(function () { - return _this6._schemaCollection(); - }).then(function (schemaCollection) { - return schemaCollection.updateSchema(className, schemaUpdate); - }); - } - - // Return a promise for all schemas known to this adapter, in Parse format. In case the - // schemas cannot be retrieved, returns a promise that rejects. Requirements for the - // rejection reason are TBD. - - }, { - key: 'getAllClasses', - value: function getAllClasses() { - return this._schemaCollection().then(function (schemasCollection) { - return schemasCollection._fetchAllSchemasFrom_SCHEMA(); - }); - } - - // Return a promise for the schema with the given name, in Parse format. If - // this adapter doesn't know about the schema, return a promise that rejects with - // undefined as the reason. - - }, { - key: 'getClass', - value: function getClass(className) { - return this._schemaCollection().then(function (schemasCollection) { - return schemasCollection._fechOneSchemaFrom_SCHEMA(className); - }); - } - - // TODO: As yet not particularly well specified. Creates an object. Maybe shouldn't even need the schema, - // and should infer from the type. Or maybe does need the schema for validations. Or maybe needs - // the schem only for the legacy mongo format. We'll figure that out later. - - }, { - key: 'createObject', - value: function createObject(className, schema, object) { - schema = convertParseSchemaToMongoSchema(schema); - var mongoObject = (0, _MongoTransform.parseObjectToMongoObjectForCreate)(className, object, schema); - return this._adaptiveCollection(className).then(function (collection) { - return collection.insertOne(mongoObject); - }).catch(function (error) { - if (error.code === 11000) { - // Duplicate value - throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); - } - throw error; - }); - } - - // Remove all objects that match the given Parse Query. - // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined. - // If there is some other error, reject with INTERNAL_SERVER_ERROR. - - }, { - key: 'deleteObjectsByQuery', - value: function deleteObjectsByQuery(className, schema, query) { - schema = convertParseSchemaToMongoSchema(schema); - return this._adaptiveCollection(className).then(function (collection) { - var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); - return collection.deleteMany(mongoWhere); - }).then(function (_ref3) { - var result = _ref3.result; - - if (result.n === 0) { - throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Object not found.'); - } - return Promise.resolve(); - }, function () { - throw new _node2.default.Error(_node2.default.Error.INTERNAL_SERVER_ERROR, 'Database adapter error'); - }); - } - - // Apply the update to all objects that match the given Parse Query. - - }, { - key: 'updateObjectsByQuery', - value: function updateObjectsByQuery(className, schema, query, update) { - schema = convertParseSchemaToMongoSchema(schema); - var mongoUpdate = (0, _MongoTransform.transformUpdate)(className, update, schema); - var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); - return this._adaptiveCollection(className).then(function (collection) { - return collection.updateMany(mongoWhere, mongoUpdate); - }); - } - - // Atomically finds and updates an object based on query. - // Return value not currently well specified. - - }, { - key: 'findOneAndUpdate', - value: function findOneAndUpdate(className, schema, query, update) { - schema = convertParseSchemaToMongoSchema(schema); - var mongoUpdate = (0, _MongoTransform.transformUpdate)(className, update, schema); - var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); - return this._adaptiveCollection(className).then(function (collection) { - return collection._mongoCollection.findAndModify(mongoWhere, [], mongoUpdate, { new: true }); - }).then(function (result) { - return (0, _MongoTransform.mongoObjectToParseObject)(className, result.value, schema); - }); - } - - // Hopefully we can get rid of this. It's only used for config and hooks. - - }, { - key: 'upsertOneObject', - value: function upsertOneObject(className, schema, query, update) { - schema = convertParseSchemaToMongoSchema(schema); - var mongoUpdate = (0, _MongoTransform.transformUpdate)(className, update, schema); - var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); - return this._adaptiveCollection(className).then(function (collection) { - return collection.upsertOne(mongoWhere, mongoUpdate); - }); - } - - // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. - - }, { - key: 'find', - value: function find(className, schema, query, _ref4) { - var _this7 = this; - - var skip = _ref4.skip, - limit = _ref4.limit, - sort = _ref4.sort, - keys = _ref4.keys; - - schema = convertParseSchemaToMongoSchema(schema); - var mongoWhere = (0, _MongoTransform.transformWhere)(className, query, schema); - var mongoSort = _lodash2.default.mapKeys(sort, function (value, fieldName) { - return (0, _MongoTransform.transformKey)(className, fieldName, schema); - }); - var mongoKeys = _lodash2.default.reduce(keys, function (memo, key) { - memo[(0, _MongoTransform.transformKey)(className, key, schema)] = 1; - return memo; - }, {}); - return this._adaptiveCollection(className).then(function (collection) { - return collection.find(mongoWhere, { - skip: skip, - limit: limit, - sort: mongoSort, - keys: mongoKeys, - maxTimeMS: _this7._maxTimeMS - }); - }).then(function (objects) { - return objects.map(function (object) { - return (0, _MongoTransform.mongoObjectToParseObject)(className, object, schema); - }); - }); - } - - // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't - // currently know which fields are nullable and which aren't, we ignore that criteria. - // As such, we shouldn't expose this function to users of parse until we have an out-of-band - // Way of determining if a field is nullable. Undefined doesn't count against uniqueness, - // which is why we use sparse indexes. - - }, { - key: 'ensureUniqueness', - value: function ensureUniqueness(className, schema, fieldNames) { - schema = convertParseSchemaToMongoSchema(schema); - var indexCreationRequest = {}; - var mongoFieldNames = fieldNames.map(function (fieldName) { - return (0, _MongoTransform.transformKey)(className, fieldName, schema); - }); - mongoFieldNames.forEach(function (fieldName) { - indexCreationRequest[fieldName] = 1; - }); - return this._adaptiveCollection(className).then(function (collection) { - return collection._ensureSparseUniqueIndexInBackground(indexCreationRequest); - }).catch(function (error) { - if (error.code === 11000) { - throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'Tried to ensure field uniqueness for a class that already has duplicates.'); - } else { - throw error; - } - }); - } - - // Used in tests - - }, { - key: '_rawFind', - value: function _rawFind(className, query) { - var _this8 = this; - - return this._adaptiveCollection(className).then(function (collection) { - return collection.find(query, { - maxTimeMS: _this8._maxTimeMS - }); - }); - } - - // Executes a count. - - }, { - key: 'count', - value: function count(className, schema, query) { - var _this9 = this; - - schema = convertParseSchemaToMongoSchema(schema); - return this._adaptiveCollection(className).then(function (collection) { - return collection.count((0, _MongoTransform.transformWhere)(className, query, schema), { - maxTimeMS: _this9._maxTimeMS - }); - }); - } - }, { - key: 'performInitialization', - value: function performInitialization() { - return Promise.resolve(); - } - }]); - - return MongoStorageAdapter; -}(); - -exports.default = MongoStorageAdapter; - -module.exports = MongoStorageAdapter; // Required for tests \ No newline at end of file diff --git a/lib/Adapters/Storage/Mongo/MongoTransform.js b/lib/Adapters/Storage/Mongo/MongoTransform.js deleted file mode 100644 index 7be1f31b25..0000000000 --- a/lib/Adapters/Storage/Mongo/MongoTransform.js +++ /dev/null @@ -1,1022 +0,0 @@ -'use strict'; - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _logger = require('../../../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var mongodb = require('mongodb'); -var Parse = require('parse/node').Parse; - -var transformKey = function transformKey(className, fieldName, schema) { - // Check if the schema is known since it's a built-in field. - switch (fieldName) { - case 'objectId': - return '_id'; - case 'createdAt': - return '_created_at'; - case 'updatedAt': - return '_updated_at'; - case 'sessionToken': - return '_session_token'; - } - - if (schema.fields[fieldName] && schema.fields[fieldName].__type == 'Pointer') { - fieldName = '_p_' + fieldName; - } else if (schema.fields[fieldName] && schema.fields[fieldName].type == 'Pointer') { - fieldName = '_p_' + fieldName; - } - - return fieldName; -}; - -var transformKeyValueForUpdate = function 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) - }; - } - 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 }; - } - - if (parseFormatSchema.fields[key] && parseFormatSchema.fields[key].type === 'Pointer' || !parseFormatSchema.fields[key] && restValue && restValue.__type == 'Pointer') { - key = '_p_' + key; - } - - // Handle atomic values - var value = transformTopLevelAtom(restValue); - if (value !== CannotTransform) { - if (timeField && typeof value === 'string') { - value = new Date(value); - } - if (restKey.indexOf('.') > 0) { - return { key: key, value: restValue }; - } - return { key: key, value: value }; - } - - // Handle arrays - if (restValue instanceof Array) { - value = restValue.map(transformInteriorValue); - return { key: key, value: value }; - } - - // Handle update operators - if ((typeof restValue === 'undefined' ? 'undefined' : _typeof(restValue)) === 'object' && '__op' in restValue) { - return { key: key, value: transformUpdateOperator(restValue, false) }; - } - - // Handle normal objects by recursing - value = _lodash2.default.mapValues(restValue, transformInteriorValue); - return { key: key, value: value }; -}; - -var transformInteriorValue = function transformInteriorValue(restValue) { - if (restValue !== null && (typeof restValue === 'undefined' ? 'undefined' : _typeof(restValue)) === 'object' && Object.keys(restValue).some(function (key) { - return key.includes('$') || key.includes('.'); - })) { - throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); - } - // Handle atomic values - var value = transformInteriorAtom(restValue); - if (value !== CannotTransform) { - return value; - } - - // Handle arrays - if (restValue instanceof Array) { - return restValue.map(transformInteriorValue); - } - - // Handle update operators - if ((typeof restValue === 'undefined' ? 'undefined' : _typeof(restValue)) === 'object' && '__op' in restValue) { - return transformUpdateOperator(restValue, true); - } - - // Handle normal objects by recursing - return _lodash2.default.mapValues(restValue, transformInteriorValue); -}; - -var valueAsDate = function valueAsDate(value) { - if (typeof value === 'string') { - return new Date(value); - } else if (value instanceof Date) { - return value; - } - return false; -}; - -function transformQueryKeyValue(className, key, value, schema) { - switch (key) { - case 'createdAt': - if (valueAsDate(value)) { - return { key: '_created_at', value: valueAsDate(value) }; - } - key = '_created_at'; - break; - case 'updatedAt': - if (valueAsDate(value)) { - return { key: '_updated_at', value: valueAsDate(value) }; - } - key = '_updated_at'; - break; - case 'expiresAt': - if (valueAsDate(value)) { - return { key: 'expiresAt', value: valueAsDate(value) }; - } - break; - case '_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: 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: key, value: value }; - case 'sessionToken': - return { key: '_session_token', value: 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: key, value: value }; - case '$or': - return { key: '$or', value: value.map(function (subQuery) { - return transformWhere(className, subQuery, schema); - }) }; - case '$and': - return { key: '$and', value: value.map(function (subQuery) { - return transformWhere(className, subQuery, schema); - }) }; - default: - { - // Other auth data - var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); - if (authDataMatch) { - var provider = authDataMatch[1]; - // Special-case auth data. - return { key: '_auth_data_' + provider + '.id', value: value }; - } - } - } - - var expectedTypeIsArray = schema && schema.fields[key] && schema.fields[key].type === 'Array'; - - var expectedTypeIsPointer = schema && schema.fields[key] && schema.fields[key].type === 'Pointer'; - - if (expectedTypeIsPointer || !schema && value && value.__type === 'Pointer') { - key = '_p_' + key; - } - - // Handle query constraints - var transformedConstraint = transformConstraint(value, expectedTypeIsArray); - if (transformedConstraint !== CannotTransform) { - return { key: key, value: transformedConstraint }; - } - - if (expectedTypeIsArray && !(value instanceof Array)) { - return { key: key, value: { '$all': [transformInteriorAtom(value)] } }; - } - - // Handle atomic values - if (transformTopLevelAtom(value) !== CannotTransform) { - return { key: key, value: transformTopLevelAtom(value) }; - } else { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'You cannot use ' + value + ' as a query parameter.'); - } -} - -// Main exposed method to help run queries. -// restWhere is the "where" clause in REST API form. -// Returns the mongo form of the query. -function transformWhere(className, restWhere, schema) { - var mongoWhere = {}; - for (var restKey in restWhere) { - var out = transformQueryKeyValue(className, restKey, restWhere[restKey], schema); - mongoWhere[out.key] = out.value; - } - return mongoWhere; -} - -var parseObjectKeyValueToMongoObjectKeyValue = function parseObjectKeyValueToMongoObjectKeyValue(restKey, restValue, schema) { - // Check if the schema is known since it's a built-in field. - var transformedValue = void 0; - var coercedToDate = void 0; - 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') { - restKey = '_p_' + restKey; - } - } - - // Handle atomic values - var value = transformTopLevelAtom(restValue); - if (value !== CannotTransform) { - return { key: restKey, value: value }; - } - - // ACLs are handled before this method is called - // If an ACL key still exists here, something is wrong. - if (restKey === 'ACL') { - throw 'There was a problem transforming an ACL.'; - } - - // Handle arrays - if (restValue instanceof Array) { - value = restValue.map(transformInteriorValue); - return { key: restKey, value: value }; - } - - // Handle normal objects by recursing - if (Object.keys(restValue).some(function (key) { - return key.includes('$') || key.includes('.'); - })) { - throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); - } - value = _lodash2.default.mapValues(restValue, transformInteriorValue); - return { key: restKey, value: value }; -}; - -var parseObjectToMongoObjectForCreate = function parseObjectToMongoObjectForCreate(className, restCreate, schema) { - restCreate = addLegacyACL(restCreate); - var mongoCreate = {}; - for (var restKey in restCreate) { - if (restCreate[restKey] && restCreate[restKey].__type === 'Relation') { - continue; - } - - var _parseObjectKeyValueT = parseObjectKeyValueToMongoObjectKeyValue(restKey, restCreate[restKey], schema), - key = _parseObjectKeyValueT.key, - value = _parseObjectKeyValueT.value; - - if (value !== undefined) { - mongoCreate[key] = value; - } - } - - // Use the legacy mongo format for createdAt and updatedAt - if (mongoCreate.createdAt) { - mongoCreate._created_at = new Date(mongoCreate.createdAt.iso || mongoCreate.createdAt); - delete mongoCreate.createdAt; - } - if (mongoCreate.updatedAt) { - mongoCreate._updated_at = new Date(mongoCreate.updatedAt.iso || mongoCreate.updatedAt); - delete mongoCreate.updatedAt; - } - - return mongoCreate; -}; - -// Main exposed method to help update old objects. -var transformUpdate = function transformUpdate(className, restUpdate, parseFormatSchema) { - var mongoUpdate = {}; - var acl = addLegacyACL(restUpdate); - if (acl._rperm || acl._wperm || acl._acl) { - mongoUpdate.$set = {}; - if (acl._rperm) { - mongoUpdate.$set._rperm = acl._rperm; - } - if (acl._wperm) { - mongoUpdate.$set._wperm = acl._wperm; - } - if (acl._acl) { - mongoUpdate.$set._acl = acl._acl; - } - } - for (var restKey in restUpdate) { - if (restUpdate[restKey] && restUpdate[restKey].__type === 'Relation') { - continue; - } - var out = transformKeyValueForUpdate(className, restKey, restUpdate[restKey], parseFormatSchema); - - // If the output value is an object with any $ keys, it's an - // operator that needs to be lifted onto the top level update - // object. - if (_typeof(out.value) === 'object' && out.value !== null && out.value.__op) { - mongoUpdate[out.value.__op] = mongoUpdate[out.value.__op] || {}; - mongoUpdate[out.value.__op][out.key] = out.value.arg; - } else { - mongoUpdate['$set'] = mongoUpdate['$set'] || {}; - mongoUpdate['$set'][out.key] = out.value; - } - } - - return mongoUpdate; -}; - -// Add the legacy _acl format. -var addLegacyACL = function addLegacyACL(restObject) { - var restObjectCopy = _extends({}, restObject); - var _acl = {}; - - if (restObject._wperm) { - restObject._wperm.forEach(function (entry) { - _acl[entry] = { w: true }; - }); - restObjectCopy._acl = _acl; - } - - if (restObject._rperm) { - restObject._rperm.forEach(function (entry) { - if (!(entry in _acl)) { - _acl[entry] = { r: true }; - } else { - _acl[entry].r = true; - } - }); - restObjectCopy._acl = _acl; - } - - return restObjectCopy; -}; - -// A sentinel value that helper transformations return when they -// cannot perform a transformation -function CannotTransform() {} - -var transformInteriorAtom = function transformInteriorAtom(atom) { - // TODO: check validity harder for the __type-defined types - if ((typeof atom === 'undefined' ? 'undefined' : _typeof(atom)) === 'object' && atom && !(atom instanceof Date) && atom.__type === 'Pointer') { - return { - __type: 'Pointer', - className: atom.className, - objectId: atom.objectId - }; - } else if (typeof atom === 'function' || (typeof atom === 'undefined' ? 'undefined' : _typeof(atom)) === 'symbol') { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot transform value: ' + atom); - } else if (DateCoder.isValidJSON(atom)) { - return DateCoder.JSONToDatabase(atom); - } else if (BytesCoder.isValidJSON(atom)) { - return BytesCoder.JSONToDatabase(atom); - } else if ((typeof atom === 'undefined' ? 'undefined' : _typeof(atom)) === 'object' && atom && atom.$regex !== undefined) { - return new RegExp(atom.$regex); - } else { - return atom; - } -}; - -// Helper function to transform an atom from REST format to Mongo format. -// An atom is anything that can't contain other expressions. So it -// includes things where objects are used to represent other -// datatypes, like pointers and dates, but it does not include objects -// or arrays with generic stuff inside. -// Raises an error if this cannot possibly be valid REST format. -// Returns CannotTransform if it's just not an atom -function transformTopLevelAtom(atom) { - switch (typeof atom === 'undefined' ? 'undefined' : _typeof(atom)) { - case 'string': - case 'number': - case 'boolean': - return atom; - case 'undefined': - return atom; - case 'symbol': - case 'function': - throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot transform value: ' + atom); - case 'object': - if (atom instanceof Date) { - // Technically dates are not rest format, but, it seems pretty - // clear what they should be transformed to, so let's just do it. - return atom; - } - - if (atom === null) { - return atom; - } - - // TODO: check validity harder for the __type-defined types - if (atom.__type == 'Pointer') { - return atom.className + '$' + atom.objectId; - } - if (DateCoder.isValidJSON(atom)) { - return DateCoder.JSONToDatabase(atom); - } - if (BytesCoder.isValidJSON(atom)) { - return BytesCoder.JSONToDatabase(atom); - } - if (GeoPointCoder.isValidJSON(atom)) { - return GeoPointCoder.JSONToDatabase(atom); - } - if (FileCoder.isValidJSON(atom)) { - return FileCoder.JSONToDatabase(atom); - } - return CannotTransform; - - default: - // I don't think typeof can ever let us get here - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'really did not expect value: ' + atom); - } -} - -// Transforms a query constraint from REST API format to Mongo format. -// A constraint is something with fields like $lt. -// If it is not a valid constraint but it could be a valid something -// else, return CannotTransform. -// inArray is whether this is an array field. -function transformConstraint(constraint, inArray) { - if ((typeof constraint === 'undefined' ? 'undefined' : _typeof(constraint)) !== 'object' || !constraint) { - return CannotTransform; - } - var transformFunction = inArray ? transformInteriorAtom : transformTopLevelAtom; - var transformer = function transformer(atom) { - var result = transformFunction(atom); - if (result === CannotTransform) { - 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 answer = {}; - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = keys[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var key = _step.value; - - switch (key) { - case '$lt': - case '$lte': - case '$gt': - case '$gte': - case '$exists': - case '$ne': - case '$eq': - answer[key] = transformer(constraint[key]); - break; - - case '$in': - case '$nin': - { - var arr = constraint[key]; - if (!(arr instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); - } - answer[key] = _lodash2.default.flatMap(arr, function (value) { - return function (atom) { - if (Array.isArray(atom)) { - return value.map(transformer); - } else { - return transformer(atom); - } - }(value); - }); - break; - } - case '$all': - { - var _arr = constraint[key]; - if (!(_arr instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad ' + key + ' value'); - } - answer[key] = _arr.map(transformInteriorAtom); - break; - } - case '$regex': - var s = constraint[key]; - if (typeof s !== 'string') { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad regex: ' + s); - } - answer[key] = s; - break; - - case '$options': - answer[key] = constraint[key]; - break; - - case '$nearSphere': - var point = constraint[key]; - answer[key] = [point.longitude, point.latitude]; - break; - - case '$maxDistance': - answer[key] = constraint[key]; - break; - - // The SDKs don't seem to use these but they are documented in the - // REST API docs. - case '$maxDistanceInRadians': - answer['$maxDistance'] = constraint[key]; - break; - case '$maxDistanceInMiles': - answer['$maxDistance'] = constraint[key] / 3959; - break; - case '$maxDistanceInKilometers': - answer['$maxDistance'] = constraint[key] / 6371; - break; - - case '$select': - case '$dontSelect': - throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'the ' + key + ' constraint is not supported yet'); - - case '$within': - var box = constraint[key]['$box']; - if (!box || box.length != 2) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'malformatted $within arg'); - } - answer[key] = { - '$box': [[box[0].longitude, box[0].latitude], [box[1].longitude, box[1].latitude]] - }; - break; - - default: - if (key.match(/^\$+/)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad constraint: ' + key); - } - return CannotTransform; - } - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - return answer; -} - -// Transforms an update operator from REST format to mongo format. -// To be transformed, the input should have an __op field. -// If flatten is true, this will flatten operators to their static -// data format. For example, an increment of 2 would simply become a -// 2. -// The output for a non-flattened operator is a hash with __op being -// the mongo op, and arg being the argument. -// The output for a flattened operator is just a value. -// Returns undefined if this should be a no-op. - -function transformUpdateOperator(_ref, flatten) { - var __op = _ref.__op, - amount = _ref.amount, - objects = _ref.objects; - - switch (__op) { - case 'Delete': - if (flatten) { - return undefined; - } else { - return { __op: '$unset', arg: '' }; - } - - case 'Increment': - if (typeof amount !== 'number') { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'incrementing must provide a number'); - } - if (flatten) { - return amount; - } else { - return { __op: '$inc', arg: amount }; - } - - case 'Add': - case 'AddUnique': - if (!(objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to add must be an array'); - } - var toAdd = objects.map(transformInteriorAtom); - if (flatten) { - return toAdd; - } else { - var mongoOp = { - Add: '$push', - AddUnique: '$addToSet' - }[__op]; - return { __op: mongoOp, arg: { '$each': toAdd } }; - } - - case 'Remove': - if (!(objects instanceof Array)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'objects to remove must be an array'); - } - var toRemove = objects.map(transformInteriorAtom); - if (flatten) { - return []; - } else { - return { __op: '$pullAll', arg: toRemove }; - } - - default: - throw new Parse.Error(Parse.Error.COMMAND_UNAVAILABLE, 'The ' + __op + ' operator is not supported yet.'); - } -} - -var nestedMongoObjectToNestedParseObject = function nestedMongoObjectToNestedParseObject(mongoObject) { - switch (typeof mongoObject === 'undefined' ? 'undefined' : _typeof(mongoObject)) { - case 'string': - case 'number': - case 'boolean': - return mongoObject; - case 'undefined': - case 'symbol': - case 'function': - throw 'bad value in mongoObjectToParseObject'; - case 'object': - if (mongoObject === null) { - return null; - } - if (mongoObject instanceof Array) { - return mongoObject.map(nestedMongoObjectToNestedParseObject); - } - - if (mongoObject instanceof Date) { - return Parse._encode(mongoObject); - } - - if (mongoObject instanceof mongodb.Long) { - return mongoObject.toNumber(); - } - - if (mongoObject instanceof mongodb.Double) { - return mongoObject.value; - } - - if (BytesCoder.isValidDatabaseObject(mongoObject)) { - return BytesCoder.databaseToJSON(mongoObject); - } - - if (mongoObject.hasOwnProperty('__type') && mongoObject.__type == 'Date' && mongoObject.iso instanceof Date) { - mongoObject.iso = mongoObject.iso.toJSON(); - return mongoObject; - } - - return _lodash2.default.mapValues(mongoObject, nestedMongoObjectToNestedParseObject); - default: - throw 'unknown js type'; - } -}; - -// Converts from a mongo-format object to a REST-format object. -// Does not strip out anything based on a lack of authentication. -var mongoObjectToParseObject = function mongoObjectToParseObject(className, mongoObject, schema) { - switch (typeof mongoObject === 'undefined' ? 'undefined' : _typeof(mongoObject)) { - case 'string': - case 'number': - case 'boolean': - return mongoObject; - case 'undefined': - case 'symbol': - case 'function': - throw 'bad value in mongoObjectToParseObject'; - case 'object': - { - if (mongoObject === null) { - return null; - } - if (mongoObject instanceof Array) { - return mongoObject.map(nestedMongoObjectToNestedParseObject); - } - - if (mongoObject instanceof Date) { - return Parse._encode(mongoObject); - } - - if (mongoObject instanceof mongodb.Long) { - return mongoObject.toNumber(); - } - - if (mongoObject instanceof mongodb.Double) { - return mongoObject.value; - } - - if (BytesCoder.isValidDatabaseObject(mongoObject)) { - return BytesCoder.databaseToJSON(mongoObject); - } - - var restObject = {}; - if (mongoObject._rperm || mongoObject._wperm) { - restObject._rperm = mongoObject._rperm || []; - restObject._wperm = mongoObject._wperm || []; - delete mongoObject._rperm; - delete mongoObject._wperm; - } - - for (var key in mongoObject) { - switch (key) { - case '_id': - restObject['objectId'] = '' + mongoObject[key]; - break; - case '_hashed_password': - restObject._hashed_password = mongoObject[key]; - break; - case '_acl': - 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; - 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]) { - _logger2.default.info('transform.js', 'Found a pointer column not in the schema, dropping it.', className, newKey); - break; - } - if (schema.fields[newKey].type !== 'Pointer') { - _logger2.default.info('transform.js', 'Found a pointer in a non-pointer column, dropping it.', className, key); - break; - } - if (mongoObject[key] === null) { - break; - } - var objData = mongoObject[key].split('$'); - if (objData[0] !== schema.fields[newKey].targetClass) { - throw 'pointer to incorrect className'; - } - restObject[newKey] = { - __type: 'Pointer', - className: objData[0], - objectId: objData[1] - }; - break; - } else if (key[0] == '_' && key != '__type') { - throw 'bad key in untransform: ' + key; - } else { - var value = mongoObject[key]; - if (schema.fields[key] && schema.fields[key].type === 'File' && FileCoder.isValidDatabaseObject(value)) { - restObject[key] = FileCoder.databaseToJSON(value); - break; - } - if (schema.fields[key] && schema.fields[key].type === 'GeoPoint' && GeoPointCoder.isValidDatabaseObject(value)) { - restObject[key] = GeoPointCoder.databaseToJSON(value); - break; - } - if (schema.fields[key] && schema.fields[key].type === 'Bytes' && BytesCoder.isValidDatabaseObject(value)) { - restObject[key] = BytesCoder.databaseToJSON(value); - break; - } - } - restObject[key] = nestedMongoObjectToNestedParseObject(mongoObject[key]); - } - } - - var relationFieldNames = Object.keys(schema.fields).filter(function (fieldName) { - return schema.fields[fieldName].type === 'Relation'; - }); - var relationFields = {}; - relationFieldNames.forEach(function (relationFieldName) { - relationFields[relationFieldName] = { - __type: 'Relation', - className: schema.fields[relationFieldName].targetClass - }; - }); - - return _extends({}, restObject, relationFields); - } - default: - throw 'unknown js type'; - } -}; - -var DateCoder = { - JSONToDatabase: function JSONToDatabase(json) { - return new Date(json.iso); - }, - isValidJSON: function isValidJSON(value) { - return (typeof value === 'undefined' ? 'undefined' : _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}=)?$"), - isBase64Value: function isBase64Value(object) { - if (typeof object !== 'string') { - return false; - } - return this.base64Pattern.test(object); - }, - databaseToJSON: function databaseToJSON(object) { - var value = void 0; - if (this.isBase64Value(object)) { - value = object; - } else { - value = object.buffer.toString('base64'); - } - return { - __type: 'Bytes', - base64: value - }; - }, - isValidDatabaseObject: function isValidDatabaseObject(object) { - return object instanceof mongodb.Binary || this.isBase64Value(object); - }, - JSONToDatabase: function JSONToDatabase(json) { - return new mongodb.Binary(new Buffer(json.base64, 'base64')); - }, - isValidJSON: function isValidJSON(value) { - return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null && value.__type === 'Bytes'; - } -}; - -var GeoPointCoder = { - databaseToJSON: function databaseToJSON(object) { - return { - __type: 'GeoPoint', - latitude: object[1], - longitude: object[0] - }; - }, - isValidDatabaseObject: function isValidDatabaseObject(object) { - return object instanceof Array && object.length == 2; - }, - JSONToDatabase: function JSONToDatabase(json) { - return [json.longitude, json.latitude]; - }, - isValidJSON: function isValidJSON(value) { - return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null && value.__type === 'GeoPoint'; - } -}; - -var FileCoder = { - databaseToJSON: function databaseToJSON(object) { - return { - __type: 'File', - name: object - }; - }, - isValidDatabaseObject: function isValidDatabaseObject(object) { - return typeof object === 'string'; - }, - JSONToDatabase: function JSONToDatabase(json) { - return json.name; - }, - isValidJSON: function isValidJSON(value) { - return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null && value.__type === 'File'; - } -}; - -module.exports = { - transformKey: transformKey, - parseObjectToMongoObjectForCreate: parseObjectToMongoObjectForCreate, - transformUpdate: transformUpdate, - transformWhere: transformWhere, - mongoObjectToParseObject: mongoObjectToParseObject -}; \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/PostgresClient.js b/lib/Adapters/Storage/Postgres/PostgresClient.js deleted file mode 100644 index 33324193fc..0000000000 --- a/lib/Adapters/Storage/Postgres/PostgresClient.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.createClient = createClient; - -var parser = require('./PostgresConfigParser'); - -function createClient(uri, databaseOptions) { - var dbOptions = {}; - databaseOptions = databaseOptions || {}; - - if (uri) { - dbOptions = parser.getDatabaseOptionsFromURI(uri); - } - - for (var key in databaseOptions) { - dbOptions[key] = databaseOptions[key]; - } - - var initOptions = dbOptions.initOptions || {}; - var pgp = require('pg-promise')(initOptions); - var client = pgp(dbOptions); - - if (dbOptions.pgOptions) { - for (var _key in dbOptions.pgOptions) { - pgp.pg.defaults[_key] = dbOptions.pgOptions[_key]; - } - } - - return { client: client, pgp: pgp }; -} \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/PostgresConfigParser.js b/lib/Adapters/Storage/Postgres/PostgresConfigParser.js deleted file mode 100644 index 1d4e7a46a3..0000000000 --- a/lib/Adapters/Storage/Postgres/PostgresConfigParser.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -var url = require('url'); - -function getDatabaseOptionsFromURI(uri) { - var databaseOptions = {}; - - var parsedURI = url.parse(uri); - var queryParams = parseQueryParams(parsedURI.query); - var authParts = parsedURI.auth ? parsedURI.auth.split(':') : []; - - databaseOptions.host = parsedURI.hostname || 'localhost'; - databaseOptions.port = parsedURI.port ? parseInt(parsedURI.port) : 5432; - databaseOptions.database = parsedURI.pathname ? parsedURI.pathname.substr(1) : undefined; - - databaseOptions.user = authParts.length > 0 ? authParts[0] : ''; - databaseOptions.password = authParts.length > 1 ? authParts[1] : ''; - - databaseOptions.ssl = queryParams.ssl && queryParams.ssl.toLowerCase() === 'true' ? true : false; - databaseOptions.binary = 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; - - if (queryParams.poolSize) { - databaseOptions.poolSize = parseInt(queryParams.poolSize) || 10; - } - - return databaseOptions; -} - -function parseQueryParams(queryString) { - queryString = queryString || ''; - - return queryString.split('&').reduce(function (p, c) { - var parts = c.split('='); - p[decodeURIComponent(parts[0])] = parts.length > 1 ? decodeURIComponent(parts.slice(1).join('=')) : ''; - return p; - }, {}); -} - -module.exports = { - parseQueryParams: parseQueryParams, - getDatabaseOptionsFromURI: getDatabaseOptionsFromURI -}; \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/lib/Adapters/Storage/Postgres/PostgresStorageAdapter.js deleted file mode 100644 index 92e92143ba..0000000000 --- a/lib/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ /dev/null @@ -1,1426 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.PostgresStorageAdapter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _PostgresClient = require('./PostgresClient'); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _sql = require('./sql'); - -var _sql2 = _interopRequireDefault(_sql); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } - -var PostgresRelationDoesNotExistError = '42P01'; -var PostgresDuplicateRelationError = '42P07'; -var PostgresDuplicateColumnError = '42701'; -var PostgresDuplicateObjectError = '42710'; -var PostgresUniqueIndexViolationError = '23505'; -var PostgresTransactionAbortedError = '25P02'; -var logger = require('../../../logger'); - -var debug = function debug() { - var args = [].concat(Array.prototype.slice.call(arguments)); - args = ['PG: ' + arguments[0]].concat(args.slice(1, args.length)); - var log = logger.getLogger(); - log.debug.apply(log, args); -}; - -var parseTypeToPostgresType = function 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 'Array': - if (type.contents && type.contents.type === 'String') { - return 'text[]'; - } else { - return 'jsonb'; - } - default: - throw 'no type for ' + JSON.stringify(type) + ' yet'; - } -}; - -var ParseToPosgresComparator = { - '$gt': '>', - '$lt': '<', - '$gte': '>=', - '$lte': '<=' -}; - -var toPostgresValue = function toPostgresValue(value) { - if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object') { - if (value.__type === 'Date') { - return value.iso; - } - if (value.__type === 'File') { - return value.name; - } - } - return value; -}; - -var transformValue = function transformValue(value) { - if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) == 'object' && value.__type === 'Pointer') { - return value.objectId; - } - return value; -}; - -// Duplicate from then mongo adapter... -var emptyCLPS = Object.freeze({ - find: {}, - get: {}, - create: {}, - update: {}, - delete: {}, - addField: {} -}); - -var defaultCLPS = Object.freeze({ - find: { '*': true }, - get: { '*': true }, - create: { '*': true }, - update: { '*': true }, - delete: { '*': true }, - addField: { '*': true } -}); - -var toParseSchema = function toParseSchema(schema) { - if (schema.className === '_User') { - delete schema.fields._hashed_password; - } - if (schema.fields) { - delete schema.fields._wperm; - delete schema.fields._rperm; - } - var clps = defaultCLPS; - if (schema.classLevelPermissions) { - clps = _extends({}, emptyCLPS, schema.classLevelPermissions); - } - return { - className: schema.className, - fields: schema.fields, - classLevelPermissions: clps - }; -}; - -var toPostgresSchema = function 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' } }; - if (schema.className === '_User') { - schema.fields._hashed_password = { type: 'String' }; - schema.fields._password_history = { type: 'Array' }; - } - return schema; -}; - -var handleDotFields = function handleDotFields(object) { - Object.keys(object).forEach(function (fieldName) { - if (fieldName.indexOf('.') > -1) { - var components = fieldName.split('.'); - var first = components.shift(); - object[first] = object[first] || {}; - var currentObj = object[first]; - var next = void 0; - var value = object[fieldName]; - if (value && value.__op === 'Delete') { - value = undefined; - } - /* eslint-disable no-cond-assign */ - while (next = components.shift()) { - /* eslint-enable no-cond-assign */ - currentObj[next] = currentObj[next] || {}; - if (components.length === 0) { - currentObj[next] = value; - } - currentObj = currentObj[next]; - } - delete object[fieldName]; - } - }); - return object; -}; - -var validateKeys = function validateKeys(object) { - if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) == 'object') { - for (var key in object) { - if (_typeof(object[key]) == 'object') { - validateKeys(object[key]); - } - - if (key.includes('$') || key.includes('.')) { - throw new _node2.default.Error(_node2.default.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); - } - } - } -}; - -// Returns the list of join tables on a schema -var joinTablesForSchema = function joinTablesForSchema(schema) { - var list = []; - if (schema) { - Object.keys(schema.fields).forEach(function (field) { - if (schema.fields[field].type === 'Relation') { - list.push('_Join:' + field + ':' + schema.className); - } - }); - } - return list; -}; - -var buildWhereClause = function buildWhereClause(_ref) { - var schema = _ref.schema, - query = _ref.query, - index = _ref.index; - - var patterns = []; - var values = []; - var sorts = []; - - schema = toPostgresSchema(schema); - - var _loop = function _loop(fieldName) { - var isArrayField = schema.fields && schema.fields[fieldName] && schema.fields[fieldName].type === 'Array'; - var initialPatternsLength = patterns.length; - var fieldValue = query[fieldName]; - - // nothingin the schema, it's gonna blow up - if (!schema.fields[fieldName]) { - // as it won't exist - if (fieldValue.$exists === false) { - return 'continue'; - } - } - - if (fieldName.indexOf('.') >= 0) { - var components = fieldName.split('.').map(function (cmpt, index) { - if (index === 0) { - return '"' + cmpt + '"'; - } - return '\'' + cmpt + '\''; - }); - var name = components.slice(0, components.length - 1).join('->'); - name += '->>' + components[components.length - 1]; - patterns.push(name + ' = \'' + fieldValue + '\''); - } else if (typeof fieldValue === 'string') { - patterns.push('$' + index + ':name = $' + (index + 1)); - values.push(fieldName, fieldValue); - index += 2; - } else if (typeof fieldValue === 'boolean') { - patterns.push('$' + index + ':name = $' + (index + 1)); - values.push(fieldName, fieldValue); - index += 2; - } else if (typeof fieldValue === 'number') { - patterns.push('$' + index + ':name = $' + (index + 1)); - values.push(fieldName, fieldValue); - index += 2; - } else if (fieldName === '$or' || fieldName === '$and') { - var _values; - - var clauses = []; - var clauseValues = []; - fieldValue.forEach(function (subQuery) { - var clause = buildWhereClause({ schema: schema, query: subQuery, index: index }); - if (clause.pattern.length > 0) { - clauses.push(clause.pattern); - clauseValues.push.apply(clauseValues, _toConsumableArray(clause.values)); - index += clause.values.length; - } - }); - var orOrAnd = fieldName === '$or' ? ' OR ' : ' AND '; - patterns.push('(' + clauses.join(orOrAnd) + ')'); - (_values = values).push.apply(_values, clauseValues); - } - - if (fieldValue.$ne) { - if (isArrayField) { - fieldValue.$ne = JSON.stringify([fieldValue.$ne]); - patterns.push('NOT array_contains($' + index + ':name, $' + (index + 1) + ')'); - } else { - if (fieldValue.$ne === null) { - patterns.push('$' + index + ':name <> $' + (index + 1)); - } else { - // if not null, we need to manually exclude null - patterns.push('($' + index + ':name <> $' + (index + 1) + ' OR $' + index + ':name IS NULL)'); - } - } - - // TODO: support arrays - values.push(fieldName, fieldValue.$ne); - index += 2; - } - - if (fieldValue.$eq) { - patterns.push('$' + index + ':name = $' + (index + 1)); - values.push(fieldName, fieldValue.$eq); - index += 2; - } - var 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') { - var inPatterns = []; - var allowNull = false; - values.push(fieldName); - fieldValue.$in.forEach(function (listElem, listIndex) { - if (listElem === null) { - allowNull = true; - } else { - values.push(listElem); - inPatterns.push('$' + (index + 1 + listIndex - (allowNull ? 1 : 0))); - } - }); - if (allowNull) { - patterns.push('($' + index + ':name IS NULL OR $' + index + ':name && ARRAY[' + inPatterns.join(',') + '])'); - } else { - patterns.push('$' + index + ':name && ARRAY[' + inPatterns.join(',') + ']'); - } - index = index + 1 + inPatterns.length; - } else if (isInOrNin) { - createConstraint = function createConstraint(baseArray, notIn) { - if (baseArray.length > 0) { - var not = notIn ? ' NOT ' : ''; - if (isArrayField) { - patterns.push(not + ' array_contains($' + index + ':name, $' + (index + 1) + ')'); - values.push(fieldName, JSON.stringify(baseArray)); - index += 2; - } else { - var _inPatterns = []; - values.push(fieldName); - baseArray.forEach(function (listElem, listIndex) { - values.push(listElem); - _inPatterns.push('$' + (index + 1 + listIndex)); - }); - patterns.push('$' + index + ':name ' + not + ' IN (' + _inPatterns.join(',') + ')'); - index = index + 1 + _inPatterns.length; - } - } else if (!notIn) { - values.push(fieldName); - patterns.push('$' + index + ':name IS NULL'); - index = index + 1; - } - }; - - if (fieldValue.$in) { - createConstraint(_lodash2.default.flatMap(fieldValue.$in, function (elt) { - return elt; - }), false); - } - if (fieldValue.$nin) { - createConstraint(_lodash2.default.flatMap(fieldValue.$nin, function (elt) { - return elt; - }), true); - } - } - - if (Array.isArray(fieldValue.$all) && isArrayField) { - patterns.push('array_contains_all($' + index + ':name, $' + (index + 1) + '::jsonb)'); - values.push(fieldName, JSON.stringify(fieldValue.$all)); - index += 2; - } - - if (typeof fieldValue.$exists !== 'undefined') { - if (fieldValue.$exists) { - patterns.push('$' + index + ':name IS NOT NULL'); - } else { - patterns.push('$' + index + ':name IS NULL'); - } - values.push(fieldName); - index += 1; - } - - if (fieldValue.$nearSphere) { - var point = fieldValue.$nearSphere; - var distance = fieldValue.$maxDistance; - var distanceInKM = distance * 6371 * 1000; - patterns.push('ST_distance_sphere($' + index + ':name::geometry, POINT($' + (index + 1) + ', $' + (index + 2) + ')::geometry) <= $' + (index + 3)); - sorts.push('ST_distance_sphere($' + index + ':name::geometry, POINT($' + (index + 1) + ', $' + (index + 2) + ')::geometry) ASC'); - values.push(fieldName, point.longitude, point.latitude, distanceInKM); - index += 4; - } - - if (fieldValue.$within && fieldValue.$within.$box) { - var box = fieldValue.$within.$box; - var left = box[0].longitude; - var bottom = box[0].latitude; - var right = box[1].longitude; - var top = box[1].latitude; - - patterns.push('$' + index + ':name::point <@ $' + (index + 1) + '::box'); - values.push(fieldName, '((' + left + ', ' + bottom + '), (' + right + ', ' + top + '))'); - index += 2; - } - - if (fieldValue.$regex) { - var regex = fieldValue.$regex; - var operator = '~'; - var opts = fieldValue.$options; - if (opts) { - if (opts.indexOf('i') >= 0) { - operator = '~*'; - } - if (opts.indexOf('x') >= 0) { - regex = removeWhiteSpace(regex); - } - } - - regex = processRegexPattern(regex); - - patterns.push('$' + index + ':name ' + operator + ' \'$' + (index + 1) + ':raw\''); - values.push(fieldName, regex); - index += 2; - } - - if (fieldValue.__type === 'Pointer') { - if (isArrayField) { - patterns.push('array_contains($' + index + ':name, $' + (index + 1) + ')'); - values.push(fieldName, JSON.stringify([fieldValue])); - index += 2; - } else { - patterns.push('$' + index + ':name = $' + (index + 1)); - values.push(fieldName, fieldValue.objectId); - index += 2; - } - } - - if (fieldValue.__type === 'Date') { - patterns.push('$' + index + ':name = $' + (index + 1)); - values.push(fieldName, fieldValue.iso); - index += 2; - } - - Object.keys(ParseToPosgresComparator).forEach(function (cmp) { - if (fieldValue[cmp]) { - var pgComparator = ParseToPosgresComparator[cmp]; - patterns.push('$' + index + ':name ' + pgComparator + ' $' + (index + 1)); - values.push(fieldName, toPostgresValue(fieldValue[cmp])); - index += 2; - } - }); - - if (initialPatternsLength === patterns.length) { - throw new _node2.default.Error(_node2.default.Error.OPERATION_FORBIDDEN, 'Postgres doesn\'t support this query type yet ' + JSON.stringify(fieldValue)); - } - }; - - for (var fieldName in query) { - var createConstraint; - - var _ret = _loop(fieldName); - - if (_ret === 'continue') continue; - } - values = values.map(transformValue); - return { pattern: patterns.join(' AND '), values: values, sorts: sorts }; -}; - -var PostgresStorageAdapter = exports.PostgresStorageAdapter = function () { - function PostgresStorageAdapter(_ref2) { - var uri = _ref2.uri, - _ref2$collectionPrefi = _ref2.collectionPrefix, - collectionPrefix = _ref2$collectionPrefi === undefined ? '' : _ref2$collectionPrefi, - databaseOptions = _ref2.databaseOptions; - - _classCallCheck(this, PostgresStorageAdapter); - - this._collectionPrefix = collectionPrefix; - - var _createClient = (0, _PostgresClient.createClient)(uri, databaseOptions), - client = _createClient.client, - pgp = _createClient.pgp; - - this._client = client; - this._pgp = pgp; - } - // Private - - - _createClass(PostgresStorageAdapter, [{ - key: '_ensureSchemaCollectionExists', - value: function _ensureSchemaCollectionExists(conn) { - conn = conn || this._client; - return conn.none('CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )').catch(function (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; - } - }); - } - }, { - key: 'classExists', - value: function classExists(name) { - return this._client.one('SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1)', [name]).then(function (res) { - return res.exists; - }); - } - }, { - key: 'setClassLevelPermissions', - value: function setClassLevelPermissions(className, CLPs) { - var _this = this; - - return this._ensureSchemaCollectionExists().then(function () { - var values = [className, 'schema', 'classLevelPermissions', JSON.stringify(CLPs)]; - return _this._client.none('UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 ', values); - }); - } - }, { - key: 'createClass', - value: function createClass(className, schema) { - var _this2 = this; - - return this._client.tx(function (t) { - var q1 = _this2.createTable(className, schema, t); - var q2 = t.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($, $, true)', { className: className, schema: schema }); - - return t.batch([q1, q2]); - }).then(function () { - return toParseSchema(schema); - }).catch(function (err) { - if (Array.isArray(err.data) && err.data.length > 1 && err.data[0].result.code === PostgresTransactionAbortedError) { - err = err.data[1].result; - } - - if (err.code === PostgresUniqueIndexViolationError && err.detail.includes(className)) { - throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'Class ' + className + ' already exists.'); - } - throw err; - }); - } - - // Just create a table, do not insert in schema - - }, { - key: 'createTable', - value: function createTable(className, schema, conn) { - conn = conn || this._client; - debug('createTable', className, schema); - var valuesArray = []; - var patternsArray = []; - var 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' }; - } - var index = 2; - var relations = []; - Object.keys(fields).forEach(function (fieldName) { - var parseType = fields[fieldName]; - // Skip when it's a relation - // We'll create the tables later - if (parseType.type === 'Relation') { - relations.push(fieldName); - return; - } - if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) { - parseType.contents = { type: 'String' }; - } - valuesArray.push(fieldName); - valuesArray.push(parseTypeToPostgresType(parseType)); - patternsArray.push('$' + index + ':name $' + (index + 1) + ':raw'); - if (fieldName === 'objectId') { - patternsArray.push('PRIMARY KEY ($' + index + ':name)'); - } - index = index + 2; - }); - var qs = 'CREATE TABLE IF NOT EXISTS $1:name (' + patternsArray.join(',') + ')'; - var values = [className].concat(valuesArray); - return this._ensureSchemaCollectionExists(conn).then(function () { - return conn.none(qs, values); - }).catch(function (error) { - if (error.code === PostgresDuplicateRelationError) { - // Table already exists, must have been created by a different request. Ignore error. - } else { - throw error; - } - }).then(function () { - // Create the relation tables - return Promise.all(relations.map(function (fieldName) { - return conn.none('CREATE TABLE IF NOT EXISTS $ ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', { joinTable: '_Join:' + fieldName + ':' + className }); - })); - }); - } - }, { - key: 'addFieldIfNotExists', - value: function addFieldIfNotExists(className, fieldName, type) { - var _this3 = this; - - // TODO: Must be revised for invalid logic... - debug('addFieldIfNotExists', { className: className, fieldName: fieldName, type: type }); - return this._client.tx("addFieldIfNotExists", function (t) { - var promise = Promise.resolve(); - if (type.type !== 'Relation') { - promise = t.none('ALTER TABLE $ ADD COLUMN $ $', { - className: className, - fieldName: fieldName, - postgresType: parseTypeToPostgresType(type) - }).catch(function (error) { - if (error.code === PostgresRelationDoesNotExistError) { - return _this3.createClass(className, { fields: _defineProperty({}, fieldName, type) }); - } else if (error.code === PostgresDuplicateColumnError) { - // Column already exists, created by other request. Carry on to - // See if it's the right type. - } else { - throw error; - } - }); - } else { - promise = t.none('CREATE TABLE IF NOT EXISTS $ ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', { joinTable: '_Join:' + fieldName + ':' + className }); - } - return promise.then(function () { - return t.any('SELECT "schema" FROM "_SCHEMA" WHERE "className" = $ and ("schema"::json->\'fields\'->$) is not null', { className: className, fieldName: fieldName }); - }).then(function (result) { - if (result[0]) { - throw "Attempted to add a field that already exists"; - } else { - var path = '{fields,' + fieldName + '}'; - return t.none('UPDATE "_SCHEMA" SET "schema"=jsonb_set("schema", $, $) WHERE "className"=$', { path: path, type: type, className: className }); - } - }); - }); - } - - // Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.) - // and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible. - - }, { - key: 'deleteClass', - value: function deleteClass(className) { - var _this4 = this; - - return Promise.resolve().then(function () { - var operations = [['DROP TABLE IF EXISTS $1:name', [className]], ['DELETE FROM "_SCHEMA" WHERE "className"=$1', [className]]]; - return _this4._client.tx(function (t) { - return t.batch(operations.map(function (statement) { - return t.none(statement[0], statement[1]); - })); - }); - }).then(function () { - // resolves with false when _Join table - return className.indexOf('_Join:') != 0; - }); - } - - // Delete all data known to this adapter. Used for testing. - - }, { - key: 'deleteAllClasses', - value: function deleteAllClasses() { - var _this5 = this; - - var now = new Date().getTime(); - debug('deleteAllClasses'); - return this._client.any('SELECT * FROM "_SCHEMA"').then(function (results) { - var joins = results.reduce(function (list, schema) { - return list.concat(joinTablesForSchema(schema.schema)); - }, []); - var classes = ['_SCHEMA', '_PushStatus', '_JobStatus', '_Hooks', '_GlobalConfig'].concat(_toConsumableArray(results.map(function (result) { - return result.className; - })), _toConsumableArray(joins)); - return _this5._client.tx(function (t) { - return t.batch(classes.map(function (className) { - return t.none('DROP TABLE IF EXISTS $', { className: className }); - })); - }); - }, function (error) { - if (error.code === PostgresRelationDoesNotExistError) { - // No _SCHEMA collection. Don't delete anything. - return; - } else { - throw error; - } - }).then(function () { - debug('deleteAllClasses done in ' + (new Date().getTime() - now)); - }); - } - - // Remove the column and all the data. For Relations, the _Join collection is handled - // specially, this function does not delete _Join columns. It should, however, indicate - // that the relation fields does not exist anymore. In mongo, this means removing it from - // the _SCHEMA collection. There should be no actual data in the collection under the same name - // as the relation column, so it's fine to attempt to delete it. If the fields listed to be - // deleted do not exist, this function should return successfully anyways. Checking for - // attempts to delete non-existent fields is the responsibility of Parse Server. - - // This function is not obligated to delete fields atomically. It is given the field - // names in a list so that databases that are capable of deleting fields atomically - // may do so. - - // Returns a Promise. - - }, { - key: 'deleteFields', - value: function deleteFields(className, schema, fieldNames) { - var _this6 = this; - - debug('deleteFields', className, fieldNames); - return Promise.resolve().then(function () { - fieldNames = fieldNames.reduce(function (list, fieldName) { - var field = schema.fields[fieldName]; - if (field.type !== 'Relation') { - list.push(fieldName); - } - delete schema.fields[fieldName]; - return list; - }, []); - - var values = [className].concat(_toConsumableArray(fieldNames)); - var columns = fieldNames.map(function (name, idx) { - return '$' + (idx + 2) + ':name'; - }).join(', DROP COLUMN'); - - var doBatch = function doBatch(t) { - var batch = [t.none('UPDATE "_SCHEMA" SET "schema"=$ WHERE "className"=$', { schema: schema, className: className })]; - if (values.length > 1) { - batch.push(t.none('ALTER TABLE $1:name DROP COLUMN ' + columns, values)); - } - return batch; - }; - return _this6._client.tx(function (t) { - return t.batch(doBatch(t)); - }); - }); - } - - // Return a promise for all schemas known to this adapter, in Parse format. In case the - // schemas cannot be retrieved, returns a promise that rejects. Requirements for the - // rejection reason are TBD. - - }, { - key: 'getAllClasses', - value: function getAllClasses() { - var _this7 = this; - - return this._ensureSchemaCollectionExists().then(function () { - return _this7._client.map('SELECT * FROM "_SCHEMA"', null, function (row) { - return _extends({ className: row.className }, row.schema); - }); - }).then(function (res) { - return res.map(toParseSchema); - }); - } - - // Return a promise for the schema with the given name, in Parse format. If - // this adapter doesn't know about the schema, return a promise that rejects with - // undefined as the reason. - - }, { - key: 'getClass', - value: function getClass(className) { - debug('getClass', className); - return this._client.any('SELECT * FROM "_SCHEMA" WHERE "className"=$', { className: className }).then(function (result) { - if (result.length === 1) { - return result[0].schema; - } else { - throw undefined; - } - }).then(toParseSchema); - } - - // TODO: remove the mongo format dependency in the return value - - }, { - key: 'createObject', - value: function createObject(className, schema, object) { - debug('createObject', className, object); - var columnsArray = []; - var valuesArray = []; - schema = toPostgresSchema(schema); - var geoPoints = {}; - - object = handleDotFields(object); - - validateKeys(object); - - Object.keys(object).forEach(function (fieldName) { - var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/); - if (authDataMatch) { - var provider = authDataMatch[1]; - object['authData'] = object['authData'] || {}; - object['authData'][provider] = object[fieldName]; - delete object[fieldName]; - fieldName = 'authData'; - } - - columnsArray.push(fieldName); - if (!schema.fields[fieldName] && className === '_User') { - if (fieldName === '_email_verify_token' || fieldName === '_failed_login_count' || fieldName === '_perishable_token' || fieldName === '_password_history') { - valuesArray.push(object[fieldName]); - } - - if (fieldName === '_email_verify_token_expires_at') { - if (object[fieldName]) { - valuesArray.push(object[fieldName].iso); - } else { - valuesArray.push(null); - } - } - - if (fieldName === '_account_lockout_expires_at' || fieldName === '_perishable_token_expires_at' || fieldName === '_password_changed_at') { - if (object[fieldName]) { - valuesArray.push(object[fieldName].iso); - } else { - valuesArray.push(null); - } - } - 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) { - valuesArray.push(object[fieldName]); - } else { - valuesArray.push(JSON.stringify(object[fieldName])); - } - break; - case 'Object': - case 'String': - case 'Number': - case 'Boolean': - valuesArray.push(object[fieldName]); - break; - case 'File': - valuesArray.push(object[fieldName].name); - 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'; - } - }); - - columnsArray = columnsArray.concat(Object.keys(geoPoints)); - var initialValues = valuesArray.map(function (val, index) { - var termination = ''; - var fieldName = columnsArray[index]; - if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) { - termination = '::text[]'; - } else if (schema.fields[fieldName] && schema.fields[fieldName].type === 'Array') { - termination = '::jsonb'; - } - return '$' + (index + 2 + columnsArray.length) + termination; - }); - var geoPointsInjects = Object.keys(geoPoints).map(function (key) { - var value = geoPoints[key]; - valuesArray.push(value.longitude, value.latitude); - var l = valuesArray.length + columnsArray.length; - return 'POINT($' + l + ', $' + (l + 1) + ')'; - }); - - var columnsPattern = columnsArray.map(function (col, index) { - return '$' + (index + 2) + ':name'; - }).join(','); - var valuesPattern = initialValues.concat(geoPointsInjects).join(','); - - var qs = 'INSERT INTO $1:name (' + columnsPattern + ') VALUES (' + valuesPattern + ')'; - var values = [className].concat(_toConsumableArray(columnsArray), valuesArray); - debug(qs, values); - return this._client.any(qs, values).then(function () { - return { ops: [object] }; - }).catch(function (error) { - if (error.code === PostgresUniqueIndexViolationError) { - throw new _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); - } else { - throw error; - } - }); - } - - // Remove all objects that match the given Parse Query. - // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined. - // If there is some other error, reject with INTERNAL_SERVER_ERROR. - - }, { - key: 'deleteObjectsByQuery', - value: function deleteObjectsByQuery(className, schema, query) { - debug('deleteObjectsByQuery', className, query); - var values = [className]; - var index = 2; - var where = buildWhereClause({ schema: schema, index: index, query: query }); - values.push.apply(values, _toConsumableArray(where.values)); - if (Object.keys(query).length === 0) { - where.pattern = 'TRUE'; - } - var 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, function (a) { - return +a.count; - }).then(function (count) { - if (count === 0) { - throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Object not found.'); - } else { - return count; - } - }); - } - // Return value not currently well specified. - - }, { - key: 'findOneAndUpdate', - value: function findOneAndUpdate(className, schema, query, update) { - debug('findOneAndUpdate', className, query, update); - return this.updateObjectsByQuery(className, schema, query, update).then(function (val) { - return val[0]; - }); - } - - // Apply the update to all objects that match the given Parse Query. - - }, { - key: 'updateObjectsByQuery', - value: function updateObjectsByQuery(className, schema, query, update) { - debug('updateObjectsByQuery', className, query, update); - var updatePatterns = []; - var values = [className]; - var index = 2; - schema = toPostgresSchema(schema); - - var originalUpdate = _extends({}, update); - update = handleDotFields(update); - // Resolve authData first, - // So we don't end up with multiple key updates - for (var fieldName in update) { - var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/); - if (authDataMatch) { - var provider = authDataMatch[1]; - var value = update[fieldName]; - delete update[fieldName]; - update['authData'] = update['authData'] || {}; - update['authData'][provider] = value; - } - } - - var _loop2 = function _loop2(_fieldName) { - var fieldValue = update[_fieldName]; - if (fieldValue === null) { - updatePatterns.push('$' + index + ':name = NULL'); - values.push(_fieldName); - index += 1; - } else if (_fieldName == 'authData') { - // This recursively sets the json_object - // Only 1 level deep - var generate = function generate(jsonb, key, value) { - return 'json_object_set_key(COALESCE(' + jsonb + ', \'{}\'::jsonb), ' + key + ', ' + value + ')::jsonb'; - }; - var lastKey = '$' + index + ':name'; - var fieldNameIndex = index; - index += 1; - values.push(_fieldName); - var _update = Object.keys(fieldValue).reduce(function (lastKey, key) { - var str = generate(lastKey, '$' + index + '::text', '$' + (index + 1) + '::jsonb'); - index += 2; - var value = fieldValue[key]; - if (value) { - if (value.__op === 'Delete') { - value = null; - } else { - value = JSON.stringify(value); - } - } - 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)); - 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)'); - values.push(_fieldName, JSON.stringify(fieldValue.objects)); - index += 2; - } else if (fieldValue.__op === 'Delete') { - 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)'); - 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)'); - 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)); - values.push(_fieldName, fieldValue); - index += 2; - } else if (typeof fieldValue === 'string') { - updatePatterns.push('$' + index + ':name = $' + (index + 1)); - values.push(_fieldName, fieldValue); - index += 2; - } else if (typeof fieldValue === 'boolean') { - updatePatterns.push('$' + index + ':name = $' + (index + 1)); - values.push(_fieldName, fieldValue); - index += 2; - } else if (fieldValue.__type === 'Pointer') { - updatePatterns.push('$' + index + ':name = $' + (index + 1)); - values.push(_fieldName, fieldValue.objectId); - index += 2; - } else if (fieldValue.__type === 'Date') { - updatePatterns.push('$' + index + ':name = $' + (index + 1)); - values.push(_fieldName, toPostgresValue(fieldValue)); - index += 2; - } else if (fieldValue instanceof Date) { - updatePatterns.push('$' + index + ':name = $' + (index + 1)); - values.push(_fieldName, fieldValue); - index += 2; - } else if (fieldValue.__type === 'File') { - updatePatterns.push('$' + index + ':name = $' + (index + 1)); - values.push(_fieldName, toPostgresValue(fieldValue)); - index += 2; - } else if (fieldValue.__type === 'GeoPoint') { - updatePatterns.push('$' + index + ':name = POINT($' + (index + 1) + ', $' + (index + 2) + ')'); - values.push(_fieldName, fieldValue.latitude, fieldValue.longitude); - index += 3; - } else if (fieldValue.__type === 'Relation') { - // noop - } else if (typeof fieldValue === 'number') { - updatePatterns.push('$' + index + ':name = $' + (index + 1)); - values.push(_fieldName, fieldValue); - index += 2; - } else if ((typeof fieldValue === 'undefined' ? 'undefined' : _typeof(fieldValue)) === 'object' && schema.fields[_fieldName] && schema.fields[_fieldName].type === 'Object') { - // Gather keys to increment - var keysToIncrement = Object.keys(originalUpdate).filter(function (k) { - // choose top level fields that have a delete operation set - return originalUpdate[k].__op === 'Increment' && k.split('.').length === 2 && k.split(".")[0] === _fieldName; - }).map(function (k) { - return k.split('.')[1]; - }); - - var incrementPatterns = ''; - if (keysToIncrement.length > 0) { - incrementPatterns = ' || ' + keysToIncrement.map(function (c) { - var amount = fieldValue[c].amount; - return 'CONCAT(\'{"' + c + '":\', COALESCE($' + index + ':name->>\'' + c + '\',\'0\')::int + ' + amount + ', \'}\')::jsonb'; - }).join(' || '); - // Strip the keys - keysToIncrement.forEach(function (key) { - delete fieldValue[key]; - }); - } - - var keysToDelete = Object.keys(originalUpdate).filter(function (k) { - // choose top level fields that have a delete operation set - return originalUpdate[k].__op === 'Delete' && k.split('.').length === 2 && k.split(".")[0] === _fieldName; - }).map(function (k) { - return k.split('.')[1]; - }); - - var deletePatterns = keysToDelete.reduce(function (p, c, i) { - return p + (' - \'$' + (index + 1 + i) + ':value\''); - }, ''); - - updatePatterns.push('$' + index + ':name = ( COALESCE($' + index + ':name, \'{}\'::jsonb) ' + deletePatterns + ' ' + incrementPatterns + ' || $' + (index + 1 + keysToDelete.length) + '::jsonb )'); - - values.push.apply(values, [_fieldName].concat(_toConsumableArray(keysToDelete), [JSON.stringify(fieldValue)])); - index += 2 + keysToDelete.length; - } else if (Array.isArray(fieldValue) && schema.fields[_fieldName] && schema.fields[_fieldName].type === 'Array') { - var expectedType = parseTypeToPostgresType(schema.fields[_fieldName]); - if (expectedType === 'text[]') { - updatePatterns.push('$' + index + ':name = $' + (index + 1) + '::text[]'); - } else { - var type = 'text'; - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = fieldValue[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var elt = _step.value; - - if ((typeof elt === 'undefined' ? 'undefined' : _typeof(elt)) == 'object') { - type = 'json'; - break; - } - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - 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 { - v: Promise.reject(new _node2.default.Error(_node2.default.Error.OPERATION_FORBIDDEN, 'Postgres doesn\'t support update ' + JSON.stringify(fieldValue) + ' yet')) - }; - } - }; - - for (var _fieldName in update) { - var _ret2 = _loop2(_fieldName); - - if ((typeof _ret2 === 'undefined' ? 'undefined' : _typeof(_ret2)) === "object") return _ret2.v; - } - - var where = buildWhereClause({ schema: schema, index: index, query: query }); - values.push.apply(values, _toConsumableArray(where.values)); - - var qs = 'UPDATE $1:name SET ' + updatePatterns.join(',') + ' WHERE ' + where.pattern + ' RETURNING *'; - debug('update: ', qs, values); - return this._client.any(qs, values); // TODO: This is unsafe, verification is needed, or a different query method; - } - - // Hopefully, we can get rid of this. It's only used for config and hooks. - - }, { - key: 'upsertOneObject', - value: function upsertOneObject(className, schema, query, update) { - var _this8 = this; - - debug('upsertOneObject', { className: className, query: query, update: update }); - var createValue = Object.assign({}, query, update); - return this.createObject(className, schema, createValue).catch(function (err) { - // ignore duplicate value errors as it's upsert - if (err.code === _node2.default.Error.DUPLICATE_VALUE) { - return _this8.findOneAndUpdate(className, schema, query, update); - } - throw err; - }); - } - }, { - key: 'find', - value: function find(className, schema, query, _ref3) { - var _values2; - - var skip = _ref3.skip, - limit = _ref3.limit, - sort = _ref3.sort, - keys = _ref3.keys; - - debug('find', className, query, { skip: skip, limit: limit, sort: sort, keys: keys }); - var hasLimit = limit !== undefined; - var hasSkip = skip !== undefined; - var values = [className]; - var where = buildWhereClause({ schema: schema, query: query, index: 2 }); - (_values2 = values).push.apply(_values2, _toConsumableArray(where.values)); - - var wherePattern = where.pattern.length > 0 ? 'WHERE ' + where.pattern : ''; - var limitPattern = hasLimit ? 'LIMIT $' + (values.length + 1) : ''; - if (hasLimit) { - values.push(limit); - } - var skipPattern = hasSkip ? 'OFFSET $' + (values.length + 1) : ''; - if (hasSkip) { - values.push(skip); - } - - var sortPattern = ''; - if (sort) { - var sorting = Object.keys(sort).map(function (key) { - // Using $idx pattern gives: non-integer constant in ORDER BY - if (sort[key] === 1) { - return '"' + key + '" ASC'; - } - return '"' + key + '" DESC'; - }).join(','); - sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? 'ORDER BY ' + sorting : ''; - } - if (where.sorts && Object.keys(where.sorts).length > 0) { - sortPattern = 'ORDER BY ' + where.sorts.join(','); - } - - var columns = '*'; - if (keys) { - // Exclude empty keys - keys = keys.filter(function (key) { - return key.length > 0; - }); - columns = keys.map(function (key, index) { - return '$' + (index + values.length + 1) + ':name'; - }).join(','); - values = values.concat(keys); - } - - var qs = 'SELECT ' + columns + ' FROM $1:name ' + wherePattern + ' ' + sortPattern + ' ' + limitPattern + ' ' + skipPattern; - debug(qs, values); - return this._client.any(qs, values).catch(function (err) { - // Query on non existing table, don't crash - if (err.code === PostgresRelationDoesNotExistError) { - return []; - } - return Promise.reject(err); - }).then(function (results) { - return results.map(function (object) { - Object.keys(schema.fields).forEach(function (fieldName) { - if (schema.fields[fieldName].type === 'Pointer' && object[fieldName]) { - 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 - }; - } - if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') { - object[fieldName] = { - __type: "GeoPoint", - latitude: object[fieldName].y, - longitude: object[fieldName].x - }; - } - if (object[fieldName] && schema.fields[fieldName].type === 'File') { - object[fieldName] = { - __type: 'File', - 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. - if (object.createdAt) { - object.createdAt = object.createdAt.toISOString(); - } - if (object.updatedAt) { - object.updatedAt = object.updatedAt.toISOString(); - } - if (object.expiresAt) { - object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() }; - } - if (object._email_verify_token_expires_at) { - 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() }; - } - if (object._perishable_token_expires_at) { - 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() }; - } - - for (var fieldName in object) { - if (object[fieldName] === null) { - delete object[fieldName]; - } - if (object[fieldName] instanceof Date) { - object[fieldName] = { __type: 'Date', iso: object[fieldName].toISOString() }; - } - } - - return object; - }); - }); - } - - // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't - // currently know which fields are nullable and which aren't, we ignore that criteria. - // As such, we shouldn't expose this function to users of parse until we have an out-of-band - // Way of determining if a field is nullable. Undefined doesn't count against uniqueness, - // which is why we use sparse indexes. - - }, { - key: 'ensureUniqueness', - value: function ensureUniqueness(className, schema, fieldNames) { - // Use the same name for every ensureUniqueness attempt, because postgres - // Will happily create the same index with multiple names. - var constraintName = 'unique_' + fieldNames.sort().join('_'); - var constraintPatterns = fieldNames.map(function (fieldName, index) { - return '$' + (index + 3) + ':name'; - }); - var qs = 'ALTER TABLE $1:name ADD CONSTRAINT $2:name UNIQUE (' + constraintPatterns.join(',') + ')'; - return this._client.none(qs, [className, constraintName].concat(_toConsumableArray(fieldNames))).catch(function (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 _node2.default.Error(_node2.default.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); - } else { - throw error; - } - }); - } - - // Executes a count. - - }, { - key: 'count', - value: function count(className, schema, query) { - debug('count', className, query); - var values = [className]; - var where = buildWhereClause({ schema: schema, query: query, index: 2 }); - values.push.apply(values, _toConsumableArray(where.values)); - - var wherePattern = where.pattern.length > 0 ? 'WHERE ' + where.pattern : ''; - var qs = 'SELECT count(*) FROM $1:name ' + wherePattern; - return this._client.one(qs, values, function (a) { - return +a.count; - }).catch(function (err) { - if (err.code === PostgresRelationDoesNotExistError) { - return 0; - } - throw err; - }); - } - }, { - key: 'performInitialization', - value: function performInitialization(_ref4) { - var _this9 = this; - - var VolatileClassesSchemas = _ref4.VolatileClassesSchemas; - - debug('performInitialization'); - var promises = VolatileClassesSchemas.map(function (schema) { - return _this9.createTable(schema.className, schema).catch(function (err) { - if (err.code === PostgresDuplicateRelationError || err.code === _node2.default.Error.INVALID_CLASS_NAME) { - return Promise.resolve(); - } - throw err; - }); - }); - return Promise.all(promises).then(function () { - return _this9._client.tx(function (t) { - return t.batch([t.none(_sql2.default.misc.jsonObjectSetKeys), t.none(_sql2.default.array.add), t.none(_sql2.default.array.addUnique), t.none(_sql2.default.array.remove), t.none(_sql2.default.array.containsAll), t.none(_sql2.default.array.contains)]); - }); - }).then(function (data) { - debug('initializationDone in ' + data.duration); - }).catch(function (error) { - /* eslint-disable no-console */ - console.error(error); - }); - } - }]); - - return PostgresStorageAdapter; -}(); - -function removeWhiteSpace(regex) { - 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(); -} - -function processRegexPattern(s) { - 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)) + '$'; - } - - // regex for contains - return literalizeRegexPart(s); -} - -function createLiteralRegex(remaining) { - return remaining.split('').map(function (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) { - var matcher1 = /\\Q((?!\\E).*)\\E$/; - var result1 = s.match(matcher1); - if (result1 && result1.length > 1 && result1.index > -1) { - // process regex that has a beginning and an end specified for the literal text - var prefix = s.substr(0, result1.index); - var remaining = result1[1]; - - return literalizeRegexPart(prefix) + createLiteralRegex(remaining); - } - - // process regex that has a beginning specified for the literal text - var matcher2 = /\\Q((?!\\E).*)$/; - var result2 = s.match(matcher2); - if (result2 && result2.length > 1 && result2.index > -1) { - var _prefix = s.substr(0, result2.index); - var _remaining = result2[1]; - - return literalizeRegexPart(_prefix) + createLiteralRegex(_remaining); - } - - // 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'); -} - -exports.default = PostgresStorageAdapter; - -module.exports = PostgresStorageAdapter; // Required for tests \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/sql/array/add-unique.sql b/lib/Adapters/Storage/Postgres/sql/array/add-unique.sql deleted file mode 100644 index aad90d45f5..0000000000 --- a/lib/Adapters/Storage/Postgres/sql/array/add-unique.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE OR REPLACE FUNCTION array_add_unique( - "array" jsonb, - "values" jsonb -) - RETURNS jsonb - LANGUAGE sql - IMMUTABLE - STRICT -AS $function$ - SELECT array_to_json(ARRAY(SELECT DISTINCT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT DISTINCT jsonb_array_elements("values")))))::jsonb; -$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/array/add.sql b/lib/Adapters/Storage/Postgres/sql/array/add.sql deleted file mode 100644 index a0b5859908..0000000000 --- a/lib/Adapters/Storage/Postgres/sql/array/add.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE OR REPLACE FUNCTION array_add( - "array" jsonb, - "values" jsonb -) - RETURNS jsonb - LANGUAGE sql - IMMUTABLE - STRICT -AS $function$ - SELECT array_to_json(ARRAY(SELECT unnest(ARRAY(SELECT DISTINCT jsonb_array_elements("array")) || ARRAY(SELECT jsonb_array_elements("values")))))::jsonb; -$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/array/contains-all.sql b/lib/Adapters/Storage/Postgres/sql/array/contains-all.sql deleted file mode 100644 index 24355bc732..0000000000 --- a/lib/Adapters/Storage/Postgres/sql/array/contains-all.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE OR REPLACE FUNCTION array_contains_all( - "array" jsonb, - "values" jsonb -) - RETURNS boolean - LANGUAGE sql - IMMUTABLE - STRICT -AS $function$ - SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES; -$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/array/contains.sql b/lib/Adapters/Storage/Postgres/sql/array/contains.sql deleted file mode 100644 index f7c458782e..0000000000 --- a/lib/Adapters/Storage/Postgres/sql/array/contains.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE OR REPLACE FUNCTION array_contains( - "array" jsonb, - "values" jsonb -) - RETURNS boolean - LANGUAGE sql - IMMUTABLE - STRICT -AS $function$ - SELECT RES.CNT >= 1 FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES; -$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/array/remove.sql b/lib/Adapters/Storage/Postgres/sql/array/remove.sql deleted file mode 100644 index 52895d2f46..0000000000 --- a/lib/Adapters/Storage/Postgres/sql/array/remove.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE OR REPLACE FUNCTION array_remove( - "array" jsonb, - "values" jsonb -) - RETURNS jsonb - LANGUAGE sql - IMMUTABLE - STRICT -AS $function$ - SELECT array_to_json(ARRAY(SELECT * FROM jsonb_array_elements("array") as elt WHERE elt NOT IN (SELECT * FROM (SELECT jsonb_array_elements("values")) AS sub)))::jsonb; -$function$; diff --git a/lib/Adapters/Storage/Postgres/sql/index.js b/lib/Adapters/Storage/Postgres/sql/index.js deleted file mode 100644 index c1b31b93ac..0000000000 --- a/lib/Adapters/Storage/Postgres/sql/index.js +++ /dev/null @@ -1,32 +0,0 @@ -'use strict'; - -var QueryFile = require('pg-promise').QueryFile; -var path = require('path'); - -module.exports = { - array: { - add: sql('array/add.sql'), - addUnique: sql('array/add-unique.sql'), - contains: sql('array/contains.sql'), - containsAll: sql('array/contains-all.sql'), - remove: sql('array/remove.sql') - }, - misc: { - jsonObjectSetKeys: sql('misc/json-object-set-keys.sql') - } -}; - -/////////////////////////////////////////////// -// Helper for linking to external query files; -function sql(file) { - - var fullPath = path.join(__dirname, file); // generating full path; - - var qf = new QueryFile(fullPath, { minify: true }); - - if (qf.error) { - throw qf.error; - } - - return qf; -} \ No newline at end of file diff --git a/lib/Adapters/Storage/Postgres/sql/misc/json-object-set-keys.sql b/lib/Adapters/Storage/Postgres/sql/misc/json-object-set-keys.sql deleted file mode 100644 index eb28b36928..0000000000 --- a/lib/Adapters/Storage/Postgres/sql/misc/json-object-set-keys.sql +++ /dev/null @@ -1,19 +0,0 @@ --- Function to set a key on a nested JSON document - -CREATE OR REPLACE FUNCTION json_object_set_key( - "json" jsonb, - key_to_set TEXT, - value_to_set anyelement -) - RETURNS jsonb - LANGUAGE sql - IMMUTABLE - STRICT -AS $function$ -SELECT concat('{', string_agg(to_json("key") || ':' || "value", ','), '}')::jsonb - FROM (SELECT * - FROM jsonb_each("json") - WHERE key <> key_to_set - UNION ALL - SELECT key_to_set, to_json("value_to_set")::jsonb) AS fields -$function$; diff --git a/lib/Auth.js b/lib/Auth.js deleted file mode 100644 index ccbd777f90..0000000000 --- a/lib/Auth.js +++ /dev/null @@ -1,241 +0,0 @@ -'use strict'; - -function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } - -var Parse = require('parse/node').Parse; -var RestQuery = require('./RestQuery'); - -// 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() { - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, - config = _ref.config, - _ref$isMaster = _ref.isMaster, - isMaster = _ref$isMaster === undefined ? false : _ref$isMaster, - user = _ref.user, - installationId = _ref.installationId; - - this.config = config; - this.installationId = installationId; - this.isMaster = isMaster; - this.user = user; - - // Assuming a users roles won't change during a single request, we'll - // only load them once. - this.userRoles = []; - this.fetchedRoles = false; - this.rolePromise = null; -} - -// Whether this auth could possibly modify the given user id. -// It still could be forbidden via ACLs even if this returns true. -Auth.prototype.couldUpdateUserId = function (userId) { - if (this.isMaster) { - return true; - } - if (this.user && this.user.id === userId) { - return true; - } - return false; -}; - -// A helper to get a master-level Auth object -function master(config) { - return new Auth({ config: config, isMaster: true }); -} - -// A helper to get a nobody-level Auth object -function nobody(config) { - return new Auth({ config: config, isMaster: false }); -} - -// Returns a promise that resolves to an Auth object -var getAuthForSessionToken = function getAuthForSessionToken() { - var _ref2 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, - config = _ref2.config, - sessionToken = _ref2.sessionToken, - installationId = _ref2.installationId; - - return config.cacheController.user.get(sessionToken).then(function (userJSON) { - if (userJSON) { - var cachedUser = Parse.Object.fromJSON(userJSON); - return Promise.resolve(new Auth({ config: config, isMaster: false, installationId: installationId, user: cachedUser })); - } - - var restOptions = { - limit: 1, - include: 'user' - }; - - var query = new RestQuery(config, master(config), '_Session', { sessionToken: sessionToken }, restOptions); - return query.execute().then(function (response) { - var results = response.results; - if (results.length !== 1 || !results[0]['user']) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid session token'); - } - - var now = new Date(), - expiresAt = results[0].expiresAt ? new Date(results[0].expiresAt.iso) : undefined; - if (expiresAt < now) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'Session token is expired.'); - } - var obj = results[0]['user']; - delete obj.password; - obj['className'] = '_User'; - obj['sessionToken'] = sessionToken; - config.cacheController.user.put(sessionToken, obj); - var userObject = Parse.Object.fromJSON(obj); - return new Auth({ config: config, isMaster: false, installationId: installationId, user: userObject }); - }); - }); -}; - -var getAuthForLegacySessionToken = function getAuthForLegacySessionToken() { - var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, - config = _ref3.config, - sessionToken = _ref3.sessionToken, - installationId = _ref3.installationId; - - var restOptions = { - limit: 1 - }; - var query = new RestQuery(config, master(config), '_User', { sessionToken: sessionToken }, restOptions); - return query.execute().then(function (response) { - var results = response.results; - if (results.length !== 1) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'invalid legacy session token'); - } - var obj = results[0]; - obj.className = '_User'; - var userObject = Parse.Object.fromJSON(obj); - return new Auth({ config: config, isMaster: false, installationId: installationId, user: userObject }); - }); -}; - -// Returns a promise that resolves to an array of role names -Auth.prototype.getUserRoles = function () { - if (this.isMaster || !this.user) { - return Promise.resolve([]); - } - if (this.fetchedRoles) { - return Promise.resolve(this.userRoles); - } - if (this.rolePromise) { - return this.rolePromise; - } - this.rolePromise = this._loadRoles(); - return this.rolePromise; -}; - -// Iterates through the role tree and compiles a users roles -Auth.prototype._loadRoles = function () { - var _this = this; - - var cacheAdapter = this.config.cacheController; - return cacheAdapter.role.get(this.user.id).then(function (cachedRoles) { - if (cachedRoles != null) { - _this.fetchedRoles = true; - _this.userRoles = cachedRoles; - return Promise.resolve(cachedRoles); - } - - var restWhere = { - 'users': { - __type: 'Pointer', - className: '_User', - objectId: _this.user.id - } - }; - // First get the role ids this user is directly a member of - var query = new RestQuery(_this.config, master(_this.config), '_Role', restWhere, {}); - return query.execute().then(function (response) { - var results = response.results; - if (!results.length) { - _this.userRoles = []; - _this.fetchedRoles = true; - _this.rolePromise = null; - - cacheAdapter.role.put(_this.user.id, _this.userRoles); - return Promise.resolve(_this.userRoles); - } - var rolesMap = results.reduce(function (m, r) { - m.names.push(r.name); - m.ids.push(r.objectId); - return m; - }, { ids: [], names: [] }); - - // run the recursive finding - return _this._getAllRolesNamesForRoleIds(rolesMap.ids, rolesMap.names).then(function (roleNames) { - _this.userRoles = roleNames.map(function (r) { - return 'role:' + r; - }); - _this.fetchedRoles = true; - _this.rolePromise = null; - - cacheAdapter.role.put(_this.user.id, _this.userRoles); - return Promise.resolve(_this.userRoles); - }); - }); - }); -}; - -// Given a list of roleIds, find all the parent roles, returns a promise with all names -Auth.prototype._getAllRolesNamesForRoleIds = function (roleIDs) { - var _this2 = this; - - var names = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; - var queriedRoles = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - - var ins = roleIDs.filter(function (roleID) { - return queriedRoles[roleID] !== true; - }).map(function (roleID) { - // mark as queried - queriedRoles[roleID] = true; - return { - __type: 'Pointer', - className: '_Role', - objectId: roleID - }; - }); - - // all roles are accounted for, return the names - if (ins.length == 0) { - return Promise.resolve([].concat(_toConsumableArray(new Set(names)))); - } - // Build an OR query across all parentRoles - var restWhere = void 0; - if (ins.length == 1) { - restWhere = { 'roles': ins[0] }; - } else { - restWhere = { 'roles': { '$in': ins } }; - } - var query = new RestQuery(this.config, master(this.config), '_Role', restWhere, {}); - return query.execute().then(function (response) { - var results = response.results; - // Nothing found - if (!results.length) { - return Promise.resolve(names); - } - // Map the results with all Ids and names - var resultMap = results.reduce(function (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 _this2._getAllRolesNamesForRoleIds(resultMap.ids, names, queriedRoles); - }).then(function (names) { - return Promise.resolve([].concat(_toConsumableArray(new Set(names)))); - }); -}; - -module.exports = { - Auth: Auth, - master: master, - nobody: nobody, - getAuthForSessionToken: getAuthForSessionToken, - getAuthForLegacySessionToken: getAuthForLegacySessionToken -}; \ No newline at end of file diff --git a/lib/ClientSDK.js b/lib/ClientSDK.js deleted file mode 100644 index ea73b24ccf..0000000000 --- a/lib/ClientSDK.js +++ /dev/null @@ -1,42 +0,0 @@ -'use strict'; - -var semver = require('semver'); - -function compatible(compatibleSDK) { - return function (clientSDK) { - if (typeof clientSDK === 'string') { - clientSDK = fromString(clientSDK); - } - // REST API, or custom SDK - if (!clientSDK) { - return true; - } - var clientVersion = clientSDK.version; - var compatiblityVersion = compatibleSDK[clientSDK.sdk]; - return semver.satisfies(clientVersion, compatiblityVersion); - }; -} - -function supportsForwardDelete(clientSDK) { - return compatible({ - js: '>=1.9.0' - })(clientSDK); -} - -function fromString(version) { - var versionRE = /([-a-zA-Z]+)([0-9\.]+)/; - var match = version.toLowerCase().match(versionRE); - if (match && match.length === 3) { - return { - sdk: match[1], - version: match[2] - }; - } - return undefined; -} - -module.exports = { - compatible: compatible, - supportsForwardDelete: supportsForwardDelete, - fromString: fromString -}; \ No newline at end of file diff --git a/lib/Config.js b/lib/Config.js deleted file mode 100644 index 185a64e58b..0000000000 --- a/lib/Config.js +++ /dev/null @@ -1,324 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Config = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // A Config object provides information about how a specific app is -// configured. -// mount is the URL for the root of the API; includes http, domain, etc. - -var _cache = require('./cache'); - -var _cache2 = _interopRequireDefault(_cache); - -var _SchemaCache = require('./Controllers/SchemaCache'); - -var _SchemaCache2 = _interopRequireDefault(_SchemaCache); - -var _DatabaseController = require('./Controllers/DatabaseController'); - -var _DatabaseController2 = _interopRequireDefault(_DatabaseController); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function removeTrailingSlash(str) { - if (!str) { - return str; - } - if (str.endsWith("/")) { - str = str.substr(0, str.length - 1); - } - return str; -} - -var Config = exports.Config = function () { - function Config(applicationId, mount) { - _classCallCheck(this, Config); - - var cacheInfo = _cache2.default.get(applicationId); - if (!cacheInfo) { - return; - } - - this.applicationId = applicationId; - this.jsonLogs = cacheInfo.jsonLogs; - this.masterKey = cacheInfo.masterKey; - this.clientKey = cacheInfo.clientKey; - this.javascriptKey = cacheInfo.javascriptKey; - this.dotNetKey = cacheInfo.dotNetKey; - this.restAPIKey = cacheInfo.restAPIKey; - this.webhookKey = cacheInfo.webhookKey; - this.fileKey = cacheInfo.fileKey; - this.allowClientClassCreation = cacheInfo.allowClientClassCreation; - this.userSensitiveFields = cacheInfo.userSensitiveFields; - - // Create a new DatabaseController per request - if (cacheInfo.databaseController) { - var schemaCache = new _SchemaCache2.default(cacheInfo.cacheController, cacheInfo.schemaCacheTTL, cacheInfo.enableSingleSchemaCache); - this.database = new _DatabaseController2.default(cacheInfo.databaseController.adapter, schemaCache); - } - - this.schemaCacheTTL = cacheInfo.schemaCacheTTL; - this.enableSingleSchemaCache = cacheInfo.enableSingleSchemaCache; - - this.serverURL = cacheInfo.serverURL; - this.publicServerURL = removeTrailingSlash(cacheInfo.publicServerURL); - this.verifyUserEmails = cacheInfo.verifyUserEmails; - this.preventLoginWithUnverifiedEmail = cacheInfo.preventLoginWithUnverifiedEmail; - this.emailVerifyTokenValidityDuration = cacheInfo.emailVerifyTokenValidityDuration; - this.accountLockout = cacheInfo.accountLockout; - this.passwordPolicy = cacheInfo.passwordPolicy; - this.appName = cacheInfo.appName; - - this.analyticsController = cacheInfo.analyticsController; - this.cacheController = cacheInfo.cacheController; - this.hooksController = cacheInfo.hooksController; - this.filesController = cacheInfo.filesController; - this.pushController = cacheInfo.pushController; - this.pushControllerQueue = cacheInfo.pushControllerQueue; - this.pushWorker = cacheInfo.pushWorker; - this.hasPushSupport = cacheInfo.hasPushSupport; - this.hasPushScheduledSupport = cacheInfo.hasPushScheduledSupport; - this.loggerController = cacheInfo.loggerController; - this.userController = cacheInfo.userController; - this.authDataManager = cacheInfo.authDataManager; - this.customPages = cacheInfo.customPages || {}; - this.mount = removeTrailingSlash(mount); - this.liveQueryController = cacheInfo.liveQueryController; - this.sessionLength = cacheInfo.sessionLength; - this.expireInactiveSessions = cacheInfo.expireInactiveSessions; - this.generateSessionExpiresAt = this.generateSessionExpiresAt.bind(this); - this.generateEmailVerifyTokenExpiresAt = this.generateEmailVerifyTokenExpiresAt.bind(this); - this.revokeSessionOnPasswordReset = cacheInfo.revokeSessionOnPasswordReset; - } - - _createClass(Config, [{ - key: 'generateEmailVerifyTokenExpiresAt', - value: function generateEmailVerifyTokenExpiresAt() { - if (!this.verifyUserEmails || !this.emailVerifyTokenValidityDuration) { - return undefined; - } - var now = new Date(); - return new Date(now.getTime() + this.emailVerifyTokenValidityDuration * 1000); - } - }, { - key: 'generatePasswordResetTokenExpiresAt', - value: function generatePasswordResetTokenExpiresAt() { - if (!this.passwordPolicy || !this.passwordPolicy.resetTokenValidityDuration) { - return undefined; - } - var now = new Date(); - return new Date(now.getTime() + this.passwordPolicy.resetTokenValidityDuration * 1000); - } - }, { - key: 'generateSessionExpiresAt', - value: function generateSessionExpiresAt() { - if (!this.expireInactiveSessions) { - return undefined; - } - var now = new Date(); - return new Date(now.getTime() + this.sessionLength * 1000); - } - }, { - key: 'mount', - get: function get() { - var mount = this._mount; - if (this.publicServerURL) { - mount = this.publicServerURL; - } - return mount; - }, - set: function set(newValue) { - this._mount = newValue; - } - }, { - key: 'invalidLinkURL', - get: function get() { - return this.customPages.invalidLink || this.publicServerURL + '/apps/invalid_link.html'; - } - }, { - key: 'invalidVerificationLinkURL', - get: function get() { - return this.customPages.invalidVerificationLink || this.publicServerURL + '/apps/invalid_verification_link.html'; - } - }, { - key: 'linkSendSuccessURL', - get: function get() { - return this.customPages.linkSendSuccess || this.publicServerURL + '/apps/link_send_success.html'; - } - }, { - key: 'linkSendFailURL', - get: function get() { - return this.customPages.linkSendFail || this.publicServerURL + '/apps/link_send_fail.html'; - } - }, { - key: 'verifyEmailSuccessURL', - get: function get() { - return this.customPages.verifyEmailSuccess || this.publicServerURL + '/apps/verify_email_success.html'; - } - }, { - key: 'choosePasswordURL', - get: function get() { - return this.customPages.choosePassword || this.publicServerURL + '/apps/choose_password'; - } - }, { - key: 'requestResetPasswordURL', - get: function get() { - return this.publicServerURL + '/apps/' + this.applicationId + '/request_password_reset'; - } - }, { - key: 'passwordResetSuccessURL', - get: function get() { - return this.customPages.passwordResetSuccess || this.publicServerURL + '/apps/password_reset_success.html'; - } - }, { - key: 'parseFrameURL', - get: function get() { - return this.customPages.parseFrameURL; - } - }, { - key: 'verifyEmailURL', - get: function get() { - return this.publicServerURL + '/apps/' + this.applicationId + '/verify_email'; - } - }], [{ - key: 'validate', - value: function validate(_ref) { - var verifyUserEmails = _ref.verifyUserEmails, - userController = _ref.userController, - appName = _ref.appName, - publicServerURL = _ref.publicServerURL, - revokeSessionOnPasswordReset = _ref.revokeSessionOnPasswordReset, - expireInactiveSessions = _ref.expireInactiveSessions, - sessionLength = _ref.sessionLength, - emailVerifyTokenValidityDuration = _ref.emailVerifyTokenValidityDuration, - accountLockout = _ref.accountLockout, - passwordPolicy = _ref.passwordPolicy; - - var emailAdapter = userController.adapter; - if (verifyUserEmails) { - this.validateEmailConfiguration({ emailAdapter: emailAdapter, appName: appName, publicServerURL: publicServerURL, emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration }); - } - - this.validateAccountLockoutPolicy(accountLockout); - - this.validatePasswordPolicy(passwordPolicy); - - if (typeof revokeSessionOnPasswordReset !== 'boolean') { - throw 'revokeSessionOnPasswordReset must be a boolean value'; - } - - if (publicServerURL) { - if (!publicServerURL.startsWith("http://") && !publicServerURL.startsWith("https://")) { - throw "publicServerURL should be a valid HTTPS URL starting with https://"; - } - } - - this.validateSessionConfiguration(sessionLength, expireInactiveSessions); - } - }, { - key: 'validateAccountLockoutPolicy', - value: function validateAccountLockoutPolicy(accountLockout) { - if (accountLockout) { - 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) { - throw 'Account lockout threshold should be an integer greater than 0 and less than 1000'; - } - } - } - }, { - key: 'validatePasswordPolicy', - value: function validatePasswordPolicy(passwordPolicy) { - if (passwordPolicy) { - 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)) { - 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)) { - throw 'passwordPolicy.validatorPattern must be a regex string or RegExp object.'; - } - } - - if (passwordPolicy.validatorCallback && typeof passwordPolicy.validatorCallback !== 'function') { - throw 'passwordPolicy.validatorCallback must be a function.'; - } - - 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)) { - throw 'passwordPolicy.maxPasswordHistory must be an integer ranging 0 - 20'; - } - } - } - - // if the passwordPolicy.validatorPattern is configured then setup a callback to process the pattern - - }, { - key: 'setupPasswordValidator', - value: function setupPasswordValidator(passwordPolicy) { - if (passwordPolicy && passwordPolicy.validatorPattern) { - passwordPolicy.patternValidator = function (value) { - return passwordPolicy.validatorPattern.test(value); - }; - } - } - }, { - key: 'validateEmailConfiguration', - value: function validateEmailConfiguration(_ref2) { - var emailAdapter = _ref2.emailAdapter, - appName = _ref2.appName, - publicServerURL = _ref2.publicServerURL, - emailVerifyTokenValidityDuration = _ref2.emailVerifyTokenValidityDuration; - - if (!emailAdapter) { - throw 'An emailAdapter is required for e-mail verification and password resets.'; - } - if (typeof appName !== 'string') { - throw 'An app name is required for e-mail verification and password resets.'; - } - if (typeof publicServerURL !== 'string') { - throw 'A public server url is required for e-mail verification and password resets.'; - } - if (emailVerifyTokenValidityDuration) { - 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.'; - } - } - } - }, { - key: 'validateSessionConfiguration', - value: function validateSessionConfiguration(sessionLength, expireInactiveSessions) { - if (expireInactiveSessions) { - if (isNaN(sessionLength)) { - throw 'Session length must be a valid number.'; - } else if (sessionLength <= 0) { - throw 'Session length must be a value greater than 0.'; - } - } - } - }]); - - return Config; -}(); - -exports.default = Config; - -module.exports = Config; \ No newline at end of file diff --git a/lib/Controllers/AdaptableController.js b/lib/Controllers/AdaptableController.js deleted file mode 100644 index a1501dca19..0000000000 --- a/lib/Controllers/AdaptableController.js +++ /dev/null @@ -1,101 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.AdaptableController = undefined; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _Config = require("../Config"); - -var _Config2 = _interopRequireDefault(_Config); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -/* -AdaptableController.js - -AdaptableController is the base class for all controllers -that support adapter, -The super class takes care of creating the right instance for the adapter -based on the parameters passed - - */ - -// _adapter is private, use Symbol -var _adapter = Symbol(); - -var AdaptableController = exports.AdaptableController = function () { - function AdaptableController(adapter, appId, options) { - _classCallCheck(this, AdaptableController); - - this.options = options; - this.appId = appId; - this.adapter = adapter; - } - - _createClass(AdaptableController, [{ - key: "expectedAdapterType", - value: function expectedAdapterType() { - throw new Error("Subclasses should implement expectedAdapterType()"); - } - }, { - key: "validateAdapter", - value: function validateAdapter(adapter) { - AdaptableController.validateAdapter(adapter, this); - } - }, { - key: "adapter", - set: function set(adapter) { - this.validateAdapter(adapter); - this[_adapter] = adapter; - }, - get: function get() { - return this[_adapter]; - } - }, { - key: "config", - get: function get() { - return new _Config2.default(this.appId); - } - }], [{ - key: "validateAdapter", - value: function validateAdapter(adapter, self, ExpectedType) { - if (!adapter) { - throw new Error(this.constructor.name + " requires an adapter"); - } - - var Type = ExpectedType || self.expectedAdapterType(); - // Allow skipping for testing - if (!Type) { - return; - } - - // Makes sure the prototype matches - var mismatches = Object.getOwnPropertyNames(Type.prototype).reduce(function (obj, key) { - var adapterType = _typeof(adapter[key]); - var expectedType = _typeof(Type.prototype[key]); - if (adapterType !== expectedType) { - obj[key] = { - expected: expectedType, - actual: adapterType - }; - } - return obj; - }, {}); - - if (Object.keys(mismatches).length > 0) { - throw new Error("Adapter prototype don't match expected prototype", adapter, mismatches); - } - } - }]); - - return AdaptableController; -}(); - -exports.default = AdaptableController; \ No newline at end of file diff --git a/lib/Controllers/AnalyticsController.js b/lib/Controllers/AnalyticsController.js deleted file mode 100644 index 100fa67632..0000000000 --- a/lib/Controllers/AnalyticsController.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.AnalyticsController = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _AdaptableController2 = require('./AdaptableController'); - -var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); - -var _AnalyticsAdapter = require('../Adapters/Analytics/AnalyticsAdapter'); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var AnalyticsController = exports.AnalyticsController = function (_AdaptableController) { - _inherits(AnalyticsController, _AdaptableController); - - function AnalyticsController() { - _classCallCheck(this, AnalyticsController); - - return _possibleConstructorReturn(this, (AnalyticsController.__proto__ || Object.getPrototypeOf(AnalyticsController)).apply(this, arguments)); - } - - _createClass(AnalyticsController, [{ - key: 'appOpened', - value: function appOpened(req) { - var _this2 = this; - - return Promise.resolve().then(function () { - return _this2.adapter.appOpened(req.body, req); - }).then(function (response) { - return { response: response || {} }; - }).catch(function () { - return { response: {} }; - }); - } - }, { - key: 'trackEvent', - value: function trackEvent(req) { - var _this3 = this; - - return Promise.resolve().then(function () { - return _this3.adapter.trackEvent(req.params.eventName, req.body, req); - }).then(function (response) { - return { response: response || {} }; - }).catch(function () { - return { response: {} }; - }); - } - }, { - key: 'expectedAdapterType', - value: function expectedAdapterType() { - return _AnalyticsAdapter.AnalyticsAdapter; - } - }]); - - return AnalyticsController; -}(_AdaptableController3.default); - -exports.default = AnalyticsController; \ No newline at end of file diff --git a/lib/Controllers/CacheController.js b/lib/Controllers/CacheController.js deleted file mode 100644 index b2ebcb465b..0000000000 --- a/lib/Controllers/CacheController.js +++ /dev/null @@ -1,129 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.CacheController = exports.SubCache = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _AdaptableController2 = require('./AdaptableController'); - -var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); - -var _CacheAdapter = require('../Adapters/Cache/CacheAdapter'); - -var _CacheAdapter2 = _interopRequireDefault(_CacheAdapter); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var KEY_SEPARATOR_CHAR = ':'; - -function joinKeys() { - for (var _len = arguments.length, keys = Array(_len), _key = 0; _key < _len; _key++) { - keys[_key] = arguments[_key]; - } - - return keys.join(KEY_SEPARATOR_CHAR); -} - -/** - * Prefix all calls to the cache via a prefix string, useful when grouping Cache by object type. - * - * eg "Role" or "Session" - */ - -var SubCache = exports.SubCache = function () { - function SubCache(prefix, cacheController, ttl) { - _classCallCheck(this, SubCache); - - this.prefix = prefix; - this.cache = cacheController; - this.ttl = ttl; - } - - _createClass(SubCache, [{ - key: 'get', - value: function get(key) { - var cacheKey = joinKeys(this.prefix, key); - return this.cache.get(cacheKey); - } - }, { - key: 'put', - value: function put(key, value, ttl) { - var cacheKey = joinKeys(this.prefix, key); - return this.cache.put(cacheKey, value, ttl); - } - }, { - key: 'del', - value: function del(key) { - var cacheKey = joinKeys(this.prefix, key); - return this.cache.del(cacheKey); - } - }, { - key: 'clear', - value: function clear() { - return this.cache.clear(); - } - }]); - - return SubCache; -}(); - -var CacheController = exports.CacheController = function (_AdaptableController) { - _inherits(CacheController, _AdaptableController); - - function CacheController(adapter, appId) { - var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - - _classCallCheck(this, CacheController); - - var _this = _possibleConstructorReturn(this, (CacheController.__proto__ || Object.getPrototypeOf(CacheController)).call(this, adapter, appId, options)); - - _this.role = new SubCache('role', _this); - _this.user = new SubCache('user', _this); - return _this; - } - - _createClass(CacheController, [{ - key: 'get', - value: function get(key) { - var cacheKey = joinKeys(this.appId, key); - return this.adapter.get(cacheKey).then(null, function () { - return Promise.resolve(null); - }); - } - }, { - key: 'put', - value: function put(key, value, ttl) { - var cacheKey = joinKeys(this.appId, key); - return this.adapter.put(cacheKey, value, ttl); - } - }, { - key: 'del', - value: function del(key) { - var cacheKey = joinKeys(this.appId, key); - return this.adapter.del(cacheKey); - } - }, { - key: 'clear', - value: function clear() { - return this.adapter.clear(); - } - }, { - key: 'expectedAdapterType', - value: function expectedAdapterType() { - return _CacheAdapter2.default; - } - }]); - - return CacheController; -}(_AdaptableController3.default); - -exports.default = CacheController; \ No newline at end of file diff --git a/lib/Controllers/DatabaseController.js b/lib/Controllers/DatabaseController.js deleted file mode 100644 index 25bab4ad81..0000000000 --- a/lib/Controllers/DatabaseController.js +++ /dev/null @@ -1,1167 +0,0 @@ -'use strict'; - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _node = require('parse/node'); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _intersect = require('intersect'); - -var _intersect2 = _interopRequireDefault(_intersect); - -var _deepcopy = require('deepcopy'); - -var _deepcopy2 = _interopRequireDefault(_deepcopy); - -var _logger = require('../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -var _SchemaController = require('./SchemaController'); - -var SchemaController = _interopRequireWildcard(_SchemaController); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } - -function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } - -function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } // A database adapter that works with data exported from the hosted -// Parse database. - -function addWriteACL(query, acl) { - var newQuery = _lodash2.default.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].concat(_toConsumableArray(acl)) }; - return newQuery; -} - -function addReadACL(query, acl) { - var newQuery = _lodash2.default.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, "*"].concat(_toConsumableArray(acl)) }; - return newQuery; -} - -// Transforms a REST API formatted ACL object to our two-field mongo format. -var transformObjectACL = function transformObjectACL(_ref) { - var ACL = _ref.ACL, - result = _objectWithoutProperties(_ref, ['ACL']); - - if (!ACL) { - return result; - } - - result._wperm = []; - result._rperm = []; - - for (var entry in ACL) { - if (ACL[entry].read) { - result._rperm.push(entry); - } - if (ACL[entry].write) { - result._wperm.push(entry); - } - } - return result; -}; - -var specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count']; - -var isSpecialQueryKey = function isSpecialQueryKey(key) { - return specialQuerykeys.indexOf(key) >= 0; -}; - -var validateQuery = function validateQuery(query) { - if (query.ACL) { - throw new _node.Parse.Error(_node.Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); - } - - if (query.$or) { - if (query.$or instanceof Array) { - query.$or.forEach(validateQuery); - - /* In MongoDB, $or queries which are not alone at the top level of the - * query can not make efficient use of indexes due to a long standing - * bug known as SERVER-13732. - * - * This block restructures queries in which $or is not the sole top - * level element by moving all other top-level predicates inside every - * subdocument of the $or predicate, allowing MongoDB's query planner - * to make full use of the most relevant indexes. - * - * EG: {$or: [{a: 1}, {a: 2}], b: 2} - * Becomes: {$or: [{a: 1, b: 2}, {a: 2, b: 2}]} - * - * The only exceptions are $near and $nearSphere operators, which are - * constrained to only 1 operator per query. As a result, these ops - * remain at the top level - * - * https://jira.mongodb.org/browse/SERVER-13732 - * https://github.com/parse-community/parse-server/issues/3767 - */ - Object.keys(query).forEach(function (key) { - var noCollisions = !query.$or.some(function (subq) { - return subq.hasOwnProperty(key); - }); - var hasNears = false; - if (query[key] != null && _typeof(query[key]) == 'object') { - hasNears = '$near' in query[key] || '$nearSphere' in query[key]; - } - if (key != '$or' && noCollisions && !hasNears) { - query.$or.forEach(function (subquery) { - subquery[key] = query[key]; - }); - delete query[key]; - } - }); - query.$or.forEach(validateQuery); - } else { - throw new _node.Parse.Error(_node.Parse.Error.INVALID_QUERY, 'Bad $or format - use an array value.'); - } - } - - if (query.$and) { - if (query.$and instanceof Array) { - query.$and.forEach(validateQuery); - } else { - throw new _node.Parse.Error(_node.Parse.Error.INVALID_QUERY, 'Bad $and format - use an array value.'); - } - } - - Object.keys(query).forEach(function (key) { - if (query && query[key] && query[key].$regex) { - if (typeof query[key].$options === 'string') { - if (!query[key].$options.match(/^[imxs]+$/)) { - throw new _node.Parse.Error(_node.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 _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Invalid key name: ' + key); - } - }); -}; - -function DatabaseController(adapter, schemaCache) { - this.adapter = adapter; - this.schemaCache = schemaCache; - // We don't want a mutable this.schema, because then you could have - // one request that uses different schemas for different parts of - // it. Instead, use loadSchema to get a schema. - this.schemaPromise = null; -} - -DatabaseController.prototype.collectionExists = function (className) { - return this.adapter.classExists(className); -}; - -DatabaseController.prototype.purgeCollection = function (className) { - var _this = this; - - return this.loadSchema().then(function (schemaController) { - return schemaController.getOneSchema(className); - }).then(function (schema) { - return _this.adapter.deleteObjectsByQuery(className, schema, {}); - }); -}; - -DatabaseController.prototype.validateClassName = function (className) { - if (!SchemaController.classNameIsValid(className)) { - return Promise.reject(new _node.Parse.Error(_node.Parse.Error.INVALID_CLASS_NAME, 'invalid className: ' + className)); - } - return Promise.resolve(); -}; - -// Returns a promise for a schemaController. -DatabaseController.prototype.loadSchema = function () { - var _this2 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { clearCache: false }; - - if (!this.schemaPromise) { - this.schemaPromise = SchemaController.load(this.adapter, this.schemaCache, options); - this.schemaPromise.then(function () { - return delete _this2.schemaPromise; - }, function () { - return delete _this2.schemaPromise; - }); - } - return this.schemaPromise; -}; - -// Returns a promise for the classname that is related to the given -// classname through the key. -// TODO: make this not in the DatabaseController interface -DatabaseController.prototype.redirectClassNameForKey = function (className, key) { - return this.loadSchema().then(function (schema) { - var t = schema.getExpectedType(className, key); - if (t && t.type == 'Relation') { - return t.targetClass; - } else { - return className; - } - }); -}; - -// Uses the schema to validate the object (REST API format). -// 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. -DatabaseController.prototype.validateObject = function (className, object, query, _ref2) { - var _this3 = this; - - var acl = _ref2.acl; - - var schema = void 0; - var isMaster = acl === undefined; - var aclGroup = acl || []; - return this.loadSchema().then(function (s) { - schema = s; - if (isMaster) { - return Promise.resolve(); - } - return _this3.canAddField(schema, className, object, aclGroup); - }).then(function () { - return schema.validateObject(className, object, query); - }); -}; - -// Filters out any data that shouldn't be on this REST-formatted object. -var filterSensitiveData = function filterSensitiveData(isMaster, aclGroup, className, object) { - if (className !== '_User') { - return object; - } - - object.password = object._hashed_password; - delete object._hashed_password; - - delete object.sessionToken; - - if (isMaster) { - return object; - } - delete object._email_verify_token; - delete object._perishable_token; - delete object._perishable_token_expires_at; - delete object._tombstone; - delete object._email_verify_token_expires_at; - delete object._failed_login_count; - delete object._account_lockout_expires_at; - delete object._password_changed_at; - - if (aclGroup.indexOf(object.objectId) > -1) { - return object; - } - delete object.authData; - return object; -}; - -// Runs an update on the database. -// Returns a promise for an object with the new values for field -// modifications that don't know their results ahead of time, like -// 'increment'. -// Options: -// 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. -var 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']; - -var isSpecialUpdateKey = function isSpecialUpdateKey(key) { - return specialKeysForUpdate.indexOf(key) >= 0; -}; - -DatabaseController.prototype.update = function (className, query, update) { - var _this4 = this; - - var _ref3 = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}, - acl = _ref3.acl, - many = _ref3.many, - upsert = _ref3.upsert; - - var skipSanitization = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false; - - var originalQuery = query; - var originalUpdate = update; - // Make a copy of the object, so we don't mutate the incoming data. - update = (0, _deepcopy2.default)(update); - var relationUpdates = []; - var isMaster = acl === undefined; - var aclGroup = acl || []; - return this.loadSchema().then(function (schemaController) { - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'update')).then(function () { - relationUpdates = _this4.collectRelationUpdates(className, originalQuery.objectId, update); - if (!isMaster) { - query = _this4.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(function (error) { - // If the schema doesn't exist, pretend it exists with no fields. This behaviour - // will likely need revisiting. - if (error === undefined) { - return { fields: {} }; - } - throw error; - }).then(function (schema) { - Object.keys(update).forEach(function (fieldName) { - if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { - throw new _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Invalid field name for update: ' + fieldName); - } - fieldName = fieldName.split('.')[0]; - if (!SchemaController.fieldNameIsValid(fieldName) && !isSpecialUpdateKey(fieldName)) { - throw new _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Invalid field name for update: ' + fieldName); - } - }); - for (var updateOperation in update) { - if (Object.keys(updateOperation).some(function (innerKey) { - return innerKey.includes('$') || innerKey.includes('.'); - })) { - throw new _node.Parse.Error(_node.Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters"); - } - } - update = transformObjectACL(update); - transformAuthData(className, update, schema); - if (many) { - return _this4.adapter.updateObjectsByQuery(className, schema, query, update); - } else if (upsert) { - return _this4.adapter.upsertOneObject(className, schema, query, update); - } else { - return _this4.adapter.findOneAndUpdate(className, schema, query, update); - } - }); - }).then(function (result) { - if (!result) { - return Promise.reject(new _node.Parse.Error(_node.Parse.Error.OBJECT_NOT_FOUND, 'Object not found.')); - } - return _this4.handleRelationUpdates(className, originalQuery.objectId, update, relationUpdates).then(function () { - return result; - }); - }).then(function (result) { - if (skipSanitization) { - return Promise.resolve(result); - } - return sanitizeDatabaseResult(originalUpdate, result); - }); - }); -}; - -function sanitizeDatabaseResult(originalObject, result) { - var response = {}; - if (!result) { - return Promise.resolve(response); - } - Object.keys(originalObject).forEach(function (key) { - var keyUpdate = originalObject[key]; - // determine if that was an op - if (keyUpdate && (typeof keyUpdate === 'undefined' ? 'undefined' : _typeof(keyUpdate)) === 'object' && keyUpdate.__op && ['Add', 'AddUnique', 'Remove', 'Increment'].indexOf(keyUpdate.__op) > -1) { - // only valid ops that produce an actionable result - response[key] = result[key]; - } - }); - return Promise.resolve(response); -} - -// Collect all relation-updating operations from a REST-format update. -// Returns a list of all relation updates to perform -// This mutates update. -DatabaseController.prototype.collectRelationUpdates = function (className, objectId, update) { - var ops = []; - var deleteMe = []; - objectId = update.objectId || objectId; - - var process = function process(op, key) { - if (!op) { - return; - } - if (op.__op == 'AddRelation') { - ops.push({ key: key, op: op }); - deleteMe.push(key); - } - - if (op.__op == 'RemoveRelation') { - ops.push({ key: key, op: op }); - deleteMe.push(key); - } - - if (op.__op == 'Batch') { - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = op.ops[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var x = _step.value; - - process(x, key); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - } - }; - - for (var key in update) { - process(update[key], key); - } - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = deleteMe[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var _key = _step2.value; - - delete update[_key]; - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - - return ops; -}; - -// Processes relation-updating operations from a REST-format update. -// Returns a promise that resolves when all updates have been performed -DatabaseController.prototype.handleRelationUpdates = function (className, objectId, update, ops) { - var _this5 = this; - - var pending = []; - objectId = update.objectId || objectId; - ops.forEach(function (_ref4) { - var key = _ref4.key, - op = _ref4.op; - - if (!op) { - return; - } - if (op.__op == 'AddRelation') { - var _iteratorNormalCompletion3 = true; - var _didIteratorError3 = false; - var _iteratorError3 = undefined; - - try { - for (var _iterator3 = op.objects[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { - var object = _step3.value; - - pending.push(_this5.addRelation(key, className, objectId, object.objectId)); - } - } catch (err) { - _didIteratorError3 = true; - _iteratorError3 = err; - } finally { - try { - if (!_iteratorNormalCompletion3 && _iterator3.return) { - _iterator3.return(); - } - } finally { - if (_didIteratorError3) { - throw _iteratorError3; - } - } - } - } - - if (op.__op == 'RemoveRelation') { - var _iteratorNormalCompletion4 = true; - var _didIteratorError4 = false; - var _iteratorError4 = undefined; - - try { - for (var _iterator4 = op.objects[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { - var _object = _step4.value; - - pending.push(_this5.removeRelation(key, className, objectId, _object.objectId)); - } - } catch (err) { - _didIteratorError4 = true; - _iteratorError4 = err; - } finally { - try { - if (!_iteratorNormalCompletion4 && _iterator4.return) { - _iterator4.return(); - } - } finally { - if (_didIteratorError4) { - throw _iteratorError4; - } - } - } - } - }); - - return Promise.all(pending); -}; - -// Adds a relation. -// Returns a promise that resolves successfully iff the add was successful. -var relationSchema = { fields: { relatedId: { type: 'String' }, owningId: { type: 'String' } } }; -DatabaseController.prototype.addRelation = function (key, fromClassName, fromId, toId) { - var doc = { - relatedId: toId, - owningId: fromId - }; - return this.adapter.upsertOneObject('_Join:' + key + ':' + fromClassName, relationSchema, doc, doc); -}; - -// Removes a relation. -// Returns a promise that resolves successfully iff the remove was -// successful. -DatabaseController.prototype.removeRelation = function (key, fromClassName, fromId, toId) { - var doc = { - relatedId: toId, - owningId: fromId - }; - return this.adapter.deleteObjectsByQuery('_Join:' + key + ':' + fromClassName, relationSchema, doc).catch(function (error) { - // We don't care if they try to delete a non-existent relation. - if (error.code == _node.Parse.Error.OBJECT_NOT_FOUND) { - return; - } - throw error; - }); -}; - -// Removes objects matches this query from the database. -// Returns a promise that resolves successfully iff the object was -// deleted. -// Options: -// 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. -DatabaseController.prototype.destroy = function (className, query) { - var _this6 = this; - - var _ref5 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, - acl = _ref5.acl; - - var isMaster = acl === undefined; - var aclGroup = acl || []; - - return this.loadSchema().then(function (schemaController) { - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'delete')).then(function () { - if (!isMaster) { - query = _this6.addPointerPermissions(schemaController, className, 'delete', query, aclGroup); - if (!query) { - throw new _node.Parse.Error(_node.Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); - } - } - // delete by query - if (acl) { - query = addWriteACL(query, acl); - } - validateQuery(query); - return schemaController.getOneSchema(className).catch(function (error) { - // If the schema doesn't exist, pretend it exists with no fields. This behaviour - // will likely need revisiting. - if (error === undefined) { - return { fields: {} }; - } - throw error; - }).then(function (parseFormatSchema) { - return _this6.adapter.deleteObjectsByQuery(className, parseFormatSchema, query); - }).catch(function (error) { - // When deleting sessions while changing passwords, don't throw an error if they don't have any sessions. - if (className === "_Session" && error.code === _node.Parse.Error.OBJECT_NOT_FOUND) { - return Promise.resolve({}); - } - throw error; - }); - }); - }); -}; - -var flattenUpdateOperatorsForCreate = function flattenUpdateOperatorsForCreate(object) { - for (var key in object) { - if (object[key] && object[key].__op) { - switch (object[key].__op) { - case 'Increment': - if (typeof object[key].amount !== 'number') { - throw new _node.Parse.Error(_node.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 _node.Parse.Error(_node.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 _node.Parse.Error(_node.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 _node.Parse.Error(_node.Parse.Error.INVALID_JSON, 'objects to add must be an array'); - } - object[key] = []; - break; - case 'Delete': - delete object[key]; - break; - default: - throw new _node.Parse.Error(_node.Parse.Error.COMMAND_UNAVAILABLE, 'The ' + object[key].__op + ' operator is not supported yet.'); - } - } - } -}; - -var transformAuthData = function transformAuthData(className, object, schema) { - if (object.authData && className === '_User') { - Object.keys(object.authData).forEach(function (provider) { - var providerData = object.authData[provider]; - var fieldName = '_auth_data_' + provider; - if (providerData == null) { - object[fieldName] = { - __op: 'Delete' - }; - } else { - object[fieldName] = providerData; - schema.fields[fieldName] = { type: 'Object' }; - } - }); - delete object.authData; - } -}; - -// Inserts an object into the database. -// Returns a promise that resolves successfully iff the object saved. -DatabaseController.prototype.create = function (className, object) { - var _this7 = this; - - var _ref6 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, - acl = _ref6.acl; - - // Make a copy of the object, so we don't mutate the incoming data. - var originalObject = object; - object = transformObjectACL(object); - - object.createdAt = { iso: object.createdAt, __type: 'Date' }; - object.updatedAt = { iso: object.updatedAt, __type: 'Date' }; - - var isMaster = acl === undefined; - var aclGroup = acl || []; - var relationUpdates = this.collectRelationUpdates(className, null, object); - return this.validateClassName(className).then(function () { - return _this7.loadSchema(); - }).then(function (schemaController) { - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'create')).then(function () { - return schemaController.enforceClassExists(className); - }).then(function () { - return schemaController.reloadData(); - }).then(function () { - return schemaController.getOneSchema(className, true); - }).then(function (schema) { - transformAuthData(className, object, schema); - flattenUpdateOperatorsForCreate(object); - return _this7.adapter.createObject(className, SchemaController.convertSchemaToAdapterSchema(schema), object); - }).then(function (result) { - return _this7.handleRelationUpdates(className, null, object, relationUpdates).then(function () { - return sanitizeDatabaseResult(originalObject, result.ops[0]); - }); - }); - }); -}; - -DatabaseController.prototype.canAddField = function (schema, className, object, aclGroup) { - var classSchema = schema.data[className]; - if (!classSchema) { - return Promise.resolve(); - } - var fields = Object.keys(object); - var schemaFields = Object.keys(classSchema); - var newKeys = fields.filter(function (field) { - return schemaFields.indexOf(field) < 0; - }); - if (newKeys.length > 0) { - return schema.validatePermission(className, aclGroup, 'addField'); - } - return Promise.resolve(); -}; - -// Won't delete collections in the system namespace -// Returns a promise. -DatabaseController.prototype.deleteEverything = function () { - this.schemaPromise = null; - return Promise.all([this.adapter.deleteAllClasses(), this.schemaCache.clear()]); -}; - -// Returns a promise for a list of related ids given an owning id. -// className here is the owning className. -DatabaseController.prototype.relatedIds = function (className, key, owningId) { - return this.adapter.find(joinTableName(className, key), relationSchema, { owningId: owningId }, {}).then(function (results) { - return results.map(function (result) { - return result.relatedId; - }); - }); -}; - -// Returns a promise for a list of owning ids given some related ids. -// className here is the owning className. -DatabaseController.prototype.owningIds = function (className, key, relatedIds) { - return this.adapter.find(joinTableName(className, key), relationSchema, { relatedId: { '$in': relatedIds } }, {}).then(function (results) { - return results.map(function (result) { - return result.owningId; - }); - }); -}; - -// Modifies query so that it no longer has $in on relation fields, or -// equal-to-pointer constraints on relation fields. -// Returns a promise that resolves when query is mutated -DatabaseController.prototype.reduceInRelation = function (className, query, schema) { - var _this8 = this; - - // Search for an in-relation or equal-to-relation - // Make it sequential for now, not sure of paralleization side effects - if (query['$or']) { - var ors = query['$or']; - return Promise.all(ors.map(function (aQuery, index) { - return _this8.reduceInRelation(className, aQuery, schema).then(function (aQuery) { - query['$or'][index] = aQuery; - }); - })).then(function () { - return Promise.resolve(query); - }); - } - - var promises = Object.keys(query).map(function (key) { - if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) { - var t = schema.getExpectedType(className, key); - if (!t || t.type !== 'Relation') { - return Promise.resolve(query); - } - // Build the list of queries - var queries = Object.keys(query[key]).map(function (constraintKey) { - var relatedIds = void 0; - var isNegation = false; - if (constraintKey === 'objectId') { - relatedIds = [query[key].objectId]; - } else if (constraintKey == '$in') { - relatedIds = query[key]['$in'].map(function (r) { - return r.objectId; - }); - } else if (constraintKey == '$nin') { - isNegation = true; - relatedIds = query[key]['$nin'].map(function (r) { - return r.objectId; - }); - } else if (constraintKey == '$ne') { - isNegation = true; - relatedIds = [query[key]['$ne'].objectId]; - } else { - return; - } - return { - isNegation: isNegation, - relatedIds: relatedIds - }; - }); - - // remove the current queryKey as we don,t need it anymore - delete query[key]; - // execute each query independnently to build the list of - // $in / $nin - var _promises = queries.map(function (q) { - if (!q) { - return Promise.resolve(); - } - return _this8.owningIds(className, key, q.relatedIds).then(function (ids) { - if (q.isNegation) { - _this8.addNotInObjectIdsIds(ids, query); - } else { - _this8.addInObjectIdsIds(ids, query); - } - return Promise.resolve(); - }); - }); - - return Promise.all(_promises).then(function () { - return Promise.resolve(); - }); - } - return Promise.resolve(); - }); - - return Promise.all(promises).then(function () { - return Promise.resolve(query); - }); -}; - -// Modifies query so that it no longer has $relatedTo -// Returns a promise that resolves when query is mutated -DatabaseController.prototype.reduceRelationKeys = function (className, query) { - var _this9 = this; - - if (query['$or']) { - return Promise.all(query['$or'].map(function (aQuery) { - return _this9.reduceRelationKeys(className, aQuery); - })); - } - - var relatedTo = query['$relatedTo']; - if (relatedTo) { - return this.relatedIds(relatedTo.object.className, relatedTo.key, relatedTo.object.objectId).then(function (ids) { - delete query['$relatedTo']; - _this9.addInObjectIdsIds(ids, query); - return _this9.reduceRelationKeys(className, query); - }); - } -}; - -DatabaseController.prototype.addInObjectIdsIds = function () { - var ids = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; - var query = arguments[1]; - - var idsFromString = typeof query.objectId === 'string' ? [query.objectId] : null; - var idsFromEq = query.objectId && query.objectId['$eq'] ? [query.objectId['$eq']] : null; - var idsFromIn = query.objectId && query.objectId['$in'] ? query.objectId['$in'] : null; - - var allIds = [idsFromString, idsFromEq, idsFromIn, ids].filter(function (list) { - return list !== null; - }); - var totalLength = allIds.reduce(function (memo, list) { - return memo + list.length; - }, 0); - - var idsIntersection = []; - if (totalLength > 125) { - idsIntersection = _intersect2.default.big(allIds); - } else { - idsIntersection = (0, _intersect2.default)(allIds); - } - - // Need to make sure we don't clobber existing shorthand $eq constraints on objectId. - if (!('objectId' in query)) { - query.objectId = {}; - } else if (typeof query.objectId === 'string') { - query.objectId = { - $eq: query.objectId - }; - } - query.objectId['$in'] = idsIntersection; - - return query; -}; - -DatabaseController.prototype.addNotInObjectIdsIds = function () { - var ids = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - var query = arguments[1]; - - var idsFromNin = query.objectId && query.objectId['$nin'] ? query.objectId['$nin'] : []; - var allIds = [].concat(_toConsumableArray(idsFromNin), _toConsumableArray(ids)).filter(function (list) { - return list !== null; - }); - - // make a set and spread to remove duplicates - allIds = [].concat(_toConsumableArray(new Set(allIds))); - - // Need to make sure we don't clobber existing shorthand $eq constraints on objectId. - if (!('objectId' in query)) { - query.objectId = {}; - } else if (typeof query.objectId === 'string') { - query.objectId = { - $eq: query.objectId - }; - } - - query.objectId['$nin'] = allIds; - return query; -}; - -// Runs a query on the database. -// Returns a promise that resolves to a list of items. -// Options: -// skip number of results to skip. -// limit limit to this number of results. -// sort an object where keys are the fields to sort by. -// the value is +1 for ascending, -1 for descending. -// count run a count instead of returning results. -// acl restrict this operation with an ACL for the provided array -// of user objectIds and roles. acl: null means no user. -// when this field is not present, don't do anything regarding ACLs. -// 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. -DatabaseController.prototype.find = function (className, query) { - var _this10 = this; - - var _ref7 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}, - skip = _ref7.skip, - limit = _ref7.limit, - acl = _ref7.acl, - _ref7$sort = _ref7.sort, - sort = _ref7$sort === undefined ? {} : _ref7$sort, - count = _ref7.count, - keys = _ref7.keys, - op = _ref7.op; - - var isMaster = acl === undefined; - var aclGroup = acl || []; - op = op || (typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find'); - // Count operation if counting - op = count === true ? 'count' : op; - - var classExists = true; - return this.loadSchema().then(function (schemaController) { - //Allow volatile classes if querying with Master (for _PushStatus) - //TODO: Move volatile classes concept into mongo adatper, postgres adapter shouldn't care - //that api.parse.com breaks when _PushStatus exists in mongo. - return schemaController.getOneSchema(className, isMaster).catch(function (error) { - // Behaviour 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(function (schema) { - // Parse.com treats queries on _created_at and _updated_at as if they were queries on createdAt and updatedAt, - // so duplicate that behaviour here. If both are specified, the corrent behaviour 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._updated_at) { - sort.updatedAt = sort._updated_at; - delete sort._updated_at; - } - Object.keys(sort).forEach(function (fieldName) { - if (fieldName.match(/^authData\.([a-zA-Z0-9_]+)\.id$/)) { - throw new _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Cannot sort by ' + fieldName); - } - if (!SchemaController.fieldNameIsValid(fieldName)) { - throw new _node.Parse.Error(_node.Parse.Error.INVALID_KEY_NAME, 'Invalid field name: ' + fieldName + '.'); - } - }); - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, op)).then(function () { - return _this10.reduceRelationKeys(className, query); - }).then(function () { - return _this10.reduceInRelation(className, query, schemaController); - }).then(function () { - if (!isMaster) { - query = _this10.addPointerPermissions(schemaController, className, op, query, aclGroup); - } - if (!query) { - if (op == 'get') { - throw new _node.Parse.Error(_node.Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); - } else { - return []; - } - } - if (!isMaster) { - query = addReadACL(query, aclGroup); - } - validateQuery(query); - if (count) { - if (!classExists) { - return 0; - } else { - return _this10.adapter.count(className, schema, query); - } - } else { - if (!classExists) { - return []; - } else { - return _this10.adapter.find(className, schema, query, { skip: skip, limit: limit, sort: sort, keys: keys }).then(function (objects) { - return objects.map(function (object) { - object = untransformObjectACL(object); - return filterSensitiveData(isMaster, aclGroup, className, object); - }); - }); - } - } - }); - }); - }); -}; - -// Transforms a Database format ACL to a REST API format ACL -var untransformObjectACL = function untransformObjectACL(_ref8) { - var _rperm = _ref8._rperm, - _wperm = _ref8._wperm, - output = _objectWithoutProperties(_ref8, ['_rperm', '_wperm']); - - if (_rperm || _wperm) { - output.ACL = {}; - - (_rperm || []).forEach(function (entry) { - if (!output.ACL[entry]) { - output.ACL[entry] = { read: true }; - } else { - output.ACL[entry]['read'] = true; - } - }); - - (_wperm || []).forEach(function (entry) { - if (!output.ACL[entry]) { - output.ACL[entry] = { write: true }; - } else { - output.ACL[entry]['write'] = true; - } - }); - } - return output; -}; - -DatabaseController.prototype.deleteSchema = function (className) { - var _this11 = this; - - return this.loadSchema(true).then(function (schemaController) { - return schemaController.getOneSchema(className, true); - }).catch(function (error) { - if (error === undefined) { - return { fields: {} }; - } else { - throw error; - } - }).then(function (schema) { - return _this11.collectionExists(className).then(function () { - return _this11.adapter.count(className, { fields: {} }); - }).then(function (count) { - if (count > 0) { - throw new _node.Parse.Error(255, 'Class ' + className + ' is not empty, contains ' + count + ' objects, cannot drop schema.'); - } - return _this11.adapter.deleteClass(className); - }).then(function (wasParseCollection) { - if (wasParseCollection) { - var relationFieldNames = Object.keys(schema.fields).filter(function (fieldName) { - return schema.fields[fieldName].type === 'Relation'; - }); - return Promise.all(relationFieldNames.map(function (name) { - return _this11.adapter.deleteClass(joinTableName(className, name)); - })); - } else { - return Promise.resolve(); - } - }); - }); -}; - -DatabaseController.prototype.addPointerPermissions = function (schema, className, operation, query) { - var aclGroup = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : []; - - // Check if class has public permission for operation - // If the BaseCLP pass, let go through - if (schema.testBaseCLP(className, aclGroup, operation)) { - return query; - } - var perms = schema.perms[className]; - var field = ['get', 'find'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields'; - var userACL = aclGroup.filter(function (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 dedup users correctly - if (userACL.length != 1) { - return; - } - var userId = userACL[0]; - var userPointer = { - "__type": "Pointer", - "className": "_User", - "objectId": userId - }; - - var permFields = perms[field]; - var ors = permFields.map(function (key) { - var q = _defineProperty({}, key, userPointer); - return { '$and': [q, query] }; - }); - if (ors.length > 1) { - return { '$or': ors }; - } - return ors[0]; - } else { - return query; - } -}; - -// 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. -DatabaseController.prototype.performInitialization = function () { - var _this12 = this; - - var requiredUserFields = { fields: _extends({}, SchemaController.defaultColumns._Default, SchemaController.defaultColumns._User) }; - var requiredRoleFields = { fields: _extends({}, SchemaController.defaultColumns._Default, SchemaController.defaultColumns._Role) }; - - var userClassPromise = this.loadSchema().then(function (schema) { - return schema.enforceClassExists('_User'); - }); - var roleClassPromise = this.loadSchema().then(function (schema) { - return schema.enforceClassExists('_Role'); - }); - - var usernameUniqueness = userClassPromise.then(function () { - return _this12.adapter.ensureUniqueness('_User', requiredUserFields, ['username']); - }).catch(function (error) { - _logger2.default.warn('Unable to ensure uniqueness for usernames: ', error); - throw error; - }); - - var emailUniqueness = userClassPromise.then(function () { - return _this12.adapter.ensureUniqueness('_User', requiredUserFields, ['email']); - }).catch(function (error) { - _logger2.default.warn('Unable to ensure uniqueness for user email addresses: ', error); - throw error; - }); - - var roleUniqueness = roleClassPromise.then(function () { - return _this12.adapter.ensureUniqueness('_Role', requiredRoleFields, ['name']); - }).catch(function (error) { - _logger2.default.warn('Unable to ensure uniqueness for role name: ', error); - throw error; - }); - - // Create tables for volatile classes - var adapterInit = this.adapter.performInitialization({ VolatileClassesSchemas: SchemaController.VolatileClassesSchemas }); - return Promise.all([usernameUniqueness, emailUniqueness, roleUniqueness, adapterInit]); -}; - -function joinTableName(className, key) { - return '_Join:' + key + ':' + className; -} - -// Expose validateQuery for tests -DatabaseController._validateQuery = validateQuery; -module.exports = DatabaseController; \ No newline at end of file diff --git a/lib/Controllers/FilesController.js b/lib/Controllers/FilesController.js deleted file mode 100644 index 0fe78978d2..0000000000 --- a/lib/Controllers/FilesController.js +++ /dev/null @@ -1,142 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.FilesController = undefined; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _cryptoUtils = require('../cryptoUtils'); - -var _AdaptableController2 = require('./AdaptableController'); - -var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); - -var _FilesAdapter = require('../Adapters/Files/FilesAdapter'); - -var _path = require('path'); - -var _path2 = _interopRequireDefault(_path); - -var _mime = require('mime'); - -var _mime2 = _interopRequireDefault(_mime); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // FilesController.js - - -var 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}-.*"); - -var FilesController = exports.FilesController = function (_AdaptableController) { - _inherits(FilesController, _AdaptableController); - - function FilesController() { - _classCallCheck(this, FilesController); - - return _possibleConstructorReturn(this, (FilesController.__proto__ || Object.getPrototypeOf(FilesController)).apply(this, arguments)); - } - - _createClass(FilesController, [{ - key: 'getFileData', - value: function getFileData(config, filename) { - return this.adapter.getFileData(filename); - } - }, { - key: 'createFile', - value: function createFile(config, filename, data, contentType) { - - var extname = _path2.default.extname(filename); - - var hasExtension = extname.length > 0; - - if (!hasExtension && contentType && _mime2.default.extension(contentType)) { - filename = filename + '.' + _mime2.default.extension(contentType); - } else if (hasExtension && !contentType) { - contentType = _mime2.default.lookup(filename); - } - - filename = (0, _cryptoUtils.randomHexString)(32) + '_' + filename; - - var location = this.adapter.getFileLocation(config, filename); - return this.adapter.createFile(filename, data, contentType).then(function () { - return Promise.resolve({ - url: location, - name: filename - }); - }); - } - }, { - key: 'deleteFile', - value: function deleteFile(config, filename) { - return this.adapter.deleteFile(filename); - } - - /** - * Find file references in REST-format object and adds the url key - * with the current mount point and app id. - * Object may be a single object or list of REST-format objects. - */ - - }, { - key: 'expandFilesInObject', - value: function expandFilesInObject(config, object) { - var _this2 = this; - - if (object instanceof Array) { - object.map(function (obj) { - return _this2.expandFilesInObject(config, obj); - }); - return; - } - if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) !== 'object') { - return; - } - for (var key in object) { - var fileObject = object[key]; - if (fileObject && fileObject['__type'] === 'File') { - if (fileObject['url']) { - continue; - } - var filename = fileObject['name']; - // all filenames starting with "tfss-" should be from files.parsetfss.com - // all filenames starting with a "-" seperated UUID should be from files.parse.com - // all other filenames have been migrated or created from Parse Server - if (config.fileKey === undefined) { - fileObject['url'] = this.adapter.getFileLocation(config, filename); - } else { - if (filename.indexOf('tfss-') === 0) { - fileObject['url'] = 'http://files.parsetfss.com/' + config.fileKey + '/' + encodeURIComponent(filename); - } else if (legacyFilesRegex.test(filename)) { - fileObject['url'] = 'http://files.parse.com/' + config.fileKey + '/' + encodeURIComponent(filename); - } else { - fileObject['url'] = this.adapter.getFileLocation(config, filename); - } - } - } - } - } - }, { - key: 'expectedAdapterType', - value: function expectedAdapterType() { - return _FilesAdapter.FilesAdapter; - } - }, { - key: 'getFileStream', - value: function getFileStream(config, filename) { - return this.adapter.getFileStream(filename); - } - }]); - - return FilesController; -}(_AdaptableController3.default); - -exports.default = FilesController; \ No newline at end of file diff --git a/lib/Controllers/HooksController.js b/lib/Controllers/HooksController.js deleted file mode 100644 index 76e32bf147..0000000000 --- a/lib/Controllers/HooksController.js +++ /dev/null @@ -1,268 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.HooksController = undefined; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /** weak */ - -var _triggers = require("../triggers"); - -var triggers = _interopRequireWildcard(_triggers); - -var _node = require("parse/node"); - -var Parse = _interopRequireWildcard(_node); - -var _request = require("request"); - -var request = _interopRequireWildcard(_request); - -var _logger = require("../logger"); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var DefaultHooksCollectionName = "_Hooks"; - -var HooksController = exports.HooksController = function () { - function HooksController(applicationId, databaseController, webhookKey) { - _classCallCheck(this, HooksController); - - this._applicationId = applicationId; - this._webhookKey = webhookKey; - this.database = databaseController; - } - - _createClass(HooksController, [{ - key: "load", - value: function load() { - var _this = this; - - return this._getHooks().then(function (hooks) { - hooks = hooks || []; - hooks.forEach(function (hook) { - _this.addHookToTriggers(hook); - }); - }); - } - }, { - key: "getFunction", - value: function getFunction(functionName) { - return this._getHooks({ functionName: functionName }, 1).then(function (results) { - return results[0]; - }); - } - }, { - key: "getFunctions", - value: function getFunctions() { - return this._getHooks({ functionName: { $exists: true } }); - } - }, { - key: "getTrigger", - value: function getTrigger(className, triggerName) { - return this._getHooks({ className: className, triggerName: triggerName }, 1).then(function (results) { - return results[0]; - }); - } - }, { - key: "getTriggers", - value: function getTriggers() { - return this._getHooks({ className: { $exists: true }, triggerName: { $exists: true } }); - } - }, { - key: "deleteFunction", - value: function deleteFunction(functionName) { - triggers.removeFunction(functionName, this._applicationId); - return this._removeHooks({ functionName: functionName }); - } - }, { - key: "deleteTrigger", - value: function deleteTrigger(className, triggerName) { - triggers.removeTrigger(triggerName, className, this._applicationId); - return this._removeHooks({ className: className, triggerName: triggerName }); - } - }, { - key: "_getHooks", - value: function _getHooks() { - var query = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - return this.database.find(DefaultHooksCollectionName, query).then(function (results) { - return results.map(function (result) { - delete result.objectId; - return result; - }); - }); - } - }, { - key: "_removeHooks", - value: function _removeHooks(query) { - return this.database.destroy(DefaultHooksCollectionName, query).then(function () { - return Promise.resolve({}); - }); - } - }, { - key: "saveHook", - value: function saveHook(hook) { - var query; - if (hook.functionName && hook.url) { - query = { functionName: hook.functionName }; - } else if (hook.triggerName && hook.className && hook.url) { - query = { className: hook.className, triggerName: hook.triggerName }; - } else { - throw new Parse.Error(143, "invalid hook declaration"); - } - return this.database.update(DefaultHooksCollectionName, query, hook, { upsert: true }).then(function () { - return Promise.resolve(hook); - }); - } - }, { - key: "addHookToTriggers", - value: function addHookToTriggers(hook) { - var wrappedFunction = wrapToHTTPRequest(hook, this._webhookKey); - wrappedFunction.url = hook.url; - if (hook.className) { - triggers.addTrigger(hook.triggerName, hook.className, wrappedFunction, this._applicationId); - } else { - triggers.addFunction(hook.functionName, wrappedFunction, null, this._applicationId); - } - } - }, { - key: "addHook", - value: function addHook(hook) { - this.addHookToTriggers(hook); - return this.saveHook(hook); - } - }, { - key: "createOrUpdateHook", - value: function createOrUpdateHook(aHook) { - var hook; - if (aHook && aHook.functionName && aHook.url) { - hook = {}; - hook.functionName = aHook.functionName; - hook.url = aHook.url; - } 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"); - } - - return this.addHook(hook); - } - }, { - key: "createHook", - value: function createHook(aHook) { - var _this2 = this; - - if (aHook.functionName) { - return this.getFunction(aHook.functionName).then(function (result) { - if (result) { - throw new Parse.Error(143, "function name: " + aHook.functionName + " already exits"); - } else { - return _this2.createOrUpdateHook(aHook); - } - }); - } else if (aHook.className && aHook.triggerName) { - return this.getTrigger(aHook.className, aHook.triggerName).then(function (result) { - if (result) { - throw new Parse.Error(143, "class " + aHook.className + " already has trigger " + aHook.triggerName); - } - return _this2.createOrUpdateHook(aHook); - }); - } - - throw new Parse.Error(143, "invalid hook declaration"); - } - }, { - key: "updateHook", - value: function updateHook(aHook) { - var _this3 = this; - - if (aHook.functionName) { - return this.getFunction(aHook.functionName).then(function (result) { - if (result) { - return _this3.createOrUpdateHook(aHook); - } - 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(function (result) { - if (result) { - return _this3.createOrUpdateHook(aHook); - } - throw new Parse.Error(143, "class " + aHook.className + " does not exist"); - }); - } - throw new Parse.Error(143, "invalid hook declaration"); - } - }]); - - return HooksController; -}(); - -function wrapToHTTPRequest(hook, key) { - return function (req, res) { - var jsonBody = {}; - for (var i in req) { - jsonBody[i] = req[i]; - } - if (req.object) { - jsonBody.object = req.object.toJSON(); - jsonBody.object.className = req.object.className; - } - if (req.original) { - jsonBody.original = req.original.toJSON(); - jsonBody.original.className = req.original.className; - } - var jsonRequest = { - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(jsonBody) - }; - - if (key) { - jsonRequest.headers['X-Parse-Webhook-Key'] = key; - } else { - _logger.logger.warn('Making outgoing webhook request without webhookKey being set!'); - } - - request.post(hook.url, jsonRequest, function (err, httpResponse, body) { - var result; - if (body) { - if (typeof body === "string") { - try { - body = JSON.parse(body); - } catch (e) { - err = { error: "Malformed response", code: -1 }; - } - } - if (!err) { - result = body.success; - err = body.error; - } - } - - if (err) { - return res.error(err); - } else if (hook.triggerName === 'beforeSave') { - if ((typeof result === "undefined" ? "undefined" : _typeof(result)) === 'object') { - delete result.createdAt; - delete result.updatedAt; - } - return res.success({ object: result }); - } else { - return res.success(result); - } - }); - }; -} - -exports.default = HooksController; \ No newline at end of file diff --git a/lib/Controllers/LiveQueryController.js b/lib/Controllers/LiveQueryController.js deleted file mode 100644 index 062d443f3c..0000000000 --- a/lib/Controllers/LiveQueryController.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.LiveQueryController = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _ParseCloudCodePublisher = require('../LiveQuery/ParseCloudCodePublisher'); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var LiveQueryController = exports.LiveQueryController = function () { - function LiveQueryController(config) { - _classCallCheck(this, LiveQueryController); - - // If config is empty, we just assume no classs needs to be registered as LiveQuery - if (!config || !config.classNames) { - this.classNames = new Set(); - } else if (config.classNames instanceof Array) { - this.classNames = new Set(config.classNames); - } else { - throw 'liveQuery.classes should be an array of string'; - } - this.liveQueryPublisher = new _ParseCloudCodePublisher.ParseCloudCodePublisher(config); - } - - _createClass(LiveQueryController, [{ - key: 'onAfterSave', - value: function onAfterSave(className, currentObject, originalObject) { - if (!this.hasLiveQuery(className)) { - return; - } - var req = this._makePublisherRequest(currentObject, originalObject); - this.liveQueryPublisher.onCloudCodeAfterSave(req); - } - }, { - key: 'onAfterDelete', - value: function onAfterDelete(className, currentObject, originalObject) { - if (!this.hasLiveQuery(className)) { - return; - } - var req = this._makePublisherRequest(currentObject, originalObject); - this.liveQueryPublisher.onCloudCodeAfterDelete(req); - } - }, { - key: 'hasLiveQuery', - value: function hasLiveQuery(className) { - return this.classNames.has(className); - } - }, { - key: '_makePublisherRequest', - value: function _makePublisherRequest(currentObject, originalObject) { - var req = { - object: currentObject - }; - if (currentObject) { - req.original = originalObject; - } - return req; - } - }]); - - return LiveQueryController; -}(); - -exports.default = LiveQueryController; \ No newline at end of file diff --git a/lib/Controllers/LoggerController.js b/lib/Controllers/LoggerController.js deleted file mode 100644 index 841eb4b597..0000000000 --- a/lib/Controllers/LoggerController.js +++ /dev/null @@ -1,276 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.LoggerController = exports.LogOrder = exports.LogLevel = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _node = require('parse/node'); - -var _AdaptableController2 = require('./AdaptableController'); - -var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); - -var _LoggerAdapter = require('../Adapters/Logger/LoggerAdapter'); - -var _url = require('url'); - -var _url2 = _interopRequireDefault(_url); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000; -var LOG_STRING_TRUNCATE_LENGTH = 1000; -var truncationMarker = '... (truncated)'; - -var LogLevel = exports.LogLevel = { - INFO: 'info', - ERROR: 'error' -}; - -var LogOrder = exports.LogOrder = { - DESCENDING: 'desc', - ASCENDING: 'asc' -}; - -var LoggerController = exports.LoggerController = function (_AdaptableController) { - _inherits(LoggerController, _AdaptableController); - - function LoggerController() { - _classCallCheck(this, LoggerController); - - return _possibleConstructorReturn(this, (LoggerController.__proto__ || Object.getPrototypeOf(LoggerController)).apply(this, arguments)); - } - - _createClass(LoggerController, [{ - key: 'maskSensitiveUrl', - value: function maskSensitiveUrl(urlString) { - var password = _url2.default.parse(urlString, true).query.password; - - if (password) { - urlString = urlString.replace('password=' + password, 'password=********'); - } - return urlString; - } - }, { - key: 'maskSensitive', - value: function maskSensitive(argArray) { - var _this2 = this; - - return argArray.map(function (e) { - if (!e) { - return e; - } - - if (typeof e === 'string') { - return e.replace(/(password".?:.?")[^"]*"/g, '$1********"'); - } - // else it is an object... - - // check the url - if (e.url) { - // for strings - if (typeof e.url === 'string') { - e.url = _this2.maskSensitiveUrl(e.url); - } else if (Array.isArray(e.url)) { - // for strings in array - e.url = e.url.map(function (item) { - if (typeof item === 'string') { - return _this2.maskSensitiveUrl(item); - } - - return item; - }); - } - } - - if (e.body) { - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = Object.keys(e.body)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var key = _step.value; - - if (key === 'password') { - e.body[key] = '********'; - break; - } - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - } - - if (e.params) { - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = Object.keys(e.params)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var _key = _step2.value; - - if (_key === 'password') { - e.params[_key] = '********'; - break; - } - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - } - - return e; - }); - } - }, { - key: 'log', - value: function log(level, args) { - // make the passed in arguments object an array with the spread operator - args = this.maskSensitive([].concat(_toConsumableArray(args))); - args = [].concat(level, args); - this.adapter.log.apply(this.adapter, args); - } - }, { - key: 'info', - value: function info() { - return this.log('info', arguments); - } - }, { - key: 'error', - value: function error() { - return this.log('error', arguments); - } - }, { - key: 'warn', - value: function warn() { - return this.log('warn', arguments); - } - }, { - key: 'verbose', - value: function verbose() { - return this.log('verbose', arguments); - } - }, { - key: 'debug', - value: function debug() { - return this.log('debug', arguments); - } - }, { - key: 'silly', - value: function silly() { - return this.log('silly', arguments); - } - // check that date input is valid - - }, { - key: 'truncateLogMessage', - value: function truncateLogMessage(string) { - if (string && string.length > LOG_STRING_TRUNCATE_LENGTH) { - var truncated = string.substring(0, LOG_STRING_TRUNCATE_LENGTH) + truncationMarker; - return truncated; - } - - return string; - } - }, { - key: 'getLogs', - - - // Returns a promise for a {response} object. - // query params: - // level (optional) Level of logging you want to query for (info || error) - // from (optional) Start time for the search. Defaults to 1 week ago. - // until (optional) End time for the search. Defaults to current time. - // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”. - // size (optional) Number of rows returned by search. Defaults to 10 - value: function getLogs() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - if (!this.adapter) { - throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Logger adapter is not available'); - } - if (typeof this.adapter.query !== 'function') { - throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Querying logs is not supported with this adapter'); - } - options = LoggerController.parseOptions(options); - return this.adapter.query(options); - } - }, { - key: 'expectedAdapterType', - value: function expectedAdapterType() { - return _LoggerAdapter.LoggerAdapter; - } - }], [{ - key: 'validDateTime', - value: function validDateTime(date) { - if (!date) { - return null; - } - date = new Date(date); - - if (!isNaN(date.getTime())) { - return date; - } - - return null; - } - }, { - key: 'parseOptions', - value: function parseOptions() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var from = LoggerController.validDateTime(options.from) || new Date(Date.now() - 7 * MILLISECONDS_IN_A_DAY); - var until = LoggerController.validDateTime(options.until) || new Date(); - var size = Number(options.size) || 10; - var order = options.order || LogOrder.DESCENDING; - var level = options.level || LogLevel.INFO; - - return { - from: from, - until: until, - size: size, - order: order, - level: level - }; - } - }]); - - return LoggerController; -}(_AdaptableController3.default); - -exports.default = LoggerController; \ No newline at end of file diff --git a/lib/Controllers/PushController.js b/lib/Controllers/PushController.js deleted file mode 100644 index b1f27a77a8..0000000000 --- a/lib/Controllers/PushController.js +++ /dev/null @@ -1,167 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.PushController = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _node = require('parse/node'); - -var _deepcopy = require('deepcopy'); - -var _deepcopy2 = _interopRequireDefault(_deepcopy); - -var _RestQuery = require('../RestQuery'); - -var _RestQuery2 = _interopRequireDefault(_RestQuery); - -var _RestWrite = require('../RestWrite'); - -var _RestWrite2 = _interopRequireDefault(_RestWrite); - -var _Auth = require('../Auth'); - -var _StatusHandler = require('../StatusHandler'); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var PushController = exports.PushController = function () { - function PushController() { - _classCallCheck(this, PushController); - } - - _createClass(PushController, [{ - key: 'sendPush', - value: function sendPush() { - var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - var where = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var config = arguments[2]; - var auth = arguments[3]; - var onPushStatusSaved = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : function () {}; - - if (!config.hasPushSupport) { - throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Missing push configuration'); - } - // Replace the expiration_time and push_time with a valid Unix epoch milliseconds time - body.expiration_time = PushController.getExpirationTime(body); - var push_time = PushController.getPushTime(body); - if (typeof push_time !== 'undefined') { - body['push_time'] = push_time; - } - // TODO: If the req can pass the checking, we return immediately instead of waiting - // pushes to be sent. We probably change this behaviour in the future. - var badgeUpdate = function badgeUpdate() { - return Promise.resolve(); - }; - if (body.data && body.data.badge) { - var badge = body.data.badge; - var restUpdate = {}; - if (typeof badge == 'string' && badge.toLowerCase() === 'increment') { - restUpdate = { badge: { __op: 'Increment', amount: 1 } }; - } else if (Number(badge)) { - restUpdate = { badge: badge }; - } else { - throw "Invalid value for badge, expected number or 'Increment'"; - } - var updateWhere = (0, _deepcopy2.default)(where); - - badgeUpdate = function badgeUpdate() { - updateWhere.deviceType = 'ios'; - // Build a real RestQuery so we can use it in RestWrite - var restQuery = new _RestQuery2.default(config, (0, _Auth.master)(config), '_Installation', updateWhere); - return restQuery.buildRestWhere().then(function () { - var write = new _RestWrite2.default(config, (0, _Auth.master)(config), '_Installation', restQuery.restWhere, restUpdate); - write.runOptions.many = true; - return write.execute(); - }); - }; - } - var pushStatus = (0, _StatusHandler.pushStatusHandler)(config); - return Promise.resolve().then(function () { - return pushStatus.setInitial(body, where); - }).then(function () { - onPushStatusSaved(pushStatus.objectId); - return badgeUpdate(); - }).then(function () { - if (body.hasOwnProperty('push_time') && config.hasPushScheduledSupport) { - return Promise.resolve(); - } - return config.pushControllerQueue.enqueue(body, where, config, auth, pushStatus); - }).catch(function (err) { - return pushStatus.fail(err).then(function () { - throw err; - }); - }); - } - - /** - * Get expiration time from the request body. - * @param {Object} request A request object - * @returns {Number|undefined} The expiration time if it exists in the request - */ - - }], [{ - key: 'getExpirationTime', - value: function getExpirationTime() { - var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var hasExpirationTime = body.hasOwnProperty('expiration_time'); - if (!hasExpirationTime) { - return; - } - var expirationTimeParam = body['expiration_time']; - var expirationTime; - if (typeof expirationTimeParam === 'number') { - expirationTime = new Date(expirationTimeParam * 1000); - } else if (typeof expirationTimeParam === 'string') { - expirationTime = new Date(expirationTimeParam); - } else { - throw new _node.Parse.Error(_node.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 _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, body['expiration_time'] + ' is not valid time.'); - } - return expirationTime.valueOf(); - } - - /** - * Get push time from the request body. - * @param {Object} request A request object - * @returns {Number|undefined} The push time if it exists in the request - */ - - }, { - key: 'getPushTime', - value: function getPushTime() { - var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var hasPushTime = body.hasOwnProperty('push_time'); - if (!hasPushTime) { - return; - } - var pushTimeParam = body['push_time']; - var pushTime; - if (typeof pushTimeParam === 'number') { - pushTime = new Date(pushTimeParam * 1000); - } else if (typeof pushTimeParam === 'string') { - pushTime = new Date(pushTimeParam); - } else { - throw new _node.Parse.Error(_node.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(pushTime)) { - throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, body['push_time'] + ' is not valid time.'); - } - return pushTime; - } - }]); - - return PushController; -}(); - -exports.default = PushController; \ No newline at end of file diff --git a/lib/Controllers/SchemaCache.js b/lib/Controllers/SchemaCache.js deleted file mode 100644 index c78a058267..0000000000 --- a/lib/Controllers/SchemaCache.js +++ /dev/null @@ -1,121 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _cryptoUtils = require("../cryptoUtils"); - -var _defaults = require("../defaults"); - -var _defaults2 = _interopRequireDefault(_defaults); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var MAIN_SCHEMA = "__MAIN_SCHEMA"; -var SCHEMA_CACHE_PREFIX = "__SCHEMA"; -var ALL_KEYS = "__ALL_KEYS"; - -var SchemaCache = function () { - function SchemaCache(cacheController) { - var ttl = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : _defaults2.default.schemaCacheTTL; - var singleCache = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; - - _classCallCheck(this, SchemaCache); - - this.ttl = ttl; - if (typeof ttl == 'string') { - this.ttl = parseInt(ttl); - } - this.cache = cacheController; - this.prefix = SCHEMA_CACHE_PREFIX; - if (!singleCache) { - this.prefix += (0, _cryptoUtils.randomString)(20); - } - } - - _createClass(SchemaCache, [{ - key: "put", - value: function put(key, value) { - var _this = this; - - return this.cache.get(this.prefix + ALL_KEYS).then(function (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)]); - }); - } - }, { - key: "getAllClasses", - value: function getAllClasses() { - if (!this.ttl) { - return Promise.resolve(null); - } - return this.cache.get(this.prefix + MAIN_SCHEMA); - } - }, { - key: "setAllClasses", - value: function setAllClasses(schema) { - if (!this.ttl) { - return Promise.resolve(null); - } - return this.put(this.prefix + MAIN_SCHEMA, schema); - } - }, { - key: "setOneSchema", - value: function setOneSchema(className, schema) { - if (!this.ttl) { - return Promise.resolve(null); - } - return this.put(this.prefix + className, schema); - } - }, { - key: "getOneSchema", - value: function getOneSchema(className) { - var _this2 = this; - - if (!this.ttl) { - return Promise.resolve(null); - } - return this.cache.get(this.prefix + className).then(function (schema) { - if (schema) { - return Promise.resolve(schema); - } - return _this2.cache.get(_this2.prefix + MAIN_SCHEMA).then(function (cachedSchemas) { - cachedSchemas = cachedSchemas || []; - schema = cachedSchemas.find(function (cachedSchema) { - return cachedSchema.className === className; - }); - if (schema) { - return Promise.resolve(schema); - } - return Promise.resolve(null); - }); - }); - } - }, { - key: "clear", - value: function clear() { - var _this3 = this; - - // That clears all caches... - return this.cache.get(this.prefix + ALL_KEYS).then(function (allKeys) { - if (!allKeys) { - return; - } - var promises = Object.keys(allKeys).map(function (key) { - return _this3.cache.del(key); - }); - return Promise.all(promises); - }); - } - }]); - - return SchemaCache; -}(); - -exports.default = SchemaCache; \ No newline at end of file diff --git a/lib/Controllers/SchemaController.js b/lib/Controllers/SchemaController.js deleted file mode 100644 index d7744cbe26..0000000000 --- a/lib/Controllers/SchemaController.js +++ /dev/null @@ -1,1114 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } - -// This class handles schema validation, persistence, and modification. -// -// Each individual Schema object should be immutable. The helpers to -// do things with the Schema just return a new schema when the schema -// is changed. -// -// The canonical place to store this Schema is in the database itself, -// in a _SCHEMA collection. This is not the right way to do it for an -// open source framework, but it's backward compatible, so we're -// keeping it this way for now. -// -// In API-handling code, you should only use the Schema class via the -// DatabaseController. This will let us replace the schema logic for -// different databases. -// TODO: hide all schema logic inside the database adapter. -var Parse = require('parse/node').Parse; - -var defaultColumns = Object.freeze({ - // Contain the default columns for every parse object type (except _Join collection) - _Default: { - "objectId": { type: 'String' }, - "createdAt": { type: 'Date' }, - "updatedAt": { type: 'Date' }, - "ACL": { type: 'ACL' } - }, - // The additional default columns for the _User collection (in addition to DefaultCols) - _User: { - "username": { type: 'String' }, - "password": { type: 'String' }, - "email": { type: 'String' }, - "emailVerified": { type: 'Boolean' }, - "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' } - }, - // The additional default columns for the _Role collection (in addition to DefaultCols) - _Role: { - "name": { type: 'String' }, - "users": { type: 'Relation', targetClass: '_User' }, - "roles": { type: 'Relation', targetClass: '_Role' } - }, - // The additional default columns for the _Session collection (in addition to DefaultCols) - _Session: { - "restricted": { type: 'Boolean' }, - "user": { type: 'Pointer', targetClass: '_User' }, - "installationId": { type: 'String' }, - "sessionToken": { type: 'String' }, - "expiresAt": { type: 'Date' }, - "createdWith": { type: 'Object' } - }, - _Product: { - "productIdentifier": { type: 'String' }, - "download": { type: 'File' }, - "downloadName": { type: 'String' }, - "icon": { type: 'File' }, - "order": { type: 'Number' }, - "title": { type: 'String' }, - "subtitle": { type: 'String' } - }, - _PushStatus: { - "pushTime": { type: 'String' }, - "source": { type: 'String' }, // rest or webui - "query": { type: 'String' }, // the stringified JSON query - "payload": { type: 'String' }, // the stringified JSON payload, - "title": { type: 'String' }, - "expiry": { type: 'Number' }, - "status": { type: 'String' }, - "numSent": { type: 'Number' }, - "numFailed": { type: 'Number' }, - "pushHash": { type: 'String' }, - "errorMessage": { type: 'Object' }, - "sentPerType": { type: 'Object' }, - "failedPerType": { type: 'Object' }, - "count": { type: 'Number' } - }, - _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' } - }, - _Hooks: { - "functionName": { type: 'String' }, - "className": { type: 'String' }, - "triggerName": { type: 'String' }, - "url": { type: 'String' } - }, - _GlobalConfig: { - "objectId": { type: 'String' }, - "params": { type: 'Object' } - } -}); - -var requiredColumns = Object.freeze({ - _Product: ["productIdentifier", "icon", "order", "title", "subtitle"], - _Role: ["name", "ACL"] -}); - -var systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus', '_JobStatus']); - -var volatileClasses = Object.freeze(['_JobStatus', '_PushStatus', '_Hooks', '_GlobalConfig']); - -// 10 alpha numberic chars + uppercase -var userIdRegex = /^[a-zA-Z0-9]{10}$/; -// Anything that start with role -var roleRegex = /^role:.*/; -// * permission -var publicRegex = /^\*$/; - -var requireAuthenticationRegex = /^requiresAuthentication$/; - -var permissionKeyRegex = Object.freeze([userIdRegex, roleRegex, publicRegex, requireAuthenticationRegex]); - -function verifyPermissionKey(key) { - var result = permissionKeyRegex.reduce(function (isGood, regEx) { - isGood = isGood || key.match(regEx) != null; - return isGood; - }, false); - if (!result) { - throw new Parse.Error(Parse.Error.INVALID_JSON, '\'' + key + '\' is not a valid key for class level permissions'); - } -} - -var CLPValidKeys = Object.freeze(['find', 'count', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']); -function validateCLP(perms, fields) { - if (!perms) { - return; - } - Object.keys(perms).forEach(function (operation) { - if (CLPValidKeys.indexOf(operation) == -1) { - throw new Parse.Error(Parse.Error.INVALID_JSON, operation + ' is not a valid operation for class level permissions'); - } - - if (operation === 'readUserFields' || operation === 'writeUserFields') { - if (!Array.isArray(perms[operation])) { - throw new Parse.Error(Parse.Error.INVALID_JSON, '\'' + perms[operation] + '\' is not a valid value for class level permissions ' + operation); - } else { - perms[operation].forEach(function (key) { - if (!fields[key] || fields[key].type != 'Pointer' || fields[key].targetClass != '_User') { - throw new Parse.Error(Parse.Error.INVALID_JSON, '\'' + key + '\' is not a valid column for class level pointer permissions ' + operation); - } - }); - } - return; - } - - Object.keys(perms[operation]).forEach(function (key) { - verifyPermissionKey(key); - var perm = perms[operation][key]; - if (perm !== true) { - throw new Parse.Error(Parse.Error.INVALID_JSON, '\'' + perm + '\' is not a valid value for class level permissions ' + operation + ':' + key + ':' + perm); - } - }); - }); -} -var joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/; -var classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/; -function classNameIsValid(className) { - // Valid classes must: - return ( - // Be one of _User, _Installation, _Role, _Session OR - systemClasses.indexOf(className) > -1 || - // Be a join table OR - joinClassRegex.test(className) || - // Include only alpha-numeric and underscores, and not start with an underscore or number - fieldNameIsValid(className) - ); -} - -// Valid fields must be alpha-numeric, and not start with an underscore or number -function fieldNameIsValid(fieldName) { - return classAndFieldRegex.test(fieldName); -} - -// Checks that it's not trying to clobber one of the default fields of the class. -function fieldNameIsValidForClass(fieldName, className) { - if (!fieldNameIsValid(fieldName)) { - return false; - } - if (defaultColumns._Default[fieldName]) { - return false; - } - if (defaultColumns[className] && defaultColumns[className][fieldName]) { - return false; - } - return true; -} - -function invalidClassNameMessage(className) { - return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character '; -} - -var invalidJsonError = new Parse.Error(Parse.Error.INVALID_JSON, "invalid JSON"); -var validNonRelationOrPointerTypes = ['Number', 'String', 'Boolean', 'Date', 'Object', 'Array', 'GeoPoint', 'File']; -// Returns an error suitable for throwing if the type is invalid -var fieldTypeIsInvalid = function fieldTypeIsInvalid(_ref) { - var type = _ref.type, - targetClass = _ref.targetClass; - - if (['Pointer', 'Relation'].indexOf(type) >= 0) { - if (!targetClass) { - return new Parse.Error(135, 'type ' + type + ' needs a class name'); - } else if (typeof targetClass !== 'string') { - return invalidJsonError; - } else if (!classNameIsValid(targetClass)) { - return new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(targetClass)); - } else { - return undefined; - } - } - if (typeof type !== 'string') { - return invalidJsonError; - } - if (validNonRelationOrPointerTypes.indexOf(type) < 0) { - return new Parse.Error(Parse.Error.INCORRECT_TYPE, 'invalid field type: ' + type); - } - return undefined; -}; - -var convertSchemaToAdapterSchema = function convertSchemaToAdapterSchema(schema) { - schema = injectDefaultSchema(schema); - delete schema.fields.ACL; - schema.fields._rperm = { type: 'Array' }; - schema.fields._wperm = { type: 'Array' }; - - if (schema.className === '_User') { - delete schema.fields.password; - schema.fields._hashed_password = { type: 'String' }; - } - - return schema; -}; - -var convertAdapterSchemaToParseSchema = function convertAdapterSchemaToParseSchema(_ref2) { - var schema = _objectWithoutProperties(_ref2, []); - - delete schema.fields._rperm; - delete schema.fields._wperm; - - schema.fields.ACL = { type: 'ACL' }; - - if (schema.className === '_User') { - delete schema.fields.authData; //Auth data is implicit - delete schema.fields._hashed_password; - schema.fields.password = { type: 'String' }; - } - - return schema; -}; - -var injectDefaultSchema = function injectDefaultSchema(_ref3) { - var className = _ref3.className, - fields = _ref3.fields, - classLevelPermissions = _ref3.classLevelPermissions; - return { - className: className, - fields: _extends({}, defaultColumns._Default, defaultColumns[className] || {}, fields), - classLevelPermissions: classLevelPermissions - }; -}; - -var _HooksSchema = { className: "_Hooks", fields: defaultColumns._Hooks }; -var _GlobalConfigSchema = { className: "_GlobalConfig", fields: defaultColumns._GlobalConfig }; -var _PushStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ - className: "_PushStatus", - fields: {}, - classLevelPermissions: {} -})); -var _JobStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ - className: "_JobStatus", - fields: {}, - classLevelPermissions: {} -})); -var VolatileClassesSchemas = [_HooksSchema, _JobStatusSchema, _PushStatusSchema, _GlobalConfigSchema]; - -var dbTypeMatchesObjectType = function dbTypeMatchesObjectType(dbType, objectType) { - if (dbType.type !== objectType.type) return false; - if (dbType.targetClass !== objectType.targetClass) return false; - if (dbType === objectType.type) return true; - if (dbType.type === objectType.type) return true; - return false; -}; - -var typeToString = function typeToString(type) { - if (type.targetClass) { - return type.type + '<' + type.targetClass + '>'; - } - return '' + (type.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. - -var SchemaController = function () { - function SchemaController(databaseAdapter, schemaCache) { - _classCallCheck(this, SchemaController); - - this._dbAdapter = databaseAdapter; - this._cache = schemaCache; - // this.data[className][fieldName] tells you the type of that field, in mongo format - this.data = {}; - // this.perms[className][operation] tells you the acl-style permissions - this.perms = {}; - } - - _createClass(SchemaController, [{ - key: 'reloadData', - value: function reloadData() { - var _this = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { clearCache: false }; - - var promise = Promise.resolve(); - if (options.clearCache) { - promise = promise.then(function () { - return _this._cache.clear(); - }); - } - if (this.reloadDataPromise && !options.clearCache) { - return this.reloadDataPromise; - } - this.reloadDataPromise = promise.then(function () { - return _this.getAllClasses(options); - }).then(function (allSchemas) { - var data = {}; - var perms = {}; - allSchemas.forEach(function (schema) { - data[schema.className] = injectDefaultSchema(schema).fields; - perms[schema.className] = schema.classLevelPermissions; - }); - - // Inject the in-memory classes - volatileClasses.forEach(function (className) { - var schema = injectDefaultSchema({ className: className }); - data[className] = schema.fields; - perms[className] = schema.classLevelPermissions; - }); - _this.data = data; - _this.perms = perms; - delete _this.reloadDataPromise; - }, function (err) { - _this.data = {}; - _this.perms = {}; - delete _this.reloadDataPromise; - throw err; - }); - return this.reloadDataPromise; - } - }, { - key: 'getAllClasses', - value: function getAllClasses() { - var _this2 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { clearCache: false }; - - var promise = Promise.resolve(); - if (options.clearCache) { - promise = this._cache.clear(); - } - return promise.then(function () { - return _this2._cache.getAllClasses(); - }).then(function (allClasses) { - if (allClasses && allClasses.length && !options.clearCache) { - return Promise.resolve(allClasses); - } - return _this2._dbAdapter.getAllClasses().then(function (allSchemas) { - return allSchemas.map(injectDefaultSchema); - }).then(function (allSchemas) { - return _this2._cache.setAllClasses(allSchemas).then(function () { - return allSchemas; - }); - }); - }); - } - }, { - key: 'getOneSchema', - value: function getOneSchema(className) { - var _this3 = this; - - var allowVolatileClasses = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : { clearCache: false }; - - var promise = Promise.resolve(); - if (options.clearCache) { - promise = this._cache.clear(); - } - return promise.then(function () { - if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) { - return Promise.resolve({ - className: className, - fields: _this3.data[className], - classLevelPermissions: _this3.perms[className] - }); - } - return _this3._cache.getOneSchema(className).then(function (cached) { - if (cached && !options.clearCache) { - return Promise.resolve(cached); - } - return _this3._dbAdapter.getClass(className).then(injectDefaultSchema).then(function (result) { - return _this3._cache.setOneSchema(className, result).then(function () { - return result; - }); - }); - }); - }); - } - - // Create a new class that includes the three default fields. - // ACL is an implicit column that does not get an entry in the - // _SCHEMAS database. Returns a promise that resolves with the - // created schema, in mongo format. - // on success, and rejects with an error on fail. Ensure you - // have authorization (master key, or client class creation - // enabled) before calling this function. - - }, { - key: 'addClassIfNotExists', - value: function addClassIfNotExists(className) { - var _this4 = this; - - var fields = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var classLevelPermissions = arguments[2]; - - var validationError = this.validateNewClass(className, fields, classLevelPermissions); - if (validationError) { - return Promise.reject(validationError); - } - - return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields: fields, classLevelPermissions: classLevelPermissions, className: className })).then(convertAdapterSchemaToParseSchema).then(function (res) { - return _this4._cache.clear().then(function () { - return Promise.resolve(res); - }); - }).catch(function (error) { - if (error && error.code === Parse.Error.DUPLICATE_VALUE) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class ' + className + ' already exists.'); - } else { - throw error; - } - }); - } - }, { - key: 'updateClass', - value: function updateClass(className, submittedFields, classLevelPermissions, database) { - var _this5 = this; - - return this.getOneSchema(className).then(function (schema) { - var existingFields = schema.fields; - Object.keys(submittedFields).forEach(function (name) { - var field = submittedFields[name]; - if (existingFields[name] && field.__op !== 'Delete') { - throw new Parse.Error(255, 'Field ' + name + ' exists, cannot update.'); - } - if (!existingFields[name] && field.__op === 'Delete') { - throw new Parse.Error(255, 'Field ' + name + ' does not exist, cannot delete.'); - } - }); - - delete existingFields._rperm; - delete existingFields._wperm; - var newSchema = buildMergedSchemaObject(existingFields, submittedFields); - var validationError = _this5.validateSchemaData(className, newSchema, classLevelPermissions, Object.keys(existingFields)); - if (validationError) { - throw new Parse.Error(validationError.code, validationError.error); - } - - // Finally we have checked to make sure the request is valid and we can start deleting fields. - // Do all deletions first, then a single save to _SCHEMA collection to handle all additions. - var deletedFields = []; - var insertedFields = []; - Object.keys(submittedFields).forEach(function (fieldName) { - if (submittedFields[fieldName].__op === 'Delete') { - deletedFields.push(fieldName); - } else { - insertedFields.push(fieldName); - } - }); - - var deletePromise = Promise.resolve(); - if (deletedFields.length > 0) { - deletePromise = _this5.deleteFields(deletedFields, className, database); - } - - return deletePromise // Delete Everything - .then(function () { - return _this5.reloadData({ clearCache: true }); - }) // Reload our Schema, so we have all the new values - .then(function () { - var promises = insertedFields.map(function (fieldName) { - var type = submittedFields[fieldName]; - return _this5.enforceFieldExists(className, fieldName, type); - }); - return Promise.all(promises); - }).then(function () { - return _this5.setPermissions(className, classLevelPermissions, newSchema); - }) - //TODO: Move this logic into the database adapter - .then(function () { - return { - className: className, - fields: _this5.data[className], - classLevelPermissions: _this5.perms[className] - }; - }); - }).catch(function (error) { - if (error === undefined) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class ' + className + ' does not exist.'); - } else { - throw error; - } - }); - } - - // Returns a promise that resolves successfully to the new schema - // object or fails with a reason. - - }, { - key: 'enforceClassExists', - value: function enforceClassExists(className) { - var _this6 = this; - - if (this.data[className]) { - return Promise.resolve(this); - } - // We don't have this class. Update the schema - return this.addClassIfNotExists(className) - // The schema update succeeded. Reload the schema - .then(function () { - return _this6.reloadData({ clearCache: true }); - }).catch(function () { - // 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 _this6.reloadData({ clearCache: true }); - }).then(function () { - // Ensure that the schema now validates - if (_this6.data[className]) { - return _this6; - } else { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'Failed to add ' + className); - } - }).catch(function () { - // The schema still doesn't validate. Give up - throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate'); - }); - } - }, { - key: 'validateNewClass', - value: function validateNewClass(className) { - var fields = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var classLevelPermissions = arguments[2]; - - if (this.data[className]) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class ' + className + ' already exists.'); - } - if (!classNameIsValid(className)) { - return { - code: Parse.Error.INVALID_CLASS_NAME, - error: invalidClassNameMessage(className) - }; - } - return this.validateSchemaData(className, fields, classLevelPermissions, []); - } - }, { - key: 'validateSchemaData', - value: function validateSchemaData(className, fields, classLevelPermissions, existingFieldNames) { - for (var fieldName in fields) { - if (existingFieldNames.indexOf(fieldName) < 0) { - if (!fieldNameIsValid(fieldName)) { - return { - code: Parse.Error.INVALID_KEY_NAME, - error: 'invalid field name: ' + fieldName - }; - } - if (!fieldNameIsValidForClass(fieldName, className)) { - return { - code: 136, - error: 'field ' + fieldName + ' cannot be added' - }; - } - var error = fieldTypeIsInvalid(fields[fieldName]); - if (error) return { code: error.code, error: error.message }; - } - } - - for (var _fieldName in defaultColumns[className]) { - fields[_fieldName] = defaultColumns[className][_fieldName]; - } - - var geoPoints = Object.keys(fields).filter(function (key) { - return fields[key] && fields[key].type === 'GeoPoint'; - }); - if (geoPoints.length > 1) { - return { - code: Parse.Error.INCORRECT_TYPE, - error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.' - }; - } - validateCLP(classLevelPermissions, fields); - } - - // Sets the Class-level permissions for a given className, which must exist. - - }, { - key: 'setPermissions', - value: function setPermissions(className, perms, newSchema) { - var _this7 = this; - - if (typeof perms === 'undefined') { - return Promise.resolve(); - } - validateCLP(perms, newSchema); - return this._dbAdapter.setClassLevelPermissions(className, perms).then(function () { - return _this7.reloadData({ clearCache: true }); - }); - } - - // Returns a promise that resolves successfully to the new schema - // object if the provided className-fieldName-type tuple is valid. - // The className must already be validated. - // If 'freeze' is true, refuse to update the schema for this field. - - }, { - key: 'enforceFieldExists', - value: function enforceFieldExists(className, fieldName, type) { - var _this8 = this; - - if (fieldName.indexOf(".") > 0) { - // subdocument key (x.y) => ok if x is of type 'object' - fieldName = fieldName.split(".")[0]; - type = 'Object'; - } - if (!fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'Invalid field name: ' + fieldName + '.'); - } - - // If someone tries to create a new field with null/undefined as the value, return; - if (!type) { - return Promise.resolve(this); - } - - return this.reloadData().then(function () { - var expectedType = _this8.getExpectedType(className, fieldName); - if (typeof type === 'string') { - type = { type: type }; - } - - if (expectedType) { - if (!dbTypeMatchesObjectType(expectedType, type)) { - throw new Parse.Error(Parse.Error.INCORRECT_TYPE, 'schema mismatch for ' + className + '.' + fieldName + '; expected ' + typeToString(expectedType) + ' but got ' + typeToString(type)); - } - return _this8; - } - - return _this8._dbAdapter.addFieldIfNotExists(className, fieldName, type).then(function () { - // The update succeeded. Reload the schema - return _this8.reloadData({ clearCache: true }); - }, function () { - //TODO: introspect the error and only reload if the error is one for which is makes sense to reload - - // The update failed. This can be okay - it might have been a race - // condition where another client updated the schema in the same - // way that we wanted to. So, just reload the schema - return _this8.reloadData({ clearCache: true }); - }).then(function () { - // Ensure that the schema now validates - if (!dbTypeMatchesObjectType(_this8.getExpectedType(className, fieldName), type)) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'Could not add field ' + fieldName); - } - // Remove the cached schema - _this8._cache.clear(); - return _this8; - }); - }); - } - - // maintain compatibility - - }, { - key: 'deleteField', - value: function deleteField(fieldName, className, database) { - return this.deleteFields([fieldName], className, database); - } - - // Delete fields, and remove that data from all objects. This is intended - // to remove unused fields, if other writers are writing objects that include - // this field, the field may reappear. Returns a Promise that resolves with - // no object on success, or rejects with { code, error } on failure. - // Passing the database and prefix is necessary in order to drop relation collections - // and remove fields from objects. Ideally the database would belong to - // a database adapter and this function would close over it or access it via member. - - }, { - key: 'deleteFields', - value: function deleteFields(fieldNames, className, database) { - var _this9 = this; - - if (!classNameIsValid(className)) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className)); - } - - fieldNames.forEach(function (fieldName) { - if (!fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid field name: ' + fieldName); - } - //Don't allow deleting the default fields. - if (!fieldNameIsValidForClass(fieldName, className)) { - throw new Parse.Error(136, 'field ' + fieldName + ' cannot be changed'); - } - }); - - return this.getOneSchema(className, false, { clearCache: true }).catch(function (error) { - if (error === undefined) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class ' + className + ' does not exist.'); - } else { - throw error; - } - }).then(function (schema) { - fieldNames.forEach(function (fieldName) { - if (!schema.fields[fieldName]) { - throw new Parse.Error(255, 'Field ' + fieldName + ' does not exist, cannot delete.'); - } - }); - - var schemaFields = _extends({}, schema.fields); - return database.adapter.deleteFields(className, schema, fieldNames).then(function () { - return Promise.all(fieldNames.map(function (fieldName) { - var 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(function () { - _this9._cache.clear(); - }); - } - - // Validates an object provided in REST format. - // Returns a promise that resolves to the new schema if this object is - // valid. - - }, { - key: 'validateObject', - value: function validateObject(className, object, query) { - var geocount = 0; - var promise = this.enforceClassExists(className); - - var _loop = function _loop(fieldName) { - if (object[fieldName] === undefined) { - return 'continue'; - } - var expected = getType(object[fieldName]); - if (expected === 'GeoPoint') { - geocount++; - } - if (geocount > 1) { - // Make sure all field validation operations run before we return. - // If not - we are continuing to run logic, but already provided response from the server. - return { - v: promise.then(function () { - return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE, 'there can only be one geopoint field in a class')); - }) - }; - } - if (!expected) { - return 'continue'; - } - if (fieldName === 'ACL') { - // Every object has ACL implicitly. - return 'continue'; - } - - promise = promise.then(function (schema) { - return schema.enforceFieldExists(className, fieldName, expected); - }); - }; - - for (var fieldName in object) { - var _ret = _loop(fieldName); - - switch (_ret) { - case 'continue': - continue; - - default: - if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; - } - } - promise = thenValidateRequiredColumns(promise, className, object, query); - return promise; - } - - // Validates that all the properties are set for the object - - }, { - key: 'validateRequiredColumns', - value: function validateRequiredColumns(className, object, query) { - var columns = requiredColumns[className]; - if (!columns || columns.length == 0) { - return Promise.resolve(this); - } - - var missingColumns = columns.filter(function (column) { - if (query && query.objectId) { - if (object[column] && _typeof(object[column]) === "object") { - // Trying to delete a required column - return object[column].__op == 'Delete'; - } - // Not trying to do anything there - return false; - } - return !object[column]; - }); - - if (missingColumns.length > 0) { - throw new Parse.Error(Parse.Error.INCORRECT_TYPE, missingColumns[0] + ' is required.'); - } - return Promise.resolve(this); - } - - // Validates the base CLP for an operation - - }, { - key: 'testBaseCLP', - value: function testBaseCLP(className, aclGroup, operation) { - if (!this.perms[className] || !this.perms[className][operation]) { - return true; - } - var classPerms = this.perms[className]; - var perms = classPerms[operation]; - // Handle the public scenario quickly - if (perms['*']) { - return true; - } - // Check permissions against the aclGroup provided (array of userId/roles) - if (aclGroup.some(function (acl) { - return perms[acl] === true; - })) { - return true; - } - return false; - } - - // Validates an operation passes class-level-permissions set in the schema - - }, { - key: 'validatePermission', - value: function validatePermission(className, aclGroup, operation) { - if (this.testBaseCLP(className, aclGroup, operation)) { - return Promise.resolve(); - } - - if (!this.perms[className] || !this.perms[className][operation]) { - return true; - } - var classPerms = this.perms[className]; - var perms = classPerms[operation]; - - // If only for authenticated users - // make sure we have an aclGroup - 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.'); - } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) { - 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' - return Promise.resolve(); - } - - // No matching CLP, let's check the Pointer permissions - // And handle those later - var 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 + '.'); - } - - // Process the readUserFields later - if (Array.isArray(classPerms[permissionField]) && classPerms[permissionField].length > 0) { - return Promise.resolve(); - } - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'Permission denied for action ' + operation + ' on class ' + className + '.'); - } - - // Returns the expected type for a className+key combination - // or undefined if the schema is not set - - }, { - key: 'getExpectedType', - value: function getExpectedType(className, fieldName) { - if (this.data && this.data[className]) { - var expectedType = this.data[className][fieldName]; - return expectedType === 'map' ? 'Object' : expectedType; - } - return undefined; - } - - // Checks if a given class is in the schema. - - }, { - key: 'hasClass', - value: function hasClass(className) { - var _this10 = this; - - return this.reloadData().then(function () { - return !!_this10.data[className]; - }); - } - }]); - - return SchemaController; -}(); - -// Returns a promise for a new Schema. - - -exports.default = SchemaController; -var load = function load(dbAdapter, schemaCache, options) { - var schema = new SchemaController(dbAdapter, schemaCache); - return schema.reloadData(options).then(function () { - return schema; - }); -}; - -// Builds a new schema (in schema API response format) out of an -// existing mongo schema + a schemas API put request. This response -// does not include the default fields, as it is intended to be passed -// to mongoSchemaFromFieldsAndClassName. No validation is done here, it -// is done in mongoSchemaFromFieldsAndClassName. -function buildMergedSchemaObject(existingFields, putRequest) { - var newSchema = {}; - var sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]); - for (var oldField in existingFields) { - if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') { - if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) { - continue; - } - var fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'; - if (!fieldIsDeleted) { - newSchema[oldField] = existingFields[oldField]; - } - } - } - for (var newField in putRequest) { - if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') { - if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) { - continue; - } - newSchema[newField] = putRequest[newField]; - } - } - return newSchema; -} - -// Given a schema promise, construct another schema promise that -// validates this field once the schema loads. -function thenValidateRequiredColumns(schemaPromise, className, object, query) { - return schemaPromise.then(function (schema) { - return schema.validateRequiredColumns(className, object, query); - }); -} - -// Gets the type from a REST API formatted object, where 'type' is -// extended past javascript types to include the rest of the Parse -// type system. -// The output should be a valid schema value. -// TODO: ensure that this is compatible with the format used in Open DB -function getType(obj) { - var type = typeof obj === 'undefined' ? 'undefined' : _typeof(obj); - switch (type) { - case 'boolean': - return 'Boolean'; - case 'string': - return 'String'; - case 'number': - return 'Number'; - case 'map': - case 'object': - if (!obj) { - return undefined; - } - return getObjectType(obj); - case 'function': - case 'symbol': - case 'undefined': - default: - throw 'bad obj: ' + obj; - } -} - -// This gets the type for non-JSON types like pointers and files, but -// also gets the appropriate type for $ operators. -// Returns null if the type is unknown. -function getObjectType(obj) { - if (obj instanceof Array) { - return 'Array'; - } - if (obj.__type) { - switch (obj.__type) { - case 'Pointer': - if (obj.className) { - return { - type: 'Pointer', - targetClass: obj.className - }; - } - 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; - } - break; - } - throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid " + obj.__type); - } - if (obj['$ne']) { - return getObjectType(obj['$ne']); - } - if (obj.__op) { - switch (obj.__op) { - case 'Increment': - return 'Number'; - case 'Delete': - return null; - case 'Add': - case 'AddUnique': - case 'Remove': - return 'Array'; - case 'AddRelation': - case 'RemoveRelation': - return { - type: 'Relation', - targetClass: obj.objects[0].className - }; - case 'Batch': - return getObjectType(obj.ops[0]); - default: - throw 'unexpected op: ' + obj.__op; - } - } - return 'Object'; -} - -exports.load = load; -exports.classNameIsValid = classNameIsValid; -exports.fieldNameIsValid = fieldNameIsValid; -exports.invalidClassNameMessage = invalidClassNameMessage; -exports.buildMergedSchemaObject = buildMergedSchemaObject; -exports.systemClasses = systemClasses; -exports.defaultColumns = defaultColumns; -exports.convertSchemaToAdapterSchema = convertSchemaToAdapterSchema; -exports.VolatileClassesSchemas = VolatileClassesSchemas; \ No newline at end of file diff --git a/lib/Controllers/UserController.js b/lib/Controllers/UserController.js deleted file mode 100644 index 3519a22b25..0000000000 --- a/lib/Controllers/UserController.js +++ /dev/null @@ -1,318 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.UserController = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - -var _cryptoUtils = require('../cryptoUtils'); - -var _triggers = require('../triggers'); - -var _AdaptableController2 = require('./AdaptableController'); - -var _AdaptableController3 = _interopRequireDefault(_AdaptableController2); - -var _MailAdapter = require('../Adapters/Email/MailAdapter'); - -var _MailAdapter2 = _interopRequireDefault(_MailAdapter); - -var _rest = require('../rest'); - -var _rest2 = _interopRequireDefault(_rest); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var RestQuery = require('../RestQuery'); -var RestWrite = require('../RestWrite'); -var Auth = require('../Auth'); - -var UserController = exports.UserController = function (_AdaptableController) { - _inherits(UserController, _AdaptableController); - - function UserController(adapter, appId) { - var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - - _classCallCheck(this, UserController); - - return _possibleConstructorReturn(this, (UserController.__proto__ || Object.getPrototypeOf(UserController)).call(this, adapter, appId, options)); - } - - _createClass(UserController, [{ - key: 'validateAdapter', - value: function validateAdapter(adapter) { - // Allow no adapter - if (!adapter && !this.shouldVerifyEmails) { - return; - } - _get(UserController.prototype.__proto__ || Object.getPrototypeOf(UserController.prototype), 'validateAdapter', this).call(this, adapter); - } - }, { - key: 'expectedAdapterType', - value: function expectedAdapterType() { - return _MailAdapter2.default; - } - }, { - key: 'setEmailVerifyToken', - value: function setEmailVerifyToken(user) { - if (this.shouldVerifyEmails) { - user._email_verify_token = (0, _cryptoUtils.randomString)(25); - user.emailVerified = false; - - if (this.config.emailVerifyTokenValidityDuration) { - user._email_verify_token_expires_at = _node2.default._encode(this.config.generateEmailVerifyTokenExpiresAt()); - } - } - } - }, { - key: 'verifyEmail', - value: function verifyEmail(username, token) { - var _this2 = this; - - if (!this.shouldVerifyEmails) { - // Trying to verify email when not enabled - // TODO: Better error here. - throw undefined; - } - - var query = { username: username, _email_verify_token: token }; - var 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 - if (this.config.emailVerifyTokenValidityDuration) { - query.emailVerified = false; - query._email_verify_token_expires_at = { $gt: _node2.default._encode(new Date()) }; - - updateFields._email_verify_token_expires_at = { __op: 'Delete' }; - } - - var checkIfAlreadyVerified = new RestQuery(this.config, Auth.master(this.config), '_User', { username: username, emailVerified: true }); - return checkIfAlreadyVerified.execute().then(function (result) { - if (result.results.length) { - return Promise.resolve(result.results.length[0]); - } - return new RestWrite(_this2.config, Auth.master(_this2.config), '_User', query, updateFields).execute(); - }); - } - }, { - key: 'checkResetTokenValidity', - value: function checkResetTokenValidity(username, token) { - var _this3 = this; - - return this.config.database.find('_User', { - username: username, - _perishable_token: token - }, { limit: 1 }).then(function (results) { - if (results.length != 1) { - throw undefined; - } - - if (_this3.config.passwordPolicy && _this3.config.passwordPolicy.resetTokenValidityDuration) { - var 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'; - } - - return results[0]; - }); - } - }, { - key: 'getUserIfNeeded', - value: function getUserIfNeeded(user) { - if (user.username && user.email) { - return Promise.resolve(user); - } - var where = {}; - if (user.username) { - where.username = user.username; - } - if (user.email) { - where.email = user.email; - } - - 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]; - }); - } - }, { - key: 'sendVerificationEmail', - value: function sendVerificationEmail(user) { - var _this4 = this; - - if (!this.shouldVerifyEmails) { - return; - } - var token = encodeURIComponent(user._email_verify_token); - // We may need to fetch the user in case of update email - this.getUserIfNeeded(user).then(function (user) { - var username = encodeURIComponent(user.username); - - var link = buildEmailLink(_this4.config.verifyEmailURL, username, token, _this4.config); - var options = { - appName: _this4.config.appName, - link: link, - user: (0, _triggers.inflate)('_User', user) - }; - if (_this4.adapter.sendVerificationEmail) { - _this4.adapter.sendVerificationEmail(options); - } else { - _this4.adapter.sendMail(_this4.defaultVerificationEmail(options)); - } - }); - } - }, { - key: 'resendVerificationEmail', - value: function resendVerificationEmail(username) { - var _this5 = this; - - return this.getUserIfNeeded({ username: username }).then(function (aUser) { - if (!aUser || aUser.emailVerified) { - throw undefined; - } - _this5.setEmailVerifyToken(aUser); - return _this5.config.database.update('_User', { username: username }, aUser).then(function () { - _this5.sendVerificationEmail(aUser); - }); - }); - } - }, { - key: 'setPasswordResetToken', - value: function setPasswordResetToken(email) { - var token = { _perishable_token: (0, _cryptoUtils.randomString)(25) }; - - if (this.config.passwordPolicy && this.config.passwordPolicy.resetTokenValidityDuration) { - token._perishable_token_expires_at = _node2.default._encode(this.config.generatePasswordResetTokenExpiresAt()); - } - - return this.config.database.update('_User', { $or: [{ email: email }, { username: email, email: { $exists: false } }] }, token, {}, true); - } - }, { - key: 'sendPasswordResetEmail', - value: function sendPasswordResetEmail(email) { - var _this6 = this; - - if (!this.adapter) { - throw "Trying to send a reset password but no adapter is set"; - // TODO: No adapter? - } - - return this.setPasswordResetToken(email).then(function (user) { - var token = encodeURIComponent(user._perishable_token); - var username = encodeURIComponent(user.username); - - var link = buildEmailLink(_this6.config.requestResetPasswordURL, username, token, _this6.config); - var options = { - appName: _this6.config.appName, - link: link, - user: (0, _triggers.inflate)('_User', user) - }; - - if (_this6.adapter.sendPasswordResetEmail) { - _this6.adapter.sendPasswordResetEmail(options); - } else { - _this6.adapter.sendMail(_this6.defaultResetPasswordEmail(options)); - } - - return Promise.resolve(user); - }); - } - }, { - key: 'updatePassword', - value: function updatePassword(username, token, password) { - var _this7 = this; - - return this.checkResetTokenValidity(username, token).then(function (user) { - return updateUserPassword(user.objectId, password, _this7.config); - }) - // clear reset password token - .then(function () { - return _this7.config.database.update('_User', { username: username }, { - _perishable_token: { __op: 'Delete' }, - _perishable_token_expires_at: { __op: 'Delete' } - }); - }).catch(function (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); - } - }); - } - }, { - key: 'defaultVerificationEmail', - value: function defaultVerificationEmail(_ref) { - var link = _ref.link, - user = _ref.user, - appName = _ref.appName; - - var 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; - var to = user.get("email"); - var subject = 'Please verify your e-mail for ' + appName; - return { text: text, to: to, subject: subject }; - } - }, { - key: 'defaultResetPasswordEmail', - value: function defaultResetPasswordEmail(_ref2) { - var link = _ref2.link, - user = _ref2.user, - appName = _ref2.appName; - - var text = "Hi,\n\n" + "You requested to reset your password for " + appName + ".\n\n" + "" + "Click here to reset it:\n" + link; - var to = user.get("email") || user.get('username'); - var subject = 'Password Reset for ' + appName; - return { text: text, to: to, subject: subject }; - } - }, { - key: 'shouldVerifyEmails', - get: function get() { - return this.options.verifyUserEmails; - } - }]); - - return UserController; -}(_AdaptableController3.default); - -// Mark this private - - -function updateUserPassword(userId, password, config) { - return _rest2.default.update(config, Auth.master(config), '_User', userId, { - password: password - }); -} - -function buildEmailLink(destination, username, token, config) { - var usernameAndToken = 'token=' + token + '&username=' + username; - - if (config.parseFrameURL) { - var destinationWithoutHost = destination.replace(config.publicServerURL, ''); - - return config.parseFrameURL + '?link=' + encodeURIComponent(destinationWithoutHost) + '&' + usernameAndToken; - } else { - return destination + '?' + usernameAndToken; - } -} - -exports.default = UserController; \ No newline at end of file diff --git a/lib/LiveQuery/Client.js b/lib/LiveQuery/Client.js deleted file mode 100644 index 6ac2e908d3..0000000000 --- a/lib/LiveQuery/Client.js +++ /dev/null @@ -1,158 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Client = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _logger = require('../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var dafaultFields = ['className', 'objectId', 'updatedAt', 'createdAt', 'ACL']; - -var Client = function () { - function Client(id, parseWebSocket) { - _classCallCheck(this, Client); - - this.id = id; - this.parseWebSocket = parseWebSocket; - this.roles = []; - this.subscriptionInfos = new Map(); - this.pushConnect = this._pushEvent('connected'); - this.pushSubscribe = this._pushEvent('subscribed'); - this.pushUnsubscribe = this._pushEvent('unsubscribed'); - this.pushCreate = this._pushEvent('create'); - this.pushEnter = this._pushEvent('enter'); - this.pushUpdate = this._pushEvent('update'); - this.pushDelete = this._pushEvent('delete'); - this.pushLeave = this._pushEvent('leave'); - } - - _createClass(Client, [{ - key: 'addSubscriptionInfo', - value: function addSubscriptionInfo(requestId, subscriptionInfo) { - this.subscriptionInfos.set(requestId, subscriptionInfo); - } - }, { - key: 'getSubscriptionInfo', - value: function getSubscriptionInfo(requestId) { - return this.subscriptionInfos.get(requestId); - } - }, { - key: 'deleteSubscriptionInfo', - value: function deleteSubscriptionInfo(requestId) { - return this.subscriptionInfos.delete(requestId); - } - }, { - key: '_pushEvent', - value: function _pushEvent(type) { - return function (subscriptionId, parseObjectJSON) { - var response = { - 'op': type, - 'clientId': this.id - }; - if (typeof subscriptionId !== 'undefined') { - response['requestId'] = subscriptionId; - } - if (typeof parseObjectJSON !== 'undefined') { - var fields = void 0; - if (this.subscriptionInfos.has(subscriptionId)) { - fields = this.subscriptionInfos.get(subscriptionId).fields; - } - response['object'] = this._toJSONWithFields(parseObjectJSON, fields); - } - Client.pushResponse(this.parseWebSocket, JSON.stringify(response)); - }; - } - }, { - key: '_toJSONWithFields', - value: function _toJSONWithFields(parseObjectJSON, fields) { - if (!fields) { - return parseObjectJSON; - } - var limitedParseObject = {}; - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = dafaultFields[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var field = _step.value; - - limitedParseObject[field] = parseObjectJSON[field]; - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = fields[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var _field = _step2.value; - - if (_field in parseObjectJSON) { - limitedParseObject[_field] = parseObjectJSON[_field]; - } - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - - return limitedParseObject; - } - }], [{ - key: 'pushResponse', - value: function pushResponse(parseWebSocket, message) { - _logger2.default.verbose('Push Response : %j', message); - parseWebSocket.send(message); - } - }, { - key: 'pushError', - value: function pushError(parseWebSocket, code, error) { - var reconnect = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : true; - - Client.pushResponse(parseWebSocket, JSON.stringify({ - 'op': 'error', - 'error': error, - 'code': code, - 'reconnect': reconnect - })); - } - }]); - - return Client; -}(); - -exports.Client = Client; \ No newline at end of file diff --git a/lib/LiveQuery/Id.js b/lib/LiveQuery/Id.js deleted file mode 100644 index 048cc21084..0000000000 --- a/lib/LiveQuery/Id.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var Id = function () { - function Id(className, objectId) { - _classCallCheck(this, Id); - - this.className = className; - this.objectId = objectId; - } - - _createClass(Id, [{ - key: 'toString', - value: function toString() { - return this.className + ':' + this.objectId; - } - }], [{ - key: 'fromString', - value: function fromString(str) { - var split = str.split(':'); - if (split.length !== 2) { - throw new TypeError('Cannot create Id object from this string'); - } - return new Id(split[0], split[1]); - } - }]); - - return Id; -}(); - -module.exports = Id; \ No newline at end of file diff --git a/lib/LiveQuery/ParseCloudCodePublisher.js b/lib/LiveQuery/ParseCloudCodePublisher.js deleted file mode 100644 index 9f1bf1d7b1..0000000000 --- a/lib/LiveQuery/ParseCloudCodePublisher.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.ParseCloudCodePublisher = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _ParsePubSub = require('./ParsePubSub'); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _logger = require('../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var ParseCloudCodePublisher = function () { - - // config object of the publisher, right now it only contains the redisURL, - // but we may extend it later. - function ParseCloudCodePublisher() { - var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - _classCallCheck(this, ParseCloudCodePublisher); - - this.parsePublisher = _ParsePubSub.ParsePubSub.createPublisher(config); - } - - _createClass(ParseCloudCodePublisher, [{ - key: 'onCloudCodeAfterSave', - value: function onCloudCodeAfterSave(request) { - this._onCloudCodeMessage(_node2.default.applicationId + 'afterSave', request); - } - }, { - key: 'onCloudCodeAfterDelete', - value: function onCloudCodeAfterDelete(request) { - this._onCloudCodeMessage(_node2.default.applicationId + 'afterDelete', request); - } - - // Request is the request object from cloud code functions. request.object is a ParseObject. - - }, { - key: '_onCloudCodeMessage', - value: function _onCloudCodeMessage(type, request) { - _logger2.default.verbose('Raw request from cloud code current : %j | original : %j', request.object, request.original); - // We need the full JSON which includes className - var message = { - currentParseObject: request.object._toFullJSON() - }; - if (request.original) { - message.originalParseObject = request.original._toFullJSON(); - } - this.parsePublisher.publish(type, JSON.stringify(message)); - } - }]); - - return ParseCloudCodePublisher; -}(); - -exports.ParseCloudCodePublisher = ParseCloudCodePublisher; \ No newline at end of file diff --git a/lib/LiveQuery/ParseLiveQueryServer.js b/lib/LiveQuery/ParseLiveQueryServer.js deleted file mode 100644 index eb2792ebdb..0000000000 --- a/lib/LiveQuery/ParseLiveQueryServer.js +++ /dev/null @@ -1,821 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.ParseLiveQueryServer = undefined; - -var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _tv = require('tv4'); - -var _tv2 = _interopRequireDefault(_tv); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _Subscription = require('./Subscription'); - -var _Client = require('./Client'); - -var _ParseWebSocketServer = require('./ParseWebSocketServer'); - -var _logger = require('../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -var _RequestSchema = require('./RequestSchema'); - -var _RequestSchema2 = _interopRequireDefault(_RequestSchema); - -var _QueryTools = require('./QueryTools'); - -var _ParsePubSub = require('./ParsePubSub'); - -var _SessionTokenCache = require('./SessionTokenCache'); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var ParseLiveQueryServer = function () { - // className -> (queryHash -> subscription) - function ParseLiveQueryServer(server, config) { - var _this = this; - - _classCallCheck(this, ParseLiveQueryServer); - - this.clientId = 0; - this.clients = new Map(); - this.subscriptions = new Map(); - - config = config || {}; - - // Store keys, convert obj to map - var keyPairs = config.keyPairs || {}; - this.keyPairs = new Map(); - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = Object.keys(keyPairs)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var key = _step.value; - - this.keyPairs.set(key, keyPairs[key]); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - _logger2.default.verbose('Support key pairs', this.keyPairs); - - // Initialize Parse - _node2.default.Object.disableSingleInstance(); - - var serverURL = config.serverURL || _node2.default.serverURL; - _node2.default.serverURL = serverURL; - var appId = config.appId || _node2.default.applicationId; - var javascriptKey = _node2.default.javaScriptKey; - var masterKey = config.masterKey || _node2.default.masterKey; - _node2.default.initialize(appId, javascriptKey, masterKey); - - // Initialize websocket server - this.parseWebSocketServer = new _ParseWebSocketServer.ParseWebSocketServer(server, function (parseWebsocket) { - return _this._onConnect(parseWebsocket); - }, config.websocketTimeout); - - // Initialize subscriber - this.subscriber = _ParsePubSub.ParsePubSub.createSubscriber(config); - this.subscriber.subscribe(_node2.default.applicationId + 'afterSave'); - this.subscriber.subscribe(_node2.default.applicationId + 'afterDelete'); - // Register message handler for subscriber. When publisher get messages, it will publish message - // to the subscribers and the handler will be called. - this.subscriber.on('message', function (channel, messageStr) { - _logger2.default.verbose('Subscribe messsage %j', messageStr); - var message = void 0; - try { - message = JSON.parse(messageStr); - } catch (e) { - _logger2.default.error('unable to parse message', messageStr, e); - return; - } - _this._inflateParseObject(message); - if (channel === _node2.default.applicationId + 'afterSave') { - _this._onAfterSave(message); - } else if (channel === _node2.default.applicationId + 'afterDelete') { - _this._onAfterDelete(message); - } else { - _logger2.default.error('Get message %s from unknown channel %j', message, channel); - } - }); - - // Initialize sessionToken cache - this.sessionTokenCache = new _SessionTokenCache.SessionTokenCache(config.cacheTimeout); - } - - // Message is the JSON object from publisher. Message.currentParseObject is the ParseObject JSON after changes. - // Message.originalParseObject is the original ParseObject JSON. - - // The subscriber we use to get object update from publisher - - - _createClass(ParseLiveQueryServer, [{ - key: '_inflateParseObject', - value: function _inflateParseObject(message) { - // Inflate merged object - var currentParseObject = message.currentParseObject; - var className = currentParseObject.className; - var parseObject = new _node2.default.Object(className); - parseObject._finishFetch(currentParseObject); - message.currentParseObject = parseObject; - // Inflate original object - var originalParseObject = message.originalParseObject; - if (originalParseObject) { - className = originalParseObject.className; - parseObject = new _node2.default.Object(className); - parseObject._finishFetch(originalParseObject); - message.originalParseObject = parseObject; - } - } - - // Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes. - // Message.originalParseObject is the original ParseObject. - - }, { - key: '_onAfterDelete', - value: function _onAfterDelete(message) { - var _this2 = this; - - _logger2.default.verbose(_node2.default.applicationId + 'afterDelete is triggered'); - - var deletedParseObject = message.currentParseObject.toJSON(); - var className = deletedParseObject.className; - _logger2.default.verbose('ClassName: %j | ObjectId: %s', className, deletedParseObject.id); - _logger2.default.verbose('Current client number : %d', this.clients.size); - - var classSubscriptions = this.subscriptions.get(className); - if (typeof classSubscriptions === 'undefined') { - _logger2.default.debug('Can not find subscriptions under this class ' + className); - return; - } - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = classSubscriptions.values()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var subscription = _step2.value; - - var isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription); - if (!isSubscriptionMatched) { - continue; - } - var _iteratorNormalCompletion3 = true; - var _didIteratorError3 = false; - var _iteratorError3 = undefined; - - try { - var _loop = function _loop() { - var _step3$value = _slicedToArray(_step3.value, 2), - clientId = _step3$value[0], - requestIds = _step3$value[1]; - - var client = _this2.clients.get(clientId); - if (typeof client === 'undefined') { - return 'continue'; - } - var _iteratorNormalCompletion4 = true; - var _didIteratorError4 = false; - var _iteratorError4 = undefined; - - try { - var _loop2 = function _loop2() { - var requestId = _step4.value; - - var acl = message.currentParseObject.getACL(); - // Check ACL - _this2._matchesACL(acl, client, requestId).then(function (isMatched) { - if (!isMatched) { - return null; - } - client.pushDelete(requestId, deletedParseObject); - }, function (error) { - _logger2.default.error('Matching ACL error : ', error); - }); - }; - - for (var _iterator4 = requestIds[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { - _loop2(); - } - } catch (err) { - _didIteratorError4 = true; - _iteratorError4 = err; - } finally { - try { - if (!_iteratorNormalCompletion4 && _iterator4.return) { - _iterator4.return(); - } - } finally { - if (_didIteratorError4) { - throw _iteratorError4; - } - } - } - }; - - for (var _iterator3 = _lodash2.default.entries(subscription.clientRequestIds)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { - var _ret = _loop(); - - if (_ret === 'continue') continue; - } - } catch (err) { - _didIteratorError3 = true; - _iteratorError3 = err; - } finally { - try { - if (!_iteratorNormalCompletion3 && _iterator3.return) { - _iterator3.return(); - } - } finally { - if (_didIteratorError3) { - throw _iteratorError3; - } - } - } - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - } - - // Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes. - // Message.originalParseObject is the original ParseObject. - - }, { - key: '_onAfterSave', - value: function _onAfterSave(message) { - var _this3 = this; - - _logger2.default.verbose(_node2.default.applicationId + 'afterSave is triggered'); - - var originalParseObject = null; - if (message.originalParseObject) { - originalParseObject = message.originalParseObject.toJSON(); - } - var currentParseObject = message.currentParseObject.toJSON(); - var className = currentParseObject.className; - _logger2.default.verbose('ClassName: %s | ObjectId: %s', className, currentParseObject.id); - _logger2.default.verbose('Current client number : %d', this.clients.size); - - var classSubscriptions = this.subscriptions.get(className); - if (typeof classSubscriptions === 'undefined') { - _logger2.default.debug('Can not find subscriptions under this class ' + className); - return; - } - var _iteratorNormalCompletion5 = true; - var _didIteratorError5 = false; - var _iteratorError5 = undefined; - - try { - var _loop3 = function _loop3() { - var subscription = _step5.value; - - var isOriginalSubscriptionMatched = _this3._matchesSubscription(originalParseObject, subscription); - var isCurrentSubscriptionMatched = _this3._matchesSubscription(currentParseObject, subscription); - var _iteratorNormalCompletion6 = true; - var _didIteratorError6 = false; - var _iteratorError6 = undefined; - - try { - var _loop4 = function _loop4() { - var _step6$value = _slicedToArray(_step6.value, 2), - clientId = _step6$value[0], - requestIds = _step6$value[1]; - - var client = _this3.clients.get(clientId); - if (typeof client === 'undefined') { - return 'continue'; - } - var _iteratorNormalCompletion7 = true; - var _didIteratorError7 = false; - var _iteratorError7 = undefined; - - try { - var _loop5 = function _loop5() { - var requestId = _step7.value; - - // Set orignal ParseObject ACL checking promise, if the object does not match - // subscription, we do not need to check ACL - var originalACLCheckingPromise = void 0; - if (!isOriginalSubscriptionMatched) { - originalACLCheckingPromise = _node2.default.Promise.as(false); - } else { - var originalACL = void 0; - if (message.originalParseObject) { - originalACL = message.originalParseObject.getACL(); - } - originalACLCheckingPromise = _this3._matchesACL(originalACL, client, requestId); - } - // Set current ParseObject ACL checking promise, if the object does not match - // subscription, we do not need to check ACL - var currentACLCheckingPromise = void 0; - if (!isCurrentSubscriptionMatched) { - currentACLCheckingPromise = _node2.default.Promise.as(false); - } else { - var currentACL = message.currentParseObject.getACL(); - currentACLCheckingPromise = _this3._matchesACL(currentACL, client, requestId); - } - - _node2.default.Promise.when(originalACLCheckingPromise, currentACLCheckingPromise).then(function (isOriginalMatched, isCurrentMatched) { - _logger2.default.verbose('Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s', originalParseObject, currentParseObject, isOriginalSubscriptionMatched, isCurrentSubscriptionMatched, isOriginalMatched, isCurrentMatched, subscription.hash); - - // Decide event type - var type = void 0; - if (isOriginalMatched && isCurrentMatched) { - type = 'Update'; - } else if (isOriginalMatched && !isCurrentMatched) { - type = 'Leave'; - } else if (!isOriginalMatched && isCurrentMatched) { - if (originalParseObject) { - type = 'Enter'; - } else { - type = 'Create'; - } - } else { - return null; - } - var functionName = 'push' + type; - client[functionName](requestId, currentParseObject); - }, function (error) { - _logger2.default.error('Matching ACL error : ', error); - }); - }; - - for (var _iterator7 = requestIds[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { - _loop5(); - } - } catch (err) { - _didIteratorError7 = true; - _iteratorError7 = err; - } finally { - try { - if (!_iteratorNormalCompletion7 && _iterator7.return) { - _iterator7.return(); - } - } finally { - if (_didIteratorError7) { - throw _iteratorError7; - } - } - } - }; - - for (var _iterator6 = _lodash2.default.entries(subscription.clientRequestIds)[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { - var _ret4 = _loop4(); - - if (_ret4 === 'continue') continue; - } - } catch (err) { - _didIteratorError6 = true; - _iteratorError6 = err; - } finally { - try { - if (!_iteratorNormalCompletion6 && _iterator6.return) { - _iterator6.return(); - } - } finally { - if (_didIteratorError6) { - throw _iteratorError6; - } - } - } - }; - - for (var _iterator5 = classSubscriptions.values()[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { - _loop3(); - } - } catch (err) { - _didIteratorError5 = true; - _iteratorError5 = err; - } finally { - try { - if (!_iteratorNormalCompletion5 && _iterator5.return) { - _iterator5.return(); - } - } finally { - if (_didIteratorError5) { - throw _iteratorError5; - } - } - } - } - }, { - key: '_onConnect', - value: function _onConnect(parseWebsocket) { - var _this4 = this; - - parseWebsocket.on('message', function (request) { - if (typeof request === 'string') { - try { - request = JSON.parse(request); - } catch (e) { - _logger2.default.error('unable to parse request', request, e); - return; - } - } - _logger2.default.verbose('Request: %j', request); - - // Check whether this request is a valid request, return error directly if not - if (!_tv2.default.validate(request, _RequestSchema2.default['general']) || !_tv2.default.validate(request, _RequestSchema2.default[request.op])) { - _Client.Client.pushError(parseWebsocket, 1, _tv2.default.error.message); - _logger2.default.error('Connect message error %s', _tv2.default.error.message); - return; - } - - switch (request.op) { - case 'connect': - _this4._handleConnect(parseWebsocket, request); - break; - case 'subscribe': - _this4._handleSubscribe(parseWebsocket, request); - break; - case 'update': - _this4._handleUpdateSubscription(parseWebsocket, request); - break; - case 'unsubscribe': - _this4._handleUnsubscribe(parseWebsocket, request); - break; - default: - _Client.Client.pushError(parseWebsocket, 3, 'Get unknown operation'); - _logger2.default.error('Get unknown operation', request.op); - } - }); - - parseWebsocket.on('disconnect', function () { - _logger2.default.info('Client disconnect: %d', parseWebsocket.clientId); - var clientId = parseWebsocket.clientId; - if (!_this4.clients.has(clientId)) { - _logger2.default.error('Can not find client %d on disconnect', clientId); - return; - } - - // Delete client - var client = _this4.clients.get(clientId); - _this4.clients.delete(clientId); - - // Delete client from subscriptions - var _iteratorNormalCompletion8 = true; - var _didIteratorError8 = false; - var _iteratorError8 = undefined; - - try { - for (var _iterator8 = _lodash2.default.entries(client.subscriptionInfos)[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { - var _step8$value = _slicedToArray(_step8.value, 2), - _requestId = _step8$value[0], - subscriptionInfo = _step8$value[1]; - - var _subscription = subscriptionInfo.subscription; - _subscription.deleteClientSubscription(clientId, _requestId); - - // If there is no client which is subscribing this subscription, remove it from subscriptions - var classSubscriptions = _this4.subscriptions.get(_subscription.className); - if (!_subscription.hasSubscribingClient()) { - classSubscriptions.delete(_subscription.hash); - } - // If there is no subscriptions under this class, remove it from subscriptions - if (classSubscriptions.size === 0) { - _this4.subscriptions.delete(_subscription.className); - } - } - } catch (err) { - _didIteratorError8 = true; - _iteratorError8 = err; - } finally { - try { - if (!_iteratorNormalCompletion8 && _iterator8.return) { - _iterator8.return(); - } - } finally { - if (_didIteratorError8) { - throw _iteratorError8; - } - } - } - - _logger2.default.verbose('Current clients %d', _this4.clients.size); - _logger2.default.verbose('Current subscriptions %d', _this4.subscriptions.size); - }); - } - }, { - key: '_matchesSubscription', - value: function _matchesSubscription(parseObject, subscription) { - // Object is undefined or null, not match - if (!parseObject) { - return false; - } - return (0, _QueryTools.matchesQuery)(parseObject, subscription.query); - } - }, { - key: '_matchesACL', - value: function _matchesACL(acl, client, requestId) { - var _this5 = this; - - // If ACL is undefined or null, or ACL has public read access, return true directly - if (!acl || acl.getPublicReadAccess()) { - return _node2.default.Promise.as(true); - } - // Check subscription sessionToken matches ACL first - var subscriptionInfo = client.getSubscriptionInfo(requestId); - if (typeof subscriptionInfo === 'undefined') { - return _node2.default.Promise.as(false); - } - - var subscriptionSessionToken = subscriptionInfo.sessionToken; - return this.sessionTokenCache.getUserId(subscriptionSessionToken).then(function (userId) { - return acl.getReadAccess(userId); - }).then(function (isSubscriptionSessionTokenMatched) { - if (isSubscriptionSessionTokenMatched) { - return _node2.default.Promise.as(true); - } - - // Check if the user has any roles that match the ACL - return new _node2.default.Promise(function (resolve, reject) { - - // Resolve false right away if the acl doesn't have any roles - var acl_has_roles = Object.keys(acl.permissionsById).some(function (key) { - return key.startsWith("role:"); - }); - if (!acl_has_roles) { - return resolve(false); - } - - _this5.sessionTokenCache.getUserId(subscriptionSessionToken).then(function (userId) { - - // Pass along a null if there is no user id - if (!userId) { - return _node2.default.Promise.as(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 _node2.default.User(); - user.id = userId; - return user; - }).then(function (user) { - - // Pass along an empty array (of roles) if no user - if (!user) { - return _node2.default.Promise.as([]); - } - - // Then get the user's roles - var rolesQuery = new _node2.default.Query(_node2.default.Role); - rolesQuery.equalTo("users", user); - return rolesQuery.find({ useMasterKey: true }); - }).then(function (roles) { - - // Finally, see if any of the user's roles allow them read access - var _iteratorNormalCompletion9 = true; - var _didIteratorError9 = false; - var _iteratorError9 = undefined; - - try { - for (var _iterator9 = roles[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { - var role = _step9.value; - - if (acl.getRoleReadAccess(role)) { - return resolve(true); - } - } - } catch (err) { - _didIteratorError9 = true; - _iteratorError9 = err; - } finally { - try { - if (!_iteratorNormalCompletion9 && _iterator9.return) { - _iterator9.return(); - } - } finally { - if (_didIteratorError9) { - throw _iteratorError9; - } - } - } - - resolve(false); - }).catch(function (error) { - reject(error); - }); - }); - }).then(function (isRoleMatched) { - - if (isRoleMatched) { - return _node2.default.Promise.as(true); - } - - // Check client sessionToken matches ACL - var clientSessionToken = client.sessionToken; - return _this5.sessionTokenCache.getUserId(clientSessionToken).then(function (userId) { - return acl.getReadAccess(userId); - }); - }).then(function (isMatched) { - return _node2.default.Promise.as(isMatched); - }, function () { - return _node2.default.Promise.as(false); - }); - } - }, { - key: '_handleConnect', - value: function _handleConnect(parseWebsocket, request) { - if (!this._validateKeys(request, this.keyPairs)) { - _Client.Client.pushError(parseWebsocket, 4, 'Key in request is not valid'); - _logger2.default.error('Key in request is not valid'); - return; - } - var client = new _Client.Client(this.clientId, parseWebsocket); - parseWebsocket.clientId = this.clientId; - this.clientId += 1; - this.clients.set(parseWebsocket.clientId, client); - _logger2.default.info('Create new client: %d', parseWebsocket.clientId); - client.pushConnect(); - } - }, { - key: '_validateKeys', - value: function _validateKeys(request, validKeyPairs) { - if (!validKeyPairs || validKeyPairs.size == 0) { - return true; - } - var isValid = false; - var _iteratorNormalCompletion10 = true; - var _didIteratorError10 = false; - var _iteratorError10 = undefined; - - try { - for (var _iterator10 = validKeyPairs[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { - var _step10$value = _slicedToArray(_step10.value, 2), - key = _step10$value[0], - secret = _step10$value[1]; - - if (!request[key] || request[key] !== secret) { - continue; - } - isValid = true; - break; - } - } catch (err) { - _didIteratorError10 = true; - _iteratorError10 = err; - } finally { - try { - if (!_iteratorNormalCompletion10 && _iterator10.return) { - _iterator10.return(); - } - } finally { - if (_didIteratorError10) { - throw _iteratorError10; - } - } - } - - return isValid; - } - }, { - key: '_handleSubscribe', - value: function _handleSubscribe(parseWebsocket, request) { - // If we can not find this client, return error to client - if (!parseWebsocket.hasOwnProperty('clientId')) { - _Client.Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before subscribing'); - _logger2.default.error('Can not find this client, make sure you connect to server before subscribing'); - return; - } - var client = this.clients.get(parseWebsocket.clientId); - - // Get subscription from subscriptions, create one if necessary - var subscriptionHash = (0, _QueryTools.queryHash)(request.query); - // Add className to subscriptions if necessary - var className = request.query.className; - if (!this.subscriptions.has(className)) { - this.subscriptions.set(className, new Map()); - } - var classSubscriptions = this.subscriptions.get(className); - var subscription = void 0; - if (classSubscriptions.has(subscriptionHash)) { - subscription = classSubscriptions.get(subscriptionHash); - } else { - subscription = new _Subscription.Subscription(className, request.query.where, subscriptionHash); - classSubscriptions.set(subscriptionHash, subscription); - } - - // Add subscriptionInfo to client - var subscriptionInfo = { - subscription: subscription - }; - // Add selected fields and sessionToken for this subscription if necessary - if (request.query.fields) { - subscriptionInfo.fields = request.query.fields; - } - if (request.sessionToken) { - subscriptionInfo.sessionToken = request.sessionToken; - } - client.addSubscriptionInfo(request.requestId, subscriptionInfo); - - // Add clientId to subscription - subscription.addClientSubscription(parseWebsocket.clientId, request.requestId); - - client.pushSubscribe(request.requestId); - - _logger2.default.verbose('Create client %d new subscription: %d', parseWebsocket.clientId, request.requestId); - _logger2.default.verbose('Current client number: %d', this.clients.size); - } - }, { - key: '_handleUpdateSubscription', - value: function _handleUpdateSubscription(parseWebsocket, request) { - this._handleUnsubscribe(parseWebsocket, request, false); - this._handleSubscribe(parseWebsocket, request); - } - }, { - key: '_handleUnsubscribe', - value: function _handleUnsubscribe(parseWebsocket, request) { - var notifyClient = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; - - // If we can not find this client, return error to client - if (!parseWebsocket.hasOwnProperty('clientId')) { - _Client.Client.pushError(parseWebsocket, 2, 'Can not find this client, make sure you connect to server before unsubscribing'); - _logger2.default.error('Can not find this client, make sure you connect to server before unsubscribing'); - return; - } - var requestId = request.requestId; - var client = this.clients.get(parseWebsocket.clientId); - if (typeof client === 'undefined') { - _Client.Client.pushError(parseWebsocket, 2, 'Cannot find client with clientId ' + parseWebsocket.clientId + '. Make sure you connect to live query server before unsubscribing.'); - _logger2.default.error('Can not find this client ' + parseWebsocket.clientId); - return; - } - - var subscriptionInfo = client.getSubscriptionInfo(requestId); - if (typeof subscriptionInfo === 'undefined') { - _Client.Client.pushError(parseWebsocket, 2, 'Cannot find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId + '. Make sure you subscribe to live query server before unsubscribing.'); - _logger2.default.error('Can not find subscription with clientId ' + parseWebsocket.clientId + ' subscriptionId ' + requestId); - return; - } - - // Remove subscription from client - client.deleteSubscriptionInfo(requestId); - // Remove client from subscription - var subscription = subscriptionInfo.subscription; - var className = subscription.className; - subscription.deleteClientSubscription(parseWebsocket.clientId, requestId); - // If there is no client which is subscribing this subscription, remove it from subscriptions - var classSubscriptions = this.subscriptions.get(className); - if (!subscription.hasSubscribingClient()) { - classSubscriptions.delete(subscription.hash); - } - // If there is no subscriptions under this class, remove it from subscriptions - if (classSubscriptions.size === 0) { - this.subscriptions.delete(className); - } - - if (!notifyClient) { - return; - } - - client.pushUnsubscribe(request.requestId); - - _logger2.default.verbose('Delete client: %d | subscription: %d', parseWebsocket.clientId, request.requestId); - } - }]); - - return ParseLiveQueryServer; -}(); - -exports.ParseLiveQueryServer = ParseLiveQueryServer; \ No newline at end of file diff --git a/lib/LiveQuery/ParsePubSub.js b/lib/LiveQuery/ParsePubSub.js deleted file mode 100644 index a1a0100160..0000000000 --- a/lib/LiveQuery/ParsePubSub.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.ParsePubSub = undefined; - -var _AdapterLoader = require('../Adapters/AdapterLoader'); - -var _EventEmitterPubSub = require('../Adapters/PubSub/EventEmitterPubSub'); - -var _RedisPubSub = require('../Adapters/PubSub/RedisPubSub'); - -var ParsePubSub = {}; - -function useRedis(config) { - var redisURL = config.redisURL; - return typeof redisURL !== 'undefined' && redisURL !== ''; -} - -ParsePubSub.createPublisher = function (config) { - if (useRedis(config)) { - return _RedisPubSub.RedisPubSub.createPublisher(config); - } else { - var adapter = (0, _AdapterLoader.loadAdapter)(config.pubSubAdapter, _EventEmitterPubSub.EventEmitterPubSub, config); - if (typeof adapter.createPublisher !== 'function') { - throw 'pubSubAdapter should have createPublisher()'; - } - return adapter.createPublisher(config); - } -}; - -ParsePubSub.createSubscriber = function (config) { - if (useRedis(config)) { - return _RedisPubSub.RedisPubSub.createSubscriber(config); - } else { - var adapter = (0, _AdapterLoader.loadAdapter)(config.pubSubAdapter, _EventEmitterPubSub.EventEmitterPubSub, config); - if (typeof adapter.createSubscriber !== 'function') { - throw 'pubSubAdapter should have createSubscriber()'; - } - return adapter.createSubscriber(config); - } -}; - -exports.ParsePubSub = ParsePubSub; \ No newline at end of file diff --git a/lib/LiveQuery/ParseWebSocketServer.js b/lib/LiveQuery/ParseWebSocketServer.js deleted file mode 100644 index 6c6e71b52e..0000000000 --- a/lib/LiveQuery/ParseWebSocketServer.js +++ /dev/null @@ -1,72 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.ParseWebSocket = exports.ParseWebSocketServer = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _logger = require('../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var typeMap = new Map([['disconnect', 'close']]); -var getWS = function getWS() { - try { - return require('uws'); - } catch (e) { - return require('ws'); - } -}; - -var ParseWebSocketServer = exports.ParseWebSocketServer = function ParseWebSocketServer(server, onConnect) { - var websocketTimeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 10 * 1000; - - _classCallCheck(this, ParseWebSocketServer); - - var WebSocketServer = getWS().Server; - var wss = new WebSocketServer({ server: server }); - wss.on('listening', function () { - _logger2.default.info('Parse LiveQuery Server starts running'); - }); - wss.on('connection', function (ws) { - onConnect(new ParseWebSocket(ws)); - // Send ping to client periodically - var pingIntervalId = setInterval(function () { - if (ws.readyState == ws.OPEN) { - ws.ping(); - } else { - clearInterval(pingIntervalId); - } - }, websocketTimeout); - }); - this.server = wss; -}; - -var ParseWebSocket = exports.ParseWebSocket = function () { - function ParseWebSocket(ws) { - _classCallCheck(this, ParseWebSocket); - - this.ws = ws; - } - - _createClass(ParseWebSocket, [{ - key: 'on', - value: function on(type, callback) { - var wsType = typeMap.has(type) ? typeMap.get(type) : type; - this.ws.on(wsType, callback); - } - }, { - key: 'send', - value: function send(message) { - this.ws.send(message); - } - }]); - - return ParseWebSocket; -}(); \ No newline at end of file diff --git a/lib/LiveQuery/QueryTools.js b/lib/LiveQuery/QueryTools.js deleted file mode 100644 index dcaf13b42c..0000000000 --- a/lib/LiveQuery/QueryTools.js +++ /dev/null @@ -1,298 +0,0 @@ -'use strict'; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var equalObjects = require('./equalObjects'); -var Id = require('./Id'); -var Parse = require('parse/node'); - -/** - * Query Hashes are deterministic hashes for Parse Queries. - * Any two queries that have the same set of constraints will produce the same - * hash. This lets us reliably group components by the queries they depend upon, - * and quickly determine if a query has changed. - */ - -/** - * Convert $or queries into an array of where conditions - */ -function flattenOrQueries(where) { - if (!where.hasOwnProperty('$or')) { - return where; - } - var accum = []; - for (var i = 0; i < where.$or.length; i++) { - accum = accum.concat(where.$or[i]); - } - return accum; -} - -/** - * Deterministically turns an object into a string. Disregards ordering - */ -function stringify(object) { - if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) !== 'object' || object === null) { - if (typeof object === 'string') { - return '"' + object.replace(/\|/g, '%|') + '"'; - } - return object + ''; - } - if (Array.isArray(object)) { - var copy = object.map(stringify); - copy.sort(); - return '[' + copy.join(',') + ']'; - } - var sections = []; - var keys = Object.keys(object); - keys.sort(); - for (var k = 0; k < keys.length; k++) { - sections.push(stringify(keys[k]) + ':' + stringify(object[keys[k]])); - } - return '{' + sections.join(',') + '}'; -} - -/** - * Generate a hash from a query, with unique fields for columns, values, order, - * skip, and limit. - */ -function queryHash(query) { - if (query instanceof Parse.Query) { - query = { - className: query.className, - where: query._where - }; - } - var where = flattenOrQueries(query.where || {}); - var columns = []; - var values = []; - var i; - if (Array.isArray(where)) { - var uniqueColumns = {}; - for (i = 0; i < where.length; i++) { - var subValues = {}; - var keys = Object.keys(where[i]); - keys.sort(); - for (var j = 0; j < keys.length; j++) { - subValues[keys[j]] = where[i][keys[j]]; - uniqueColumns[keys[j]] = true; - } - values.push(subValues); - } - columns = Object.keys(uniqueColumns); - columns.sort(); - } else { - columns = Object.keys(where); - columns.sort(); - for (i = 0; i < columns.length; i++) { - values.push(where[columns[i]]); - } - } - - var sections = [columns.join(','), stringify(values)]; - - return query.className + ':' + sections.join('|'); -} - -/** - * matchesQuery -- Determines if an object would be returned by a Parse Query - * It's a lightweight, where-clause only implementation of a full query engine. - * Since we find queries that match objects, rather than objects that match - * queries, we can avoid building a full-blown query tool. - */ -function matchesQuery(object, query) { - if (query instanceof Parse.Query) { - var className = object.id instanceof Id ? object.id.className : object.className; - if (className !== query.className) { - return false; - } - return matchesQuery(object, query._where); - } - for (var field in query) { - if (!matchesKeyConstraints(object, field, query[field])) { - return false; - } - } - return true; -} - -function equalObjectsGeneric(obj, compareTo, eqlFn) { - if (Array.isArray(obj)) { - for (var i = 0; i < obj.length; i++) { - if (eqlFn(obj[i], compareTo)) { - return true; - } - } - return false; - } - - return eqlFn(obj, compareTo); -} - -/** - * Determines whether an object matches a single key's constraints - */ -function matchesKeyConstraints(object, key, constraints) { - if (constraints === null) { - return false; - } - if (key.indexOf(".") >= 0) { - // Key references a subobject - var keyComponents = key.split("."); - var subObjectKey = keyComponents[0]; - var keyRemainder = keyComponents.slice(1).join("."); - return matchesKeyConstraints(object[subObjectKey] || {}, keyRemainder, constraints); - } - var i; - if (key === '$or') { - for (i = 0; i < constraints.length; i++) { - if (matchesQuery(object, constraints[i])) { - return true; - } - } - return false; - } - if (key === '$relatedTo') { - // Bail! We can't handle relational queries locally - return false; - } - // Equality (or Array contains) cases - if ((typeof constraints === 'undefined' ? 'undefined' : _typeof(constraints)) !== 'object') { - if (Array.isArray(object[key])) { - return object[key].indexOf(constraints) > -1; - } - return object[key] === constraints; - } - var compareTo; - if (constraints.__type) { - if (constraints.__type === 'Pointer') { - return equalObjectsGeneric(object[key], constraints, function (obj, ptr) { - return typeof obj !== 'undefined' && ptr.className === obj.className && ptr.objectId === obj.objectId; - }); - } - - return equalObjectsGeneric(object[key], Parse._decode(key, constraints), equalObjects); - } - // More complex cases - for (var condition in constraints) { - compareTo = constraints[condition]; - if (compareTo.__type) { - 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 (compareTo.indexOf(object[key]) < 0) { - return false; - } - break; - case '$nin': - if (compareTo.indexOf(object[key]) > -1) { - return false; - } - break; - case '$all': - for (i = 0; i < compareTo.length; i++) { - if (object[key].indexOf(compareTo[i]) < 0) { - return false; - } - } - break; - case '$exists': - { - var propertyExists = typeof object[key] !== 'undefined'; - var 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 === 'undefined' ? 'undefined' : _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': - var distance = compareTo.radiansTo(object[key]); - var max = constraints.$maxDistance || Infinity; - return distance <= max; - case '$within': - 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; - } - } - return true; -} - -var QueryTools = { - queryHash: queryHash, - matchesQuery: matchesQuery -}; - -module.exports = QueryTools; \ No newline at end of file diff --git a/lib/LiveQuery/RequestSchema.js b/lib/LiveQuery/RequestSchema.js deleted file mode 100644 index c1cf04a54a..0000000000 --- a/lib/LiveQuery/RequestSchema.js +++ /dev/null @@ -1,145 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -var general = { - 'title': 'General request schema', - 'type': 'object', - 'properties': { - 'op': { - 'type': 'string', - 'enum': ['connect', 'subscribe', 'unsubscribe', 'update'] - } - } -}; - -var connect = { - 'title': 'Connect operation schema', - 'type': 'object', - 'properties': { - 'op': 'connect', - 'applicationId': { - 'type': 'string' - }, - 'javascriptKey': { - type: 'string' - }, - 'masterKey': { - type: 'string' - }, - 'clientKey': { - type: 'string' - }, - 'windowsKey': { - type: 'string' - }, - 'restAPIKey': { - 'type': 'string' - }, - 'sessionToken': { - 'type': 'string' - } - }, - 'required': ['op', 'applicationId'], - "additionalProperties": false -}; - -var subscribe = { - 'title': 'Subscribe operation schema', - 'type': 'object', - 'properties': { - 'op': 'subscribe', - 'requestId': { - 'type': 'number' - }, - 'query': { - 'title': 'Query field schema', - 'type': 'object', - 'properties': { - 'className': { - 'type': 'string' - }, - 'where': { - 'type': 'object' - }, - 'fields': { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "uniqueItems": true - } - }, - 'required': ['where', 'className'], - 'additionalProperties': false - }, - 'sessionToken': { - 'type': 'string' - } - }, - 'required': ['op', 'requestId', 'query'], - 'additionalProperties': false -}; - -var update = { - 'title': 'Update operation schema', - 'type': 'object', - 'properties': { - 'op': 'update', - 'requestId': { - 'type': 'number' - }, - 'query': { - 'title': 'Query field schema', - 'type': 'object', - 'properties': { - 'className': { - 'type': 'string' - }, - 'where': { - 'type': 'object' - }, - 'fields': { - "type": "array", - "items": { - "type": "string" - }, - "minItems": 1, - "uniqueItems": true - } - }, - 'required': ['where', 'className'], - 'additionalProperties': false - }, - 'sessionToken': { - 'type': 'string' - } - }, - 'required': ['op', 'requestId', 'query'], - 'additionalProperties': false -}; - -var unsubscribe = { - 'title': 'Unsubscribe operation schema', - 'type': 'object', - 'properties': { - 'op': 'unsubscribe', - 'requestId': { - 'type': 'number' - } - }, - 'required': ['op', 'requestId'], - "additionalProperties": false -}; - -var RequestSchema = { - 'general': general, - 'connect': connect, - 'subscribe': subscribe, - 'update': update, - 'unsubscribe': unsubscribe -}; - -exports.default = RequestSchema; \ No newline at end of file diff --git a/lib/LiveQuery/SessionTokenCache.js b/lib/LiveQuery/SessionTokenCache.js deleted file mode 100644 index d89c466b9f..0000000000 --- a/lib/LiveQuery/SessionTokenCache.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.SessionTokenCache = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _lruCache = require('lru-cache'); - -var _lruCache2 = _interopRequireDefault(_lruCache); - -var _logger = require('../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function userForSessionToken(sessionToken) { - var q = new _node2.default.Query("_Session"); - q.equalTo("sessionToken", sessionToken); - return q.first({ useMasterKey: true }).then(function (session) { - if (!session) { - return _node2.default.Promise.error("No session found for session token"); - } - return session.get("user"); - }); -} - -var SessionTokenCache = function () { - function SessionTokenCache() { - var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 30 * 24 * 60 * 60 * 1000; - var maxSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 10000; - - _classCallCheck(this, SessionTokenCache); - - this.cache = new _lruCache2.default({ - max: maxSize, - maxAge: timeout - }); - } - - _createClass(SessionTokenCache, [{ - key: 'getUserId', - value: function getUserId(sessionToken) { - var _this = this; - - if (!sessionToken) { - return _node2.default.Promise.error('Empty sessionToken'); - } - var userId = this.cache.get(sessionToken); - if (userId) { - _logger2.default.verbose('Fetch userId %s of sessionToken %s from Cache', userId, sessionToken); - return _node2.default.Promise.as(userId); - } - return userForSessionToken(sessionToken).then(function (user) { - _logger2.default.verbose('Fetch userId %s of sessionToken %s from Parse', user.id, sessionToken); - var userId = user.id; - _this.cache.set(sessionToken, userId); - return _node2.default.Promise.as(userId); - }, function (error) { - _logger2.default.error('Can not fetch userId for sessionToken %j, error %j', sessionToken, error); - return _node2.default.Promise.error(error); - }); - } - }]); - - return SessionTokenCache; -}(); - -exports.SessionTokenCache = SessionTokenCache; \ No newline at end of file diff --git a/lib/LiveQuery/Subscription.js b/lib/LiveQuery/Subscription.js deleted file mode 100644 index 55eaa77e33..0000000000 --- a/lib/LiveQuery/Subscription.js +++ /dev/null @@ -1,68 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Subscription = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _logger = require('../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var Subscription = function () { - // It is query condition eg query.where - function Subscription(className, query, queryHash) { - _classCallCheck(this, Subscription); - - this.className = className; - this.query = query; - this.hash = queryHash; - this.clientRequestIds = new Map(); - } - - _createClass(Subscription, [{ - key: 'addClientSubscription', - value: function addClientSubscription(clientId, requestId) { - if (!this.clientRequestIds.has(clientId)) { - this.clientRequestIds.set(clientId, []); - } - var requestIds = this.clientRequestIds.get(clientId); - requestIds.push(requestId); - } - }, { - key: 'deleteClientSubscription', - value: function deleteClientSubscription(clientId, requestId) { - var requestIds = this.clientRequestIds.get(clientId); - if (typeof requestIds === 'undefined') { - _logger2.default.error('Can not find client %d to delete', clientId); - return; - } - - var index = requestIds.indexOf(requestId); - if (index < 0) { - _logger2.default.error('Can not find client %d subscription %d to delete', clientId, requestId); - return; - } - requestIds.splice(index, 1); - // Delete client reference if it has no subscription - if (requestIds.length == 0) { - this.clientRequestIds.delete(clientId); - } - } - }, { - key: 'hasSubscribingClient', - value: function hasSubscribingClient() { - return this.clientRequestIds.size > 0; - } - }]); - - return Subscription; -}(); - -exports.Subscription = Subscription; \ No newline at end of file diff --git a/lib/LiveQuery/equalObjects.js b/lib/LiveQuery/equalObjects.js deleted file mode 100644 index 0e6d38fd69..0000000000 --- a/lib/LiveQuery/equalObjects.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var toString = Object.prototype.toString; - -/** - * Determines whether two objects represent the same primitive, special Parse - * type, or full Parse Object. - */ -function equalObjects(a, b) { - if ((typeof a === 'undefined' ? 'undefined' : _typeof(a)) !== (typeof b === 'undefined' ? 'undefined' : _typeof(b))) { - return false; - } - if ((typeof a === 'undefined' ? 'undefined' : _typeof(a)) !== 'object') { - return a === b; - } - if (a === b) { - return true; - } - if (toString.call(a) === '[object Date]') { - if (toString.call(b) === '[object Date]') { - return +a === +b; - } - return false; - } - if (Array.isArray(a)) { - if (Array.isArray(b)) { - if (a.length !== b.length) { - return false; - } - for (var i = 0; i < a.length; i++) { - if (!equalObjects(a[i], b[i])) { - return false; - } - } - return true; - } - return false; - } - if (Object.keys(a).length !== Object.keys(b).length) { - return false; - } - for (var key in a) { - if (!equalObjects(a[key], b[key])) { - return false; - } - } - return true; -} - -module.exports = equalObjects; \ No newline at end of file diff --git a/lib/ParseMessageQueue.js b/lib/ParseMessageQueue.js deleted file mode 100644 index 91d9728147..0000000000 --- a/lib/ParseMessageQueue.js +++ /dev/null @@ -1,30 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.ParseMessageQueue = undefined; - -var _AdapterLoader = require('./Adapters/AdapterLoader'); - -var _EventEmitterMQ = require('./Adapters/MessageQueue/EventEmitterMQ'); - -var ParseMessageQueue = {}; - -ParseMessageQueue.createPublisher = function (config) { - var adapter = (0, _AdapterLoader.loadAdapter)(config.messageQueueAdapter, _EventEmitterMQ.EventEmitterMQ, config); - if (typeof adapter.createPublisher !== 'function') { - throw 'pubSubAdapter should have createPublisher()'; - } - return adapter.createPublisher(config); -}; - -ParseMessageQueue.createSubscriber = function (config) { - var adapter = (0, _AdapterLoader.loadAdapter)(config.messageQueueAdapter, _EventEmitterMQ.EventEmitterMQ, config); - if (typeof adapter.createSubscriber !== 'function') { - throw 'messageQueueAdapter should have createSubscriber()'; - } - return adapter.createSubscriber(config); -}; - -exports.ParseMessageQueue = ParseMessageQueue; \ No newline at end of file diff --git a/lib/ParseServer.js b/lib/ParseServer.js deleted file mode 100644 index 5e37b09300..0000000000 --- a/lib/ParseServer.js +++ /dev/null @@ -1,514 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _defaults = require('./defaults'); - -var _defaults2 = _interopRequireDefault(_defaults); - -var _logger = require('./logger'); - -var logging = _interopRequireWildcard(_logger); - -var _cache = require('./cache'); - -var _cache2 = _interopRequireDefault(_cache); - -var _Config = require('./Config'); - -var _Config2 = _interopRequireDefault(_Config); - -var _PromiseRouter = require('./PromiseRouter'); - -var _PromiseRouter2 = _interopRequireDefault(_PromiseRouter); - -var _requiredParameter = require('./requiredParameter'); - -var _requiredParameter2 = _interopRequireDefault(_requiredParameter); - -var _AnalyticsRouter = require('./Routers/AnalyticsRouter'); - -var _ClassesRouter = require('./Routers/ClassesRouter'); - -var _FeaturesRouter = require('./Routers/FeaturesRouter'); - -var _InMemoryCacheAdapter = require('./Adapters/Cache/InMemoryCacheAdapter'); - -var _AnalyticsController = require('./Controllers/AnalyticsController'); - -var _CacheController = require('./Controllers/CacheController'); - -var _AnalyticsAdapter = require('./Adapters/Analytics/AnalyticsAdapter'); - -var _WinstonLoggerAdapter = require('./Adapters/Logger/WinstonLoggerAdapter'); - -var _FilesController = require('./Controllers/FilesController'); - -var _FilesRouter = require('./Routers/FilesRouter'); - -var _FunctionsRouter = require('./Routers/FunctionsRouter'); - -var _GlobalConfigRouter = require('./Routers/GlobalConfigRouter'); - -var _GridStoreAdapter = require('./Adapters/Files/GridStoreAdapter'); - -var _HooksController = require('./Controllers/HooksController'); - -var _HooksRouter = require('./Routers/HooksRouter'); - -var _IAPValidationRouter = require('./Routers/IAPValidationRouter'); - -var _InstallationsRouter = require('./Routers/InstallationsRouter'); - -var _AdapterLoader = require('./Adapters/AdapterLoader'); - -var _LiveQueryController = require('./Controllers/LiveQueryController'); - -var _LoggerController = require('./Controllers/LoggerController'); - -var _LogsRouter = require('./Routers/LogsRouter'); - -var _ParseLiveQueryServer = require('./LiveQuery/ParseLiveQueryServer'); - -var _PublicAPIRouter = require('./Routers/PublicAPIRouter'); - -var _PushController = require('./Controllers/PushController'); - -var _PushQueue = require('./Push/PushQueue'); - -var _PushWorker = require('./Push/PushWorker'); - -var _PushRouter = require('./Routers/PushRouter'); - -var _CloudCodeRouter = require('./Routers/CloudCodeRouter'); - -var _RolesRouter = require('./Routers/RolesRouter'); - -var _SchemasRouter = require('./Routers/SchemasRouter'); - -var _SessionsRouter = require('./Routers/SessionsRouter'); - -var _UserController = require('./Controllers/UserController'); - -var _UsersRouter = require('./Routers/UsersRouter'); - -var _PurgeRouter = require('./Routers/PurgeRouter'); - -var _DatabaseController = require('./Controllers/DatabaseController'); - -var _DatabaseController2 = _interopRequireDefault(_DatabaseController); - -var _SchemaCache = require('./Controllers/SchemaCache'); - -var _SchemaCache2 = _interopRequireDefault(_SchemaCache); - -var _parseServerPushAdapter = require('parse-server-push-adapter'); - -var _parseServerPushAdapter2 = _interopRequireDefault(_parseServerPushAdapter); - -var _MongoStorageAdapter = require('./Adapters/Storage/Mongo/MongoStorageAdapter'); - -var _MongoStorageAdapter2 = _interopRequireDefault(_MongoStorageAdapter); - -var _PostgresStorageAdapter = require('./Adapters/Storage/Postgres/PostgresStorageAdapter'); - -var _PostgresStorageAdapter2 = _interopRequireDefault(_PostgresStorageAdapter); - -var _ParseServerRESTController = require('./ParseServerRESTController'); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -// ParseServer - open-source compatible API Server for Parse apps - -var batch = require('./batch'), - bodyParser = require('body-parser'), - express = require('express'), - middlewares = require('./middlewares'), - Parse = require('parse/node').Parse, - path = require('path'), - url = require('url'), - authDataManager = require('./Adapters/Auth'); - -// Mutate the Parse object to add the Cloud Code handlers -addParseCloud(); - -// ParseServer works like a constructor of an express app. -// The args that we understand are: -// "analyticsAdapter": an adapter class for analytics -// "filesAdapter": a class like GridStoreAdapter providing create, get, -// and delete -// "loggerAdapter": a class like WinstonLoggerAdapter providing info, error, -// and query -// "jsonLogs": log as structured JSON objects -// "databaseURI": a uri like mongodb://localhost:27017/dbname to tell us -// what database this Parse API connects to. -// "cloud": relative location to cloud code to require, or a function -// that is given an instance of Parse as a parameter. Use this instance of Parse -// to register your cloud code hooks and functions. -// "appId": the application id to host -// "masterKey": the master key for requests to this app -// "collectionPrefix": optional prefix for database collection names -// "fileKey": optional key from Parse dashboard for supporting older files -// hosted by Parse -// "clientKey": optional key from Parse dashboard -// "dotNetKey": optional key from Parse dashboard -// "restAPIKey": optional key from Parse dashboard -// "webhookKey": optional key from Parse dashboard -// "javascriptKey": optional key from Parse dashboard -// "push": optional key from configure push -// "sessionLength": optional length in seconds for how long Sessions should be valid for - -var ParseServer = function () { - function ParseServer(_ref) { - var _ref$appId = _ref.appId, - appId = _ref$appId === undefined ? (0, _requiredParameter2.default)('You must provide an appId!') : _ref$appId, - _ref$masterKey = _ref.masterKey, - masterKey = _ref$masterKey === undefined ? (0, _requiredParameter2.default)('You must provide a masterKey!') : _ref$masterKey, - appName = _ref.appName, - analyticsAdapter = _ref.analyticsAdapter, - filesAdapter = _ref.filesAdapter, - push = _ref.push, - _ref$scheduledPush = _ref.scheduledPush, - scheduledPush = _ref$scheduledPush === undefined ? false : _ref$scheduledPush, - loggerAdapter = _ref.loggerAdapter, - _ref$jsonLogs = _ref.jsonLogs, - jsonLogs = _ref$jsonLogs === undefined ? _defaults2.default.jsonLogs : _ref$jsonLogs, - _ref$logsFolder = _ref.logsFolder, - logsFolder = _ref$logsFolder === undefined ? _defaults2.default.logsFolder : _ref$logsFolder, - _ref$verbose = _ref.verbose, - verbose = _ref$verbose === undefined ? _defaults2.default.verbose : _ref$verbose, - _ref$logLevel = _ref.logLevel, - logLevel = _ref$logLevel === undefined ? _defaults2.default.level : _ref$logLevel, - _ref$silent = _ref.silent, - silent = _ref$silent === undefined ? _defaults2.default.silent : _ref$silent, - _ref$databaseURI = _ref.databaseURI, - databaseURI = _ref$databaseURI === undefined ? _defaults2.default.DefaultMongoURI : _ref$databaseURI, - databaseOptions = _ref.databaseOptions, - databaseAdapter = _ref.databaseAdapter, - cloud = _ref.cloud, - _ref$collectionPrefix = _ref.collectionPrefix, - collectionPrefix = _ref$collectionPrefix === undefined ? '' : _ref$collectionPrefix, - clientKey = _ref.clientKey, - javascriptKey = _ref.javascriptKey, - dotNetKey = _ref.dotNetKey, - restAPIKey = _ref.restAPIKey, - webhookKey = _ref.webhookKey, - fileKey = _ref.fileKey, - _ref$userSensitiveFie = _ref.userSensitiveFields, - userSensitiveFields = _ref$userSensitiveFie === undefined ? [] : _ref$userSensitiveFie, - _ref$enableAnonymousU = _ref.enableAnonymousUsers, - enableAnonymousUsers = _ref$enableAnonymousU === undefined ? _defaults2.default.enableAnonymousUsers : _ref$enableAnonymousU, - _ref$allowClientClass = _ref.allowClientClassCreation, - allowClientClassCreation = _ref$allowClientClass === undefined ? _defaults2.default.allowClientClassCreation : _ref$allowClientClass, - _ref$oauth = _ref.oauth, - oauth = _ref$oauth === undefined ? {} : _ref$oauth, - _ref$auth = _ref.auth, - auth = _ref$auth === undefined ? {} : _ref$auth, - _ref$serverURL = _ref.serverURL, - serverURL = _ref$serverURL === undefined ? (0, _requiredParameter2.default)('You must provide a serverURL!') : _ref$serverURL, - _ref$maxUploadSize = _ref.maxUploadSize, - maxUploadSize = _ref$maxUploadSize === undefined ? _defaults2.default.maxUploadSize : _ref$maxUploadSize, - _ref$verifyUserEmails = _ref.verifyUserEmails, - verifyUserEmails = _ref$verifyUserEmails === undefined ? _defaults2.default.verifyUserEmails : _ref$verifyUserEmails, - _ref$preventLoginWith = _ref.preventLoginWithUnverifiedEmail, - preventLoginWithUnverifiedEmail = _ref$preventLoginWith === undefined ? _defaults2.default.preventLoginWithUnverifiedEmail : _ref$preventLoginWith, - emailVerifyTokenValidityDuration = _ref.emailVerifyTokenValidityDuration, - accountLockout = _ref.accountLockout, - passwordPolicy = _ref.passwordPolicy, - cacheAdapter = _ref.cacheAdapter, - emailAdapter = _ref.emailAdapter, - publicServerURL = _ref.publicServerURL, - _ref$customPages = _ref.customPages, - customPages = _ref$customPages === undefined ? { - invalidLink: undefined, - verifyEmailSuccess: undefined, - choosePassword: undefined, - passwordResetSuccess: undefined - } : _ref$customPages, - _ref$liveQuery = _ref.liveQuery, - liveQuery = _ref$liveQuery === undefined ? {} : _ref$liveQuery, - _ref$sessionLength = _ref.sessionLength, - sessionLength = _ref$sessionLength === undefined ? _defaults2.default.sessionLength : _ref$sessionLength, - _ref$expireInactiveSe = _ref.expireInactiveSessions, - expireInactiveSessions = _ref$expireInactiveSe === undefined ? _defaults2.default.expireInactiveSessions : _ref$expireInactiveSe, - _ref$revokeSessionOnP = _ref.revokeSessionOnPasswordReset, - revokeSessionOnPasswordReset = _ref$revokeSessionOnP === undefined ? _defaults2.default.revokeSessionOnPasswordReset : _ref$revokeSessionOnP, - _ref$schemaCacheTTL = _ref.schemaCacheTTL, - schemaCacheTTL = _ref$schemaCacheTTL === undefined ? _defaults2.default.schemaCacheTTL : _ref$schemaCacheTTL, - _ref$enableSingleSche = _ref.enableSingleSchemaCache, - enableSingleSchemaCache = _ref$enableSingleSche === undefined ? false : _ref$enableSingleSche, - _ref$__indexBuildComp = _ref.__indexBuildCompletionCallbackForTests, - __indexBuildCompletionCallbackForTests = _ref$__indexBuildComp === undefined ? function () {} : _ref$__indexBuildComp; - - _classCallCheck(this, ParseServer); - - // Initialize the node client SDK automatically - Parse.initialize(appId, javascriptKey || 'unused', masterKey); - Parse.serverURL = serverURL; - if ((databaseOptions || databaseURI && databaseURI != _defaults2.default.DefaultMongoURI || collectionPrefix !== '') && databaseAdapter) { - throw 'You cannot specify both a databaseAdapter and a databaseURI/databaseOptions/collectionPrefix.'; - } else if (!databaseAdapter) { - databaseAdapter = this.getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions); - } else { - databaseAdapter = (0, _AdapterLoader.loadAdapter)(databaseAdapter); - } - - if (!filesAdapter && !databaseURI) { - throw 'When using an explicit database adapter, you must also use an explicit filesAdapter.'; - } - - userSensitiveFields = Array.from(new Set(userSensitiveFields.concat(_defaults2.default.userSensitiveFields, userSensitiveFields))); - - var loggerControllerAdapter = (0, _AdapterLoader.loadAdapter)(loggerAdapter, _WinstonLoggerAdapter.WinstonLoggerAdapter, { jsonLogs: jsonLogs, logsFolder: logsFolder, verbose: verbose, logLevel: logLevel, silent: silent }); - var loggerController = new _LoggerController.LoggerController(loggerControllerAdapter, appId); - logging.setLogger(loggerController); - - var filesControllerAdapter = (0, _AdapterLoader.loadAdapter)(filesAdapter, function () { - return new _GridStoreAdapter.GridStoreAdapter(databaseURI); - }); - var filesController = new _FilesController.FilesController(filesControllerAdapter, appId); - - var pushOptions = Object.assign({}, push); - var pushQueueOptions = pushOptions.queueOptions || {}; - if (pushOptions.queueOptions) { - delete pushOptions.queueOptions; - } - // Pass the push options too as it works with the default - var pushAdapter = (0, _AdapterLoader.loadAdapter)(pushOptions && pushOptions.adapter, _parseServerPushAdapter2.default, pushOptions); - // We pass the options and the base class for the adatper, - // Note that passing an instance would work too - var pushController = new _PushController.PushController(); - - var hasPushSupport = pushAdapter && push; - var hasPushScheduledSupport = pushAdapter && push && scheduledPush; - - var disablePushWorker = pushQueueOptions.disablePushWorker; - - - var pushControllerQueue = new _PushQueue.PushQueue(pushQueueOptions); - var pushWorker = void 0; - if (!disablePushWorker) { - pushWorker = new _PushWorker.PushWorker(pushAdapter, pushQueueOptions); - } - - var emailControllerAdapter = (0, _AdapterLoader.loadAdapter)(emailAdapter); - var userController = new _UserController.UserController(emailControllerAdapter, appId, { verifyUserEmails: verifyUserEmails }); - - var cacheControllerAdapter = (0, _AdapterLoader.loadAdapter)(cacheAdapter, _InMemoryCacheAdapter.InMemoryCacheAdapter, { appId: appId }); - var cacheController = new _CacheController.CacheController(cacheControllerAdapter, appId); - - var analyticsControllerAdapter = (0, _AdapterLoader.loadAdapter)(analyticsAdapter, _AnalyticsAdapter.AnalyticsAdapter); - var analyticsController = new _AnalyticsController.AnalyticsController(analyticsControllerAdapter); - - var liveQueryController = new _LiveQueryController.LiveQueryController(liveQuery); - var databaseController = new _DatabaseController2.default(databaseAdapter, new _SchemaCache2.default(cacheController, schemaCacheTTL, enableSingleSchemaCache)); - var hooksController = new _HooksController.HooksController(appId, databaseController, webhookKey); - - var dbInitPromise = databaseController.performInitialization(); - - if (Object.keys(oauth).length > 0) { - /* eslint-disable no-console */ - console.warn('oauth option is deprecated and will be removed in a future release, please use auth option instead'); - if (Object.keys(auth).length > 0) { - console.warn('You should use only the auth option.'); - } - /* eslint-enable */ - } - - auth = Object.assign({}, oauth, auth); - - _cache2.default.put(appId, { - appId: appId, - masterKey: masterKey, - serverURL: serverURL, - collectionPrefix: collectionPrefix, - clientKey: clientKey, - javascriptKey: javascriptKey, - dotNetKey: dotNetKey, - restAPIKey: restAPIKey, - webhookKey: webhookKey, - fileKey: fileKey, - analyticsController: analyticsController, - cacheController: cacheController, - filesController: filesController, - pushController: pushController, - loggerController: loggerController, - hooksController: hooksController, - userController: userController, - verifyUserEmails: verifyUserEmails, - preventLoginWithUnverifiedEmail: preventLoginWithUnverifiedEmail, - emailVerifyTokenValidityDuration: emailVerifyTokenValidityDuration, - accountLockout: accountLockout, - passwordPolicy: passwordPolicy, - allowClientClassCreation: allowClientClassCreation, - authDataManager: authDataManager(auth, enableAnonymousUsers), - appName: appName, - publicServerURL: publicServerURL, - customPages: customPages, - maxUploadSize: maxUploadSize, - liveQueryController: liveQueryController, - sessionLength: Number(sessionLength), - expireInactiveSessions: expireInactiveSessions, - jsonLogs: jsonLogs, - revokeSessionOnPasswordReset: revokeSessionOnPasswordReset, - databaseController: databaseController, - schemaCacheTTL: schemaCacheTTL, - enableSingleSchemaCache: enableSingleSchemaCache, - userSensitiveFields: userSensitiveFields, - pushWorker: pushWorker, - pushControllerQueue: pushControllerQueue, - hasPushSupport: hasPushSupport, - hasPushScheduledSupport: hasPushScheduledSupport - }); - - _Config2.default.validate(_cache2.default.get(appId)); - this.config = _cache2.default.get(appId); - _Config2.default.setupPasswordValidator(this.config.passwordPolicy); - hooksController.load(); - - // Note: Tests will start to fail if any validation happens after this is called. - if (process.env.TESTING) { - __indexBuildCompletionCallbackForTests(dbInitPromise); - } - - if (cloud) { - addParseCloud(); - if (typeof cloud === 'function') { - cloud(Parse); - } else if (typeof cloud === 'string') { - require(path.resolve(process.cwd(), cloud)); - } else { - throw "argument 'cloud' must either be a string or a function"; - } - } - } - - _createClass(ParseServer, [{ - key: 'getDatabaseAdapter', - value: function getDatabaseAdapter(databaseURI, collectionPrefix, databaseOptions) { - var protocol = void 0; - try { - var parsedURI = url.parse(databaseURI); - protocol = parsedURI.protocol ? parsedURI.protocol.toLowerCase() : null; - } catch (e) {/* */} - switch (protocol) { - case 'postgres:': - return new _PostgresStorageAdapter2.default({ - uri: databaseURI, - collectionPrefix: collectionPrefix, - databaseOptions: databaseOptions - }); - default: - return new _MongoStorageAdapter2.default({ - uri: databaseURI, - collectionPrefix: collectionPrefix, - mongoOptions: databaseOptions - }); - } - } - }, { - key: 'handleShutdown', - value: function handleShutdown() { - var adapter = this.config.databaseController.adapter; - - if (adapter && typeof adapter.handleShutdown === 'function') { - adapter.handleShutdown(); - } - } - }, { - key: 'app', - get: function get() { - return ParseServer.app(this.config); - } - }], [{ - key: 'app', - value: function app(_ref2) { - var _ref2$maxUploadSize = _ref2.maxUploadSize, - maxUploadSize = _ref2$maxUploadSize === undefined ? '20mb' : _ref2$maxUploadSize, - appId = _ref2.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.FilesRouter().expressRouter({ - maxUploadSize: maxUploadSize - })); - - api.use('/health', function (req, res) { - return res.sendStatus(200); - }); - - api.use('/', bodyParser.urlencoded({ extended: false }), new _PublicAPIRouter.PublicAPIRouter().expressRouter()); - - api.use(bodyParser.json({ 'type': '*/*', limit: maxUploadSize })); - api.use(middlewares.allowCrossDomain); - api.use(middlewares.allowMethodOverride); - api.use(middlewares.handleParseHeaders); - - var appRouter = ParseServer.promiseRouter({ appId: appId }); - api.use(appRouter.expressRouter()); - - api.use(middlewares.handleParseErrors); - - //This causes tests to spew some useless warnings, so disable in test - if (!process.env.TESTING) { - process.on('uncaughtException', function (err) { - if (err.code === "EADDRINUSE") { - // user-friendly message for this common error - /* eslint-disable no-console */ - console.error('Unable to listen on port ' + err.port + '. The port is already in use.'); - /* eslint-enable no-console */ - process.exit(0); - } else { - throw err; - } - }); - } - if (process.env.PARSE_SERVER_ENABLE_EXPERIMENTAL_DIRECT_ACCESS === '1') { - Parse.CoreManager.setRESTController((0, _ParseServerRESTController.ParseServerRESTController)(appId, appRouter)); - } - return api; - } - }, { - key: 'promiseRouter', - value: function promiseRouter(_ref3) { - var appId = _ref3.appId; - - var routers = [new _ClassesRouter.ClassesRouter(), new _UsersRouter.UsersRouter(), new _SessionsRouter.SessionsRouter(), new _RolesRouter.RolesRouter(), new _AnalyticsRouter.AnalyticsRouter(), new _InstallationsRouter.InstallationsRouter(), new _FunctionsRouter.FunctionsRouter(), new _SchemasRouter.SchemasRouter(), new _PushRouter.PushRouter(), new _LogsRouter.LogsRouter(), new _IAPValidationRouter.IAPValidationRouter(), new _FeaturesRouter.FeaturesRouter(), new _GlobalConfigRouter.GlobalConfigRouter(), new _PurgeRouter.PurgeRouter(), new _HooksRouter.HooksRouter(), new _CloudCodeRouter.CloudCodeRouter()]; - - var routes = routers.reduce(function (memo, router) { - return memo.concat(router.routes); - }, []); - - var appRouter = new _PromiseRouter2.default(routes, appId); - - batch.mountOnto(appRouter); - return appRouter; - } - }, { - key: 'createLiveQueryServer', - value: function createLiveQueryServer(httpServer, config) { - return new _ParseLiveQueryServer.ParseLiveQueryServer(httpServer, config); - } - }]); - - return ParseServer; -}(); - -function addParseCloud() { - var ParseCloud = require("./cloud-code/Parse.Cloud"); - Object.assign(Parse.Cloud, ParseCloud); - global.Parse = Parse; -} - -exports.default = ParseServer; \ No newline at end of file diff --git a/lib/ParseServerRESTController.js b/lib/ParseServerRESTController.js deleted file mode 100644 index f39caff341..0000000000 --- a/lib/ParseServerRESTController.js +++ /dev/null @@ -1,109 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -var Config = require('./Config'); -var Auth = require('./Auth'); -var RESTController = require('parse/lib/node/RESTController'); -var URL = require('url'); -var Parse = require('parse/node'); - -function getSessionToken(options) { - if (options && typeof options.sessionToken === 'string') { - return Parse.Promise.as(options.sessionToken); - } - return Parse.Promise.as(null); -} - -function getAuth() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - var config = arguments[1]; - - var installationId = options.installationId || 'cloud'; - if (options.useMasterKey) { - return Parse.Promise.as(new Auth.Auth({ config: config, isMaster: true, installationId: installationId })); - } - return getSessionToken(options).then(function (sessionToken) { - if (sessionToken) { - options.sessionToken = sessionToken; - return Auth.getAuthForSessionToken({ - config: config, - sessionToken: sessionToken, - installationId: installationId - }); - } else { - return Parse.Promise.as(new Auth.Auth({ config: config, installationId: installationId })); - } - }); -} - -function ParseServerRESTController(applicationId, router) { - function handleRequest(method, path) { - var data = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; - - // Store the arguments, for later use if internal fails - var args = arguments; - - var config = new Config(applicationId); - var serverURL = URL.parse(config.serverURL); - if (path.indexOf(serverURL.path) === 0) { - path = path.slice(serverURL.path.length, path.length); - } - - if (path[0] !== "/") { - path = "/" + path; - } - - if (path === '/batch') { - var promises = data.requests.map(function (request) { - return handleRequest(request.method, request.path, request.body, options).then(function (response) { - return Parse.Promise.as({ success: response }); - }, function (error) { - return Parse.Promise.as({ error: { code: error.code, error: error.message } }); - }); - }); - return Parse.Promise.all(promises); - } - - var query = void 0; - if (method === 'GET') { - query = data; - } - - return new Parse.Promise(function (resolve, reject) { - getAuth(options, config).then(function (auth) { - var request = { - body: data, - config: config, - auth: auth, - info: { - applicationId: applicationId, - sessionToken: options.sessionToken - }, - query: query - }; - return Promise.resolve().then(function () { - return router.tryRouteRequest(method, path, request); - }).then(function (response) { - resolve(response.response, response.status, response); - }, function (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 { - request: handleRequest, - ajax: RESTController.ajax - }; -} - -exports.default = ParseServerRESTController; -exports.ParseServerRESTController = ParseServerRESTController; \ No newline at end of file diff --git a/lib/PromiseRouter.js b/lib/PromiseRouter.js deleted file mode 100644 index 1d1de1893f..0000000000 --- a/lib/PromiseRouter.js +++ /dev/null @@ -1,302 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // A router that is based on promises rather than req/res/next. -// This is intended to replace the use of express.Router to handle -// subsections of the API surface. -// This will make it easier to have methods like 'batch' that -// themselves use our routing information, without disturbing express -// components that external developers may be modifying. - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _express = require('express'); - -var _express2 = _interopRequireDefault(_express); - -var _logger = require('./logger'); - -var _logger2 = _interopRequireDefault(_logger); - -var _util = require('util'); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var Layer = require('express/lib/router/layer'); - -function validateParameter(key, value) { - if (key == 'className') { - if (value.match(/_?[A-Za-z][A-Za-z_0-9]*/)) { - return value; - } - } else if (key == 'objectId') { - if (value.match(/[A-Za-z0-9]+/)) { - return value; - } - } else { - return value; - } -} - -var PromiseRouter = function () { - // Each entry should be an object with: - // path: the path to route, in express format - // method: the HTTP method that this route handles. - // Must be one of: POST, GET, PUT, DELETE - // handler: a function that takes request, and returns a promise. - // Successful handlers should resolve to an object with fields: - // status: optional. the http status code. defaults to 200 - // response: a json object with the content of the response - // location: optional. a location header - function PromiseRouter() { - var routes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - var appId = arguments[1]; - - _classCallCheck(this, PromiseRouter); - - this.routes = routes; - this.appId = appId; - this.mountRoutes(); - } - - // Leave the opportunity to - // subclasses to mount their routes by overriding - - - _createClass(PromiseRouter, [{ - key: 'mountRoutes', - value: function mountRoutes() {} - - // Merge the routes into this one - - }, { - key: 'merge', - value: function merge(router) { - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = router.routes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var route = _step.value; - - this.routes.push(route); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - } - }, { - key: 'route', - value: function route(method, path) { - for (var _len = arguments.length, handlers = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { - handlers[_key - 2] = arguments[_key]; - } - - switch (method) { - case 'POST': - case 'GET': - case 'PUT': - case 'DELETE': - break; - default: - throw 'cannot route method: ' + method; - } - - var handler = handlers[0]; - - if (handlers.length > 1) { - handler = function handler(req) { - return handlers.reduce(function (promise, handler) { - return promise.then(function () { - return handler(req); - }); - }, Promise.resolve()); - }; - } - - this.routes.push({ - path: path, - method: method, - handler: handler, - layer: new Layer(path, null, handler) - }); - } - - // Returns an object with: - // handler: the handler that should deal with this request - // params: any :-params that got parsed from the path - // Returns undefined if there is no match. - - }, { - key: 'match', - value: function match(method, path) { - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = this.routes[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var route = _step2.value; - - if (route.method != method) { - continue; - } - var layer = route.layer || new Layer(route.path, null, route.handler); - var match = layer.match(path); - if (match) { - var _ret = function () { - var params = layer.params; - Object.keys(params).forEach(function (key) { - params[key] = validateParameter(key, params[key]); - }); - return { - v: { params: params, handler: route.handler } - }; - }(); - - if ((typeof _ret === 'undefined' ? 'undefined' : _typeof(_ret)) === "object") return _ret.v; - } - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - } - - // Mount the routes on this router onto an express app (or express router) - - }, { - key: 'mountOnto', - value: function mountOnto(expressApp) { - var _this = this; - - this.routes.forEach(function (route) { - var method = route.method.toLowerCase(); - var handler = makeExpressHandler(_this.appId, route.handler); - expressApp[method].call(expressApp, route.path, handler); - }); - return expressApp; - } - }, { - key: 'expressRouter', - value: function expressRouter() { - return this.mountOnto(_express2.default.Router()); - } - }, { - key: 'tryRouteRequest', - value: function tryRouteRequest(method, path, request) { - var match = this.match(method, path); - if (!match) { - throw new _node2.default.Error(_node2.default.Error.INVALID_JSON, 'cannot route ' + method + ' ' + path); - } - request.params = match.params; - return new Promise(function (resolve, reject) { - match.handler(request).then(resolve, reject); - }); - } - }]); - - return PromiseRouter; -}(); - -// A helper function to make an express handler out of a a promise -// handler. -// Express handlers should never throw; if a promise handler throws we -// just treat it like it resolved to an error. - - -exports.default = PromiseRouter; -function makeExpressHandler(appId, promiseHandler) { - return function (req, res, next) { - try { - var url = maskSensitiveUrl(req); - var body = Object.assign({}, req.body); - var stringifiedBody = JSON.stringify(body, null, 2); - _logger2.default.verbose('REQUEST for [' + req.method + '] ' + url + ': ' + stringifiedBody, { - method: req.method, - url: url, - headers: req.headers, - body: body - }); - promiseHandler(req).then(function (result) { - if (!result.response && !result.location && !result.text) { - _logger2.default.error('the handler did not include a "response" or a "location" field'); - throw 'control should not get here'; - } - - var stringifiedResponse = JSON.stringify(result, null, 2); - _logger2.default.verbose('RESPONSE from [' + req.method + '] ' + url + ': ' + stringifiedResponse, { result: 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(function (header) { - res.set(header, result.headers[header]); - }); - } - res.json(result.response); - }, function (e) { - _logger2.default.error('Error generating response. ' + (0, _util.inspect)(e), { error: e }); - next(e); - }); - } catch (e) { - _logger2.default.error('Error handling request: ' + (0, _util.inspect)(e), { error: e }); - next(e); - } - }; -} - -function maskSensitiveUrl(req) { - var maskUrl = req.originalUrl.toString(); - var shouldMaskUrl = req.method === 'GET' && req.originalUrl.includes('/login') && !req.originalUrl.includes('classes'); - if (shouldMaskUrl) { - maskUrl = _logger2.default.maskSensitiveUrl(maskUrl); - } - return maskUrl; -} \ No newline at end of file diff --git a/lib/Push/PushQueue.js b/lib/Push/PushQueue.js deleted file mode 100644 index 286ddd6fed..0000000000 --- a/lib/Push/PushQueue.js +++ /dev/null @@ -1,92 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.PushQueue = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _ParseMessageQueue = require('../ParseMessageQueue'); - -var _rest = require('../rest'); - -var _rest2 = _interopRequireDefault(_rest); - -var _utils = require('./utils'); - -var _deepcopy = require('deepcopy'); - -var _deepcopy2 = _interopRequireDefault(_deepcopy); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var PUSH_CHANNEL = 'parse-server-push'; -var DEFAULT_BATCH_SIZE = 100; - -var PushQueue = exports.PushQueue = function () { - - // config object of the publisher, right now it only contains the redisURL, - // but we may extend it later. - function PushQueue() { - var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - _classCallCheck(this, PushQueue); - - this.channel = config.channel || PUSH_CHANNEL; - this.batchSize = config.batchSize || DEFAULT_BATCH_SIZE; - this.parsePublisher = _ParseMessageQueue.ParseMessageQueue.createPublisher(config); - } - - _createClass(PushQueue, [{ - key: 'enqueue', - value: function enqueue(body, where, config, auth, pushStatus) { - var _this = this; - - var limit = this.batchSize; - // Order by badge (because the payload is badge dependant) - // and createdAt to fix the order - var order = (0, _utils.isPushIncrementing)(body) ? 'badge,createdAt' : 'createdAt'; - where = (0, _deepcopy2.default)(where); - if (!where.hasOwnProperty('deviceToken')) { - where['deviceToken'] = { '$exists': true }; - } - return Promise.resolve().then(function () { - return _rest2.default.find(config, auth, '_Installation', where, { limit: 0, count: true }); - }).then(function (_ref) { - var results = _ref.results, - count = _ref.count; - - if (!results) { - return Promise.reject({ error: 'PushController: no results in query' }); - } - pushStatus.setRunning(count); - var skip = 0; - while (skip < count) { - var query = { where: where, - limit: limit, - skip: skip, - order: order }; - - var pushWorkItem = { - body: body, - query: query, - pushStatus: { objectId: pushStatus.objectId }, - applicationId: config.applicationId - }; - _this.parsePublisher.publish(_this.channel, JSON.stringify(pushWorkItem)); - skip += limit; - } - }); - } - }], [{ - key: 'defaultPushChannel', - value: function defaultPushChannel() { - return PUSH_CHANNEL; - } - }]); - - return PushQueue; -}(); \ No newline at end of file diff --git a/lib/Push/PushWorker.js b/lib/Push/PushWorker.js deleted file mode 100644 index 0e0bc8c090..0000000000 --- a/lib/Push/PushWorker.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.PushWorker = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _deepcopy = require('deepcopy'); - -var _deepcopy2 = _interopRequireDefault(_deepcopy); - -var _AdaptableController = require('../Controllers/AdaptableController'); - -var _AdaptableController2 = _interopRequireDefault(_AdaptableController); - -var _Auth = require('../Auth'); - -var _Config = require('../Config'); - -var _Config2 = _interopRequireDefault(_Config); - -var _PushAdapter = require('../Adapters/Push/PushAdapter'); - -var _rest = require('../rest'); - -var _rest2 = _interopRequireDefault(_rest); - -var _StatusHandler = require('../StatusHandler'); - -var _utils = require('./utils'); - -var _ParseMessageQueue = require('../ParseMessageQueue'); - -var _PushQueue = require('./PushQueue'); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var UNSUPPORTED_BADGE_KEY = "unsupported"; - -function groupByBadge(installations) { - return installations.reduce(function (map, installation) { - var badge = installation.badge + ''; - if (installation.deviceType != "ios") { - badge = UNSUPPORTED_BADGE_KEY; - } - map[badge] = map[badge] || []; - map[badge].push(installation); - return map; - }, {}); -} - -var PushWorker = exports.PushWorker = function () { - function PushWorker(pushAdapter) { - var _this = this; - - var subscriberConfig = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - - _classCallCheck(this, PushWorker); - - _AdaptableController2.default.validateAdapter(pushAdapter, this, _PushAdapter.PushAdapter); - this.adapter = pushAdapter; - - this.channel = subscriberConfig.channel || _PushQueue.PushQueue.defaultPushChannel(); - this.subscriber = _ParseMessageQueue.ParseMessageQueue.createSubscriber(subscriberConfig); - if (this.subscriber) { - var subscriber = this.subscriber; - subscriber.subscribe(this.channel); - subscriber.on('message', function (channel, messageStr) { - var workItem = JSON.parse(messageStr); - _this.run(workItem); - }); - } - } - - _createClass(PushWorker, [{ - key: 'unsubscribe', - value: function unsubscribe() { - if (this.subscriber) { - this.subscriber.unsubscribe(this.channel); - } - } - }, { - key: 'run', - value: function run(_ref) { - var _this2 = this; - - var body = _ref.body, - query = _ref.query, - pushStatus = _ref.pushStatus, - applicationId = _ref.applicationId; - - var config = new _Config2.default(applicationId); - var auth = (0, _Auth.master)(config); - var where = query.where; - delete query.where; - return _rest2.default.find(config, auth, '_Installation', where, query).then(function (_ref2) { - var results = _ref2.results; - - if (results.length == 0) { - return; - } - return _this2.sendToAdapter(body, results, pushStatus, config); - }, function (err) { - throw err; - }); - } - }, { - key: 'sendToAdapter', - value: function sendToAdapter(body, installations, pushStatus, config) { - var _this3 = this; - - pushStatus = (0, _StatusHandler.pushStatusHandler)(config, pushStatus.objectId); - if (!(0, _utils.isPushIncrementing)(body)) { - return this.adapter.send(body, installations, pushStatus.objectId).then(function (results) { - return pushStatus.trackSent(results); - }); - } - - // Collect the badges to reduce the # of calls - var badgeInstallationsMap = groupByBadge(installations); - - // Map the on the badges count and return the send result - var promises = Object.keys(badgeInstallationsMap).map(function (badge) { - var payload = (0, _deepcopy2.default)(body); - if (badge == UNSUPPORTED_BADGE_KEY) { - delete payload.data.badge; - } else { - payload.data.badge = parseInt(badge); - } - var installations = badgeInstallationsMap[badge]; - return _this3.sendToAdapter(payload, installations, pushStatus, config); - }); - return Promise.all(promises); - } - }]); - - return PushWorker; -}(); - -exports.default = PushWorker; \ No newline at end of file diff --git a/lib/Push/utils.js b/lib/Push/utils.js deleted file mode 100644 index ce08e94c13..0000000000 --- a/lib/Push/utils.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.isPushIncrementing = isPushIncrementing; -exports.validatePushType = validatePushType; - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function isPushIncrementing(body) { - return body.data && body.data.badge && typeof body.data.badge == 'string' && body.data.badge.toLowerCase() == "increment"; -} - -/** - * Check whether the deviceType parameter in qury condition is valid or not. - * @param {Object} where A query condition - * @param {Array} validPushTypes An array of valid push types(string) - */ -function validatePushType() { - var where = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - var validPushTypes = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; - - var deviceTypeField = where.deviceType || {}; - var deviceTypes = []; - if (typeof deviceTypeField === 'string') { - deviceTypes.push(deviceTypeField); - } else if (Array.isArray(deviceTypeField['$in'])) { - deviceTypes.concat(deviceTypeField['$in']); - } - for (var i = 0; i < deviceTypes.length; i++) { - var deviceType = deviceTypes[i]; - if (validPushTypes.indexOf(deviceType) < 0) { - throw new _node2.default.Error(_node2.default.Error.PUSH_MISCONFIGURED, deviceType + ' is not supported push type.'); - } - } -} \ No newline at end of file diff --git a/lib/RestQuery.js b/lib/RestQuery.js deleted file mode 100644 index 9489f59c63..0000000000 --- a/lib/RestQuery.js +++ /dev/null @@ -1,989 +0,0 @@ -'use strict'; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -// An object that encapsulates everything we need to run a 'find' -// operation, encoded in the REST API format. - -var SchemaController = require('./Controllers/SchemaController'); -var Parse = require('parse/node').Parse; -var triggers = require('./triggers'); - -var AlwaysSelectedKeys = ['objectId', 'createdAt', 'updatedAt']; -// restOptions can include: -// skip -// limit -// order -// count -// include -// keys -// redirectClassNameForKey -function RestQuery(config, auth, className) { - var restWhere = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; - var restOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; - var clientSDK = arguments[5]; - - - this.config = config; - this.auth = auth; - this.className = className; - this.restWhere = restWhere; - this.restOptions = restOptions; - this.clientSDK = clientSDK; - this.response = null; - this.findOptions = {}; - if (!this.auth.isMaster) { - this.findOptions.acl = this.auth.user ? [this.auth.user.id] : null; - if (this.className == '_Session') { - if (!this.findOptions.acl) { - throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, 'This session token is invalid.'); - } - this.restWhere = { - '$and': [this.restWhere, { - 'user': { - __type: 'Pointer', - className: '_User', - objectId: this.auth.user.id - } - }] - }; - } - } - - this.doCount = false; - - // The format for this.include is not the same as the format for the - // include option - it's the paths we should include, in order, - // stored as arrays, taking into account that we need to include foo - // before including foo.bar. Also it should dedupe. - // For example, passing an arg of include=foo.bar,foo.baz could lead to - // this.include = [['foo'], ['foo', 'baz'], ['foo', 'bar']] - this.include = []; - - // If we have keys, we probably want to force some includes (n-1 level) - // See issue: https://github.com/ParsePlatform/parse-server/issues/3185 - if (restOptions.hasOwnProperty('keys')) { - var keysForInclude = restOptions.keys.split(',').filter(function (key) { - // At least 2 components - return key.split(".").length > 1; - }).map(function (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. - if (keysForInclude.length > 0) { - if (!restOptions.include || restOptions.include.length == 0) { - restOptions.include = keysForInclude; - } else { - restOptions.include += "," + keysForInclude; - } - } - } - - for (var option in restOptions) { - switch (option) { - case 'keys': - { - var keys = restOptions.keys.split(',').concat(AlwaysSelectedKeys); - this.keys = Array.from(new Set(keys)); - break; - } - case 'count': - this.doCount = true; - break; - case 'skip': - case 'limit': - this.findOptions[option] = restOptions[option]; - break; - case 'order': - var fields = restOptions.order.split(','); - this.findOptions.sort = fields.reduce(function (sortMap, field) { - field = field.trim(); - if (field[0] == '-') { - sortMap[field.slice(1)] = -1; - } else { - sortMap[field] = 1; - } - return sortMap; - }, {}); - break; - case 'include': - { - var paths = restOptions.include.split(','); - // Load the existing includes (from keys) - var pathSet = paths.reduce(function (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(function (memo, path, index, parts) { - memo[parts.slice(0, index + 1).join('.')] = true; - return memo; - }, memo); - }, {}); - - this.include = Object.keys(pathSet).map(function (s) { - return s.split('.'); - }).sort(function (a, b) { - return a.length - b.length; // Sort by number of components - }); - break; - } - case 'redirectClassNameForKey': - this.redirectKey = restOptions.redirectClassNameForKey; - this.redirectClassName = null; - break; - default: - throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad option: ' + option); - } - } -} - -// A convenient method to perform all the steps of processing a query -// in order. -// Returns a promise for the response - an object with optional keys -// 'results' and 'count'. -// TODO: consolidate the replaceX functions -RestQuery.prototype.execute = function (executeOptions) { - var _this = this; - - return Promise.resolve().then(function () { - return _this.buildRestWhere(); - }).then(function () { - return _this.runFind(executeOptions); - }).then(function () { - return _this.runCount(); - }).then(function () { - return _this.handleInclude(); - }).then(function () { - return _this.runAfterFindTrigger(); - }).then(function () { - return _this.response; - }); -}; - -RestQuery.prototype.buildRestWhere = function () { - var _this2 = this; - - return Promise.resolve().then(function () { - return _this2.getUserAndRoleACL(); - }).then(function () { - return _this2.redirectClassNameForKey(); - }).then(function () { - return _this2.validateClientClassCreation(); - }).then(function () { - return _this2.replaceSelect(); - }).then(function () { - return _this2.replaceDontSelect(); - }).then(function () { - return _this2.replaceInQuery(); - }).then(function () { - return _this2.replaceNotInQuery(); - }).then(function () { - return _this2.replaceEquality(); - }); -}; - -// Uses the Auth object to get the list of roles, adds the user id -RestQuery.prototype.getUserAndRoleACL = function () { - var _this3 = this; - - if (this.auth.isMaster || !this.auth.user) { - return Promise.resolve(); - } - return this.auth.getUserRoles().then(function (roles) { - // Concat with the roles to prevent duplications on multiple calls - var aclSet = new Set([].concat(_this3.findOptions.acl, roles)); - _this3.findOptions.acl = Array.from(aclSet); - return Promise.resolve(); - }); -}; - -// Changes the className if redirectClassNameForKey is set. -// Returns a promise. -RestQuery.prototype.redirectClassNameForKey = function () { - var _this4 = this; - - if (!this.redirectKey) { - return Promise.resolve(); - } - - // We need to change the class name based on the schema - return this.config.database.redirectClassNameForKey(this.className, this.redirectKey).then(function (newClassName) { - _this4.className = newClassName; - _this4.redirectClassName = newClassName; - }); -}; - -// Validates this operation against the allowClientClassCreation config. -RestQuery.prototype.validateClientClassCreation = function () { - var _this5 = this; - - if (this.config.allowClientClassCreation === false && !this.auth.isMaster && SchemaController.systemClasses.indexOf(this.className) === -1) { - return this.config.database.loadSchema().then(function (schemaController) { - return schemaController.hasClass(_this5.className); - }).then(function (hasClass) { - if (hasClass !== true) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'This user is not allowed to access ' + 'non-existent class: ' + _this5.className); - } - }); - } else { - return Promise.resolve(); - } -}; - -function transformInQuery(inQueryObject, className, results) { - var values = []; - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = results[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var result = _step.value; - - values.push({ - __type: 'Pointer', - className: className, - objectId: result.objectId - }); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - delete inQueryObject['$inQuery']; - if (Array.isArray(inQueryObject['$in'])) { - inQueryObject['$in'] = inQueryObject['$in'].concat(values); - } else { - inQueryObject['$in'] = values; - } -} - -// Replaces a $inQuery clause by running the subquery, if there is an -// $inQuery clause. -// The $inQuery clause turns into an $in with values that are just -// pointers to the objects returned in the subquery. -RestQuery.prototype.replaceInQuery = function () { - var _this6 = this; - - var inQueryObject = findObjectWithKey(this.restWhere, '$inQuery'); - if (!inQueryObject) { - return; - } - - // 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'); - } - - var additionalOptions = { - redirectClassNameForKey: inQueryValue.redirectClassNameForKey - }; - - var subquery = new RestQuery(this.config, this.auth, inQueryValue.className, inQueryValue.where, additionalOptions); - return subquery.execute().then(function (response) { - transformInQuery(inQueryObject, subquery.className, response.results); - // Recurse to repeat - return _this6.replaceInQuery(); - }); -}; - -function transformNotInQuery(notInQueryObject, className, results) { - var values = []; - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = results[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var result = _step2.value; - - values.push({ - __type: 'Pointer', - className: className, - objectId: result.objectId - }); - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - - delete notInQueryObject['$notInQuery']; - if (Array.isArray(notInQueryObject['$nin'])) { - notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values); - } else { - notInQueryObject['$nin'] = values; - } -} - -// Replaces a $notInQuery clause by running the subquery, if there is an -// $notInQuery clause. -// The $notInQuery clause turns into a $nin with values that are just -// pointers to the objects returned in the subquery. -RestQuery.prototype.replaceNotInQuery = function () { - var _this7 = this; - - var notInQueryObject = findObjectWithKey(this.restWhere, '$notInQuery'); - if (!notInQueryObject) { - return; - } - - // 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'); - } - - var additionalOptions = { - redirectClassNameForKey: notInQueryValue.redirectClassNameForKey - }; - - var subquery = new RestQuery(this.config, this.auth, notInQueryValue.className, notInQueryValue.where, additionalOptions); - return subquery.execute().then(function (response) { - transformNotInQuery(notInQueryObject, subquery.className, response.results); - // Recurse to repeat - return _this7.replaceNotInQuery(); - }); -}; - -var transformSelect = function transformSelect(selectObject, key, objects) { - var values = []; - var _iteratorNormalCompletion3 = true; - var _didIteratorError3 = false; - var _iteratorError3 = undefined; - - try { - for (var _iterator3 = objects[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { - var result = _step3.value; - - values.push(result[key]); - } - } catch (err) { - _didIteratorError3 = true; - _iteratorError3 = err; - } finally { - try { - if (!_iteratorNormalCompletion3 && _iterator3.return) { - _iterator3.return(); - } - } finally { - if (_didIteratorError3) { - throw _iteratorError3; - } - } - } - - delete selectObject['$select']; - if (Array.isArray(selectObject['$in'])) { - selectObject['$in'] = selectObject['$in'].concat(values); - } else { - selectObject['$in'] = values; - } -}; - -// Replaces a $select clause by running the subquery, if there is a -// $select clause. -// The $select clause turns into an $in with values selected out of -// the subquery. -// Returns a possible-promise. -RestQuery.prototype.replaceSelect = function () { - var _this8 = this; - - var selectObject = findObjectWithKey(this.restWhere, '$select'); - if (!selectObject) { - return; - } - - // 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'); - } - - var additionalOptions = { - redirectClassNameForKey: selectValue.query.redirectClassNameForKey - }; - - var subquery = new RestQuery(this.config, this.auth, selectValue.query.className, selectValue.query.where, additionalOptions); - return subquery.execute().then(function (response) { - transformSelect(selectObject, selectValue.key, response.results); - // Keep replacing $select clauses - return _this8.replaceSelect(); - }); -}; - -var transformDontSelect = function transformDontSelect(dontSelectObject, key, objects) { - var values = []; - var _iteratorNormalCompletion4 = true; - var _didIteratorError4 = false; - var _iteratorError4 = undefined; - - try { - for (var _iterator4 = objects[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { - var result = _step4.value; - - values.push(result[key]); - } - } catch (err) { - _didIteratorError4 = true; - _iteratorError4 = err; - } finally { - try { - if (!_iteratorNormalCompletion4 && _iterator4.return) { - _iterator4.return(); - } - } finally { - if (_didIteratorError4) { - throw _iteratorError4; - } - } - } - - delete dontSelectObject['$dontSelect']; - if (Array.isArray(dontSelectObject['$nin'])) { - dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values); - } else { - dontSelectObject['$nin'] = values; - } -}; - -// Replaces a $dontSelect clause by running the subquery, if there is a -// $dontSelect clause. -// The $dontSelect clause turns into an $nin with values selected out of -// the subquery. -// Returns a possible-promise. -RestQuery.prototype.replaceDontSelect = function () { - var _this9 = this; - - var dontSelectObject = findObjectWithKey(this.restWhere, '$dontSelect'); - if (!dontSelectObject) { - return; - } - - // 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'); - } - var additionalOptions = { - redirectClassNameForKey: dontSelectValue.query.redirectClassNameForKey - }; - - var subquery = new RestQuery(this.config, this.auth, dontSelectValue.query.className, dontSelectValue.query.where, additionalOptions); - return subquery.execute().then(function (response) { - transformDontSelect(dontSelectObject, dontSelectValue.key, response.results); - // Keep replacing $dontSelect clauses - return _this9.replaceDontSelect(); - }); -}; - -var cleanResultOfSensitiveUserInfo = function cleanResultOfSensitiveUserInfo(result, auth, config) { - delete result.password; - - if (auth.isMaster || auth.user && auth.user.id === result.objectId) { - return; - } - - var _iteratorNormalCompletion5 = true; - var _didIteratorError5 = false; - var _iteratorError5 = undefined; - - try { - for (var _iterator5 = config.userSensitiveFields[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { - var field = _step5.value; - - delete result[field]; - } - } catch (err) { - _didIteratorError5 = true; - _iteratorError5 = err; - } finally { - try { - if (!_iteratorNormalCompletion5 && _iterator5.return) { - _iterator5.return(); - } - } finally { - if (_didIteratorError5) { - throw _iteratorError5; - } - } - } -}; - -var cleanResultAuthData = function cleanResultAuthData(result) { - if (result.authData) { - Object.keys(result.authData).forEach(function (provider) { - if (result.authData[provider] === null) { - delete result.authData[provider]; - } - }); - - if (Object.keys(result.authData).length == 0) { - delete result.authData; - } - } -}; - -var replaceEqualityConstraint = function replaceEqualityConstraint(constraint) { - if ((typeof constraint === 'undefined' ? 'undefined' : _typeof(constraint)) !== 'object') { - return constraint; - } - var equalToObject = {}; - var hasDirectConstraint = false; - var hasOperatorConstraint = false; - for (var key in constraint) { - if (key.indexOf('$') !== 0) { - hasDirectConstraint = true; - equalToObject[key] = constraint[key]; - } else { - hasOperatorConstraint = true; - } - } - if (hasDirectConstraint && hasOperatorConstraint) { - constraint['$eq'] = equalToObject; - Object.keys(equalToObject).forEach(function (key) { - delete constraint[key]; - }); - } - return constraint; -}; - -RestQuery.prototype.replaceEquality = function () { - if (_typeof(this.restWhere) !== 'object') { - return; - } - for (var 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 () { - var _this10 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - if (this.findOptions.limit === 0) { - this.response = { results: [] }; - return Promise.resolve(); - } - var findOptions = Object.assign({}, this.findOptions); - if (this.keys) { - findOptions.keys = this.keys.map(function (key) { - return key.split('.')[0]; - }); - } - if (options.op) { - findOptions.op = options.op; - } - return this.config.database.find(this.className, this.restWhere, findOptions).then(function (results) { - if (_this10.className === '_User') { - var _iteratorNormalCompletion6 = true; - var _didIteratorError6 = false; - var _iteratorError6 = undefined; - - try { - for (var _iterator6 = results[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { - var result = _step6.value; - - cleanResultOfSensitiveUserInfo(result, _this10.auth, _this10.config); - cleanResultAuthData(result); - } - } catch (err) { - _didIteratorError6 = true; - _iteratorError6 = err; - } finally { - try { - if (!_iteratorNormalCompletion6 && _iterator6.return) { - _iterator6.return(); - } - } finally { - if (_didIteratorError6) { - throw _iteratorError6; - } - } - } - } - - _this10.config.filesController.expandFilesInObject(_this10.config, results); - - if (_this10.redirectClassName) { - var _iteratorNormalCompletion7 = true; - var _didIteratorError7 = false; - var _iteratorError7 = undefined; - - try { - for (var _iterator7 = results[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { - var r = _step7.value; - - r.className = _this10.redirectClassName; - } - } catch (err) { - _didIteratorError7 = true; - _iteratorError7 = err; - } finally { - try { - if (!_iteratorNormalCompletion7 && _iterator7.return) { - _iterator7.return(); - } - } finally { - if (_didIteratorError7) { - throw _iteratorError7; - } - } - } - } - _this10.response = { results: results }; - }); -}; - -// Returns a promise for whether it was successful. -// Populates this.response.count with the count -RestQuery.prototype.runCount = function () { - var _this11 = this; - - if (!this.doCount) { - return; - } - this.findOptions.count = true; - delete this.findOptions.skip; - delete this.findOptions.limit; - return this.config.database.find(this.className, this.restWhere, this.findOptions).then(function (c) { - _this11.response.count = c; - }); -}; - -// Augments this.response with data at the paths provided in this.include. -RestQuery.prototype.handleInclude = function () { - var _this12 = this; - - if (this.include.length == 0) { - return; - } - - var pathResponse = includePath(this.config, this.auth, this.response, this.include[0], this.restOptions); - if (pathResponse.then) { - return pathResponse.then(function (newResponse) { - _this12.response = newResponse; - _this12.include = _this12.include.slice(1); - return _this12.handleInclude(); - }); - } else if (this.include.length > 0) { - this.include = this.include.slice(1); - return this.handleInclude(); - } - - return pathResponse; -}; - -//Returns a promise of a processed set of results -RestQuery.prototype.runAfterFindTrigger = function () { - var _this13 = this; - - if (!this.response) { - return; - } - // Avoid doing any setup for triggers if there is no 'afterFind' trigger for this class. - var hasAfterFindHook = triggers.triggerExists(this.className, triggers.Types.afterFind, this.config.applicationId); - if (!hasAfterFindHook) { - 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(function (results) { - _this13.response.results = results; - }); -}; - -// Adds included values to the response. -// Path is a list of field names. -// Returns a promise for an augmented response. -function includePath(config, auth, response, path) { - var restOptions = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; - - var pointers = findPointers(response.results, path); - if (pointers.length == 0) { - return response; - } - var pointersHash = {}; - var _iteratorNormalCompletion8 = true; - var _didIteratorError8 = false; - var _iteratorError8 = undefined; - - try { - for (var _iterator8 = pointers[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { - var pointer = _step8.value; - - if (!pointer) { - continue; - } - var className = pointer.className; - // only include the good pointers - if (className) { - pointersHash[className] = pointersHash[className] || new Set(); - pointersHash[className].add(pointer.objectId); - } - } - } catch (err) { - _didIteratorError8 = true; - _iteratorError8 = err; - } finally { - try { - if (!_iteratorNormalCompletion8 && _iterator8.return) { - _iterator8.return(); - } - } finally { - if (_didIteratorError8) { - throw _iteratorError8; - } - } - } - - var includeRestOptions = {}; - if (restOptions.keys) { - var keys = new Set(restOptions.keys.split(',')); - var keySet = Array.from(keys).reduce(function (set, key) { - var keyPath = key.split('.'); - var i = 0; - for (i; i < path.length; i++) { - if (path[i] != keyPath[i]) { - return set; - } - } - if (i < keyPath.length) { - set.add(keyPath[i]); - } - return set; - }, new Set()); - if (keySet.size > 0) { - includeRestOptions.keys = Array.from(keySet).join(','); - } - } - - var queryPromises = Object.keys(pointersHash).map(function (className) { - var where = { 'objectId': { '$in': Array.from(pointersHash[className]) } }; - var query = new RestQuery(config, auth, className, where, includeRestOptions); - return query.execute({ op: 'get' }).then(function (results) { - results.className = className; - return Promise.resolve(results); - }); - }); - - // Get the objects for all these object ids - return Promise.all(queryPromises).then(function (responses) { - var replace = responses.reduce(function (replace, includeResponse) { - var _iteratorNormalCompletion9 = true; - var _didIteratorError9 = false; - var _iteratorError9 = undefined; - - try { - for (var _iterator9 = includeResponse.results[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { - var obj = _step9.value; - - obj.__type = 'Object'; - obj.className = includeResponse.className; - - if (obj.className == "_User" && !auth.isMaster) { - delete obj.sessionToken; - delete obj.authData; - } - replace[obj.objectId] = obj; - } - } catch (err) { - _didIteratorError9 = true; - _iteratorError9 = err; - } finally { - try { - if (!_iteratorNormalCompletion9 && _iterator9.return) { - _iterator9.return(); - } - } finally { - if (_didIteratorError9) { - throw _iteratorError9; - } - } - } - - return replace; - }, {}); - - var resp = { - results: replacePointers(response.results, path, replace) - }; - if (response.count) { - resp.count = response.count; - } - return resp; - }); -} - -// Object may be a list of REST-format object to find pointers in, or -// it may be a single object. -// If the path yields things that aren't pointers, this throws an error. -// Path is a list of fields to search into. -// Returns a list of pointers in REST format. -function findPointers(object, path) { - if (object instanceof Array) { - var answer = []; - var _iteratorNormalCompletion10 = true; - var _didIteratorError10 = false; - var _iteratorError10 = undefined; - - try { - for (var _iterator10 = object[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { - var x = _step10.value; - - answer = answer.concat(findPointers(x, path)); - } - } catch (err) { - _didIteratorError10 = true; - _iteratorError10 = err; - } finally { - try { - if (!_iteratorNormalCompletion10 && _iterator10.return) { - _iterator10.return(); - } - } finally { - if (_didIteratorError10) { - throw _iteratorError10; - } - } - } - - return answer; - } - - if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) !== 'object' || !object) { - return []; - } - - if (path.length == 0) { - if (object === null || object.__type == 'Pointer') { - return [object]; - } - return []; - } - - var subobject = object[path[0]]; - if (!subobject) { - return []; - } - return findPointers(subobject, path.slice(1)); -} - -// Object may be a list of REST-format objects to replace pointers -// in, or it may be a single object. -// Path is a list of fields to search into. -// replace is a map from object id -> object. -// Returns something analogous to object, but with the appropriate -// pointers inflated. -function replacePointers(object, path, replace) { - if (object instanceof Array) { - return object.map(function (obj) { - return replacePointers(obj, path, replace); - }).filter(function (obj) { - return typeof obj !== 'undefined'; - }); - } - - if ((typeof object === 'undefined' ? 'undefined' : _typeof(object)) !== 'object' || !object) { - return object; - } - - if (path.length === 0) { - if (object && object.__type === 'Pointer') { - return replace[object.objectId]; - } - return object; - } - - var subobject = object[path[0]]; - if (!subobject) { - return object; - } - var newsub = replacePointers(subobject, path.slice(1), replace); - var answer = {}; - for (var key in object) { - if (key == path[0]) { - answer[key] = newsub; - } else { - answer[key] = object[key]; - } - } - return answer; -} - -// Finds a subobject that has the given key, if there is one. -// Returns undefined otherwise. -function findObjectWithKey(root, key) { - if ((typeof root === 'undefined' ? 'undefined' : _typeof(root)) !== 'object') { - return; - } - if (root instanceof Array) { - var _iteratorNormalCompletion11 = true; - var _didIteratorError11 = false; - var _iteratorError11 = undefined; - - try { - for (var _iterator11 = root[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) { - var item = _step11.value; - - var answer = findObjectWithKey(item, key); - if (answer) { - return answer; - } - } - } catch (err) { - _didIteratorError11 = true; - _iteratorError11 = err; - } finally { - try { - if (!_iteratorNormalCompletion11 && _iterator11.return) { - _iterator11.return(); - } - } finally { - if (_didIteratorError11) { - throw _iteratorError11; - } - } - } - } - if (root && root[key]) { - return root; - } - for (var subkey in root) { - var _answer = findObjectWithKey(root[subkey], key); - if (_answer) { - return _answer; - } - } -} - -module.exports = RestQuery; \ No newline at end of file diff --git a/lib/RestWrite.js b/lib/RestWrite.js deleted file mode 100644 index cd115ac922..0000000000 --- a/lib/RestWrite.js +++ /dev/null @@ -1,1104 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _RestQuery = require('./RestQuery'); - -var _RestQuery2 = _interopRequireDefault(_RestQuery); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -// A RestWrite encapsulates everything we need to run an operation -// that writes to the database. -// This could be either a "create" or an "update". - -var SchemaController = require('./Controllers/SchemaController'); -var deepcopy = require('deepcopy'); - -var Auth = require('./Auth'); -var cryptoUtils = require('./cryptoUtils'); -var passwordCrypto = require('./password'); -var Parse = require('parse/node'); -var triggers = require('./triggers'); -var ClientSDK = require('./ClientSDK'); - - -// query and data are both provided in REST API format. So data -// types are encoded by plain old objects. -// If query is null, this is a "create" and the data in data should be -// created. -// Otherwise this is an "update" - the object matching the query -// should get updated with data. -// 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) { - this.config = config; - this.auth = auth; - this.className = className; - this.clientSDK = clientSDK; - this.storage = {}; - this.runOptions = {}; - if (!query && data.objectId) { - 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 - // fields. - // response: the actual data to be returned - // status: the http status code. if not present, treated like a 200 - // location: the location header. if not present, no location header - this.response = null; - - // Processing this operation may mutate our data, so we operate on a - // copy - this.query = deepcopy(query); - this.data = deepcopy(data); - // We never change originalData, so we do not need a deep copy - this.originalData = originalData; - - // The timestamp we'll use for this whole operation - this.updatedAt = Parse._encode(new Date()).iso; -} - -// A convenient method to perform all the steps of processing the -// write, in order. -// Returns a promise for a {response, status, location} object. -// status and location are optional. -RestWrite.prototype.execute = function () { - var _this = this; - - return Promise.resolve().then(function () { - return _this.getUserAndRoleACL(); - }).then(function () { - return _this.validateClientClassCreation(); - }).then(function () { - return _this.handleInstallation(); - }).then(function () { - return _this.handleSession(); - }).then(function () { - return _this.validateAuthData(); - }).then(function () { - return _this.runBeforeTrigger(); - }).then(function () { - return _this.validateSchema(); - }).then(function () { - return _this.setRequiredFieldsIfNeeded(); - }).then(function () { - return _this.transformUser(); - }).then(function () { - return _this.expandFilesForExistingObjects(); - }).then(function () { - return _this.runDatabaseOperation(); - }).then(function () { - return _this.createSessionTokenIfNeeded(); - }).then(function () { - return _this.handleFollowup(); - }).then(function () { - return _this.runAfterTrigger(); - }).then(function () { - return _this.cleanUserAuthData(); - }).then(function () { - return _this.response; - }); -}; - -// Uses the Auth object to get the list of roles, adds the user id -RestWrite.prototype.getUserAndRoleACL = function () { - var _this2 = this; - - if (this.auth.isMaster) { - return Promise.resolve(); - } - - this.runOptions.acl = ['*']; - - if (this.auth.user) { - return this.auth.getUserRoles().then(function (roles) { - roles.push(_this2.auth.user.id); - _this2.runOptions.acl = _this2.runOptions.acl.concat(roles); - return; - }); - } else { - return Promise.resolve(); - } -}; - -// Validates this operation against the allowClientClassCreation config. -RestWrite.prototype.validateClientClassCreation = function () { - var _this3 = this; - - if (this.config.allowClientClassCreation === false && !this.auth.isMaster && SchemaController.systemClasses.indexOf(this.className) === -1) { - return this.config.database.loadSchema().then(function (schemaController) { - return schemaController.hasClass(_this3.className); - }).then(function (hasClass) { - if (hasClass !== true) { - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, 'This user is not allowed to access ' + 'non-existent class: ' + _this3.className); - } - }); - } else { - return Promise.resolve(); - } -}; - -// Validates this operation against the schema. -RestWrite.prototype.validateSchema = function () { - return this.config.database.validateObject(this.className, this.data, this.query, this.runOptions); -}; - -// Runs any beforeSave triggers against this operation. -// Any change leads to our data being mutated. -RestWrite.prototype.runBeforeTrigger = function () { - var _this4 = this; - - if (this.response) { - return; - } - - // 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)) { - return Promise.resolve(); - } - - // Cloud code gets a bit of extra data for its objects - var extraData = { className: this.className }; - if (this.query && this.query.objectId) { - extraData.objectId = this.query.objectId; - } - - var originalObject = null; - var updatedObject = triggers.inflate(extraData, this.originalData); - if (this.query && this.query.objectId) { - // This is an update for existing object. - originalObject = triggers.inflate(extraData, this.originalData); - } - updatedObject.set(this.sanitizedData()); - - return Promise.resolve().then(function () { - return triggers.maybeRunTrigger(triggers.Types.beforeSave, _this4.auth, updatedObject, originalObject, _this4.config); - }).then(function (response) { - if (response && response.object) { - _this4.storage.fieldsChangedByTrigger = _lodash2.default.reduce(response.object, function (result, value, key) { - if (!_lodash2.default.isEqual(_this4.data[key], value)) { - result.push(key); - } - return result; - }, []); - _this4.data = response.object; - // We should delete the objectId for an update write - if (_this4.query && _this4.query.objectId) { - delete _this4.data.objectId; - } - } - }); -}; - -RestWrite.prototype.setRequiredFieldsIfNeeded = function () { - if (this.data) { - // Add default fields - this.data.updatedAt = this.updatedAt; - if (!this.query) { - this.data.createdAt = this.updatedAt; - - // Only assign new objectId if we are creating new object - if (!this.data.objectId) { - this.data.objectId = cryptoUtils.newObjectId(); - } - } - } - return Promise.resolve(); -}; - -// Transforms auth data for a user object. -// Does nothing if this isn't a user object. -// Returns a promise for when we're done if it can't finish this tick. -RestWrite.prototype.validateAuthData = function () { - if (this.className !== '_User') { - return; - } - - if (!this.query && !this.data.authData) { - if (typeof this.data.username !== 'string' || _lodash2.default.isEmpty(this.data.username)) { - throw new Parse.Error(Parse.Error.USERNAME_MISSING, 'bad or missing username'); - } - if (typeof this.data.password !== 'string' || _lodash2.default.isEmpty(this.data.password)) { - throw new Parse.Error(Parse.Error.PASSWORD_MISSING, 'password is required'); - } - } - - if (!this.data.authData || !Object.keys(this.data.authData).length) { - return; - } - - var authData = this.data.authData; - var providers = Object.keys(authData); - if (providers.length > 0) { - var canHandleAuthData = providers.reduce(function (canHandle, provider) { - var providerAuthData = authData[provider]; - 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.'); -}; - -RestWrite.prototype.handleAuthDataValidation = function (authData) { - var _this5 = this; - - var validations = Object.keys(authData).map(function (provider) { - if (authData[provider] === null) { - return Promise.resolve(); - } - var validateAuthData = _this5.config.authDataManager.getValidatorForProvider(provider); - if (!validateAuthData) { - 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) { - var providers = Object.keys(authData); - var query = providers.reduce(function (memo, provider) { - if (!authData[provider]) { - return memo; - } - var queryKey = 'authData.' + provider + '.id'; - var query = {}; - query[queryKey] = authData[provider].id; - memo.push(query); - return memo; - }, []).filter(function (q) { - return typeof q !== 'undefined'; - }); - - var findPromise = Promise.resolve([]); - if (query.length > 0) { - findPromise = this.config.database.find(this.className, { '$or': query }, {}); - } - - return findPromise; -}; - -RestWrite.prototype.handleAuthData = function (authData) { - var _this6 = this; - - var results = void 0; - return this.findUsersWithAuthData(authData).then(function (r) { - results = 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'); - } - - _this6.storage['authProvider'] = Object.keys(authData).join(','); - - if (results.length > 0) { - if (!_this6.query) { - // Login with auth data - delete results[0].password; - var userResult = results[0]; - - // need to set the objectId first otherwise location has trailing undefined - _this6.data.objectId = userResult.objectId; - - // Determine if authData was updated - var mutatedAuthData = {}; - Object.keys(authData).forEach(function (provider) { - var providerData = authData[provider]; - var userAuthData = userResult.authData[provider]; - if (!_lodash2.default.isEqual(providerData, userAuthData)) { - mutatedAuthData[provider] = providerData; - } - }); - _this6.response = { - response: userResult, - location: _this6.location() - }; - - // If we didn't change the auth data, just keep going - if (Object.keys(mutatedAuthData).length === 0) { - return; - } - // We have authData that is updated on login - // that can happen when token are refreshed, - // We should update the token and let the user in - // We should only check the mutated keys - return _this6.handleAuthDataValidation(mutatedAuthData).then(function () { - // Assign the new authData in the response - Object.keys(mutatedAuthData).forEach(function (provider) { - _this6.response.response.authData[provider] = mutatedAuthData[provider]; - }); - // Run the DB update directly, as 'master' - // Just update the authData part - return _this6.config.database.update(_this6.className, { objectId: _this6.data.objectId }, { authData: mutatedAuthData }, {}); - }); - } else if (_this6.query && _this6.query.objectId) { - // Trying to update auth data but users - // are different - if (results[0].objectId !== _this6.query.objectId) { - throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED, 'this auth is already used'); - } - } - } - return _this6.handleAuthDataValidation(authData); - }); -}; - -// The non-third-party parts of User transformation -RestWrite.prototype.transformUser = function () { - var _this7 = this; - - var promise = Promise.resolve(); - - if (this.className !== '_User') { - return promise; - } - - if (!this.auth.isMaster && "emailVerified" in this.data) { - var error = 'Clients aren\'t allowed to manually update email verification.'; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); - } - - // Do not cleanup session if objectId is not set - if (this.query && this.objectId()) { - // If we're updating a _User object, we need to clear out the cache for that user. Find all their - // session tokens, and remove them from the cache. - promise = new _RestQuery2.default(this.config, Auth.master(this.config), '_Session', { - user: { - __type: "Pointer", - className: "_User", - objectId: this.objectId() - } - }).execute().then(function (results) { - results.results.forEach(function (session) { - return _this7.config.cacheController.user.del(session.sessionToken); - }); - }); - } - - return promise.then(function () { - // Transform the password - if (_this7.data.password === undefined) { - // ignore only if undefined. should proceed if empty ('') - return Promise.resolve(); - } - - if (_this7.query) { - _this7.storage['clearSessions'] = true; - // Generate a new session only if the user requested - if (!_this7.auth.isMaster) { - _this7.storage['generateNewSession'] = true; - } - } - - return _this7._validatePasswordPolicy().then(function () { - return passwordCrypto.hash(_this7.data.password).then(function (hashedPassword) { - _this7.data._hashed_password = hashedPassword; - delete _this7.data.password; - }); - }); - }).then(function () { - return _this7._validateUserName(); - }).then(function () { - return _this7._validateEmail(); - }); -}; - -RestWrite.prototype._validateUserName = function () { - // Check for username uniqueness - if (!this.data.username) { - if (!this.query) { - this.data.username = cryptoUtils.randomString(25); - this.responseShouldHaveUsername = true; - } - return Promise.resolve(); - } - // 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(function (results) { - if (results.length > 0) { - throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); - } - return; - }); -}; - -RestWrite.prototype._validateEmail = function () { - var _this8 = this; - - if (!this.data.email || this.data.email.__op === 'Delete') { - return Promise.resolve(); - } - // 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.')); - } - // 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(function (results) { - if (results.length > 0) { - throw new Parse.Error(Parse.Error.EMAIL_TAKEN, 'Account already exists for this email address.'); - } - // We updated the email, send a new validation - _this8.storage['sendVerificationEmail'] = true; - _this8.config.userController.setEmailVerifyToken(_this8.data); - }); -}; - -RestWrite.prototype._validatePasswordPolicy = function () { - var _this9 = this; - - if (!this.config.passwordPolicy) return Promise.resolve(); - return this._validatePasswordRequirements().then(function () { - return _this9._validatePasswordHistory(); - }); -}; - -RestWrite.prototype._validatePasswordRequirements = function () { - var _this10 = this; - - // check if the password conforms to the defined password policy if configured - var 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)); - } - - // 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.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() }).then(function (results) { - if (results.length != 1) { - throw undefined; - } - if (_this10.data.password.indexOf(results[0].username) >= 0) return Promise.reject(new Parse.Error(Parse.Error.VALIDATION_ERROR, policyError)); - return Promise.resolve(); - }); - } - } - return Promise.resolve(); -}; - -RestWrite.prototype._validatePasswordHistory = function () { - var _this11 = this; - - // 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"] }).then(function (results) { - if (results.length != 1) { - throw undefined; - } - var user = results[0]; - var oldPasswords = []; - if (user._password_history) oldPasswords = _lodash2.default.take(user._password_history, _this11.config.passwordPolicy.maxPasswordHistory - 1); - oldPasswords.push(user.password); - var newPassword = _this11.data.password; - // compare the new password hash with all old password hashes - var promises = oldPasswords.map(function (hash) { - return passwordCrypto.compare(newPassword, hash).then(function (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(function () { - return Promise.resolve(); - }).catch(function (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 ' + _this11.config.passwordPolicy.maxPasswordHistory + ' passwords.')); - throw err; - }); - }); - } - return Promise.resolve(); -}; - -RestWrite.prototype.createSessionTokenIfNeeded = function () { - if (this.className !== '_User') { - return; - } - if (this.query) { - return; - } - return this.createSessionToken(); -}; - -RestWrite.prototype.createSessionToken = function () { - // cloud installationId from Cloud Code, - // never create session tokens from there. - if (this.auth.installationId && this.auth.installationId === 'cloud') { - return; - } - var token = 'r:' + cryptoUtils.newToken(); - - var expiresAt = this.config.generateSessionExpiresAt(); - var sessionData = { - sessionToken: token, - user: { - __type: 'Pointer', - className: '_User', - objectId: this.objectId() - }, - createdWith: { - 'action': 'signup', - 'authProvider': this.storage['authProvider'] || 'password' - }, - restricted: false, - installationId: this.auth.installationId, - expiresAt: Parse._encode(expiresAt) - }; - if (this.response && this.response.response) { - this.response.response.sessionToken = token; - } - var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData); - return create.execute(); -}; - -// Handles any followup logic -RestWrite.prototype.handleFollowup = function () { - if (this.storage && this.storage['clearSessions'] && this.config.revokeSessionOnPasswordReset) { - var sessionQuery = { - user: { - __type: 'Pointer', - className: '_User', - objectId: this.objectId() - } - }; - delete this.storage['clearSessions']; - 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)); - } - - if (this.storage && this.storage['sendVerificationEmail']) { - delete this.storage['sendVerificationEmail']; - // Fire and forget! - this.config.userController.sendVerificationEmail(this.data); - return this.handleFollowup.bind(this); - } -}; - -// Handles the _Session class specialness. -// Does nothing if this isn't an installation object. -RestWrite.prototype.handleSession = function () { - var _this12 = this; - - if (this.response || this.className !== '_Session') { - return; - } - - if (!this.auth.user && !this.auth.isMaster) { - 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.'); - } - - if (!this.query && !this.auth.isMaster) { - var token = 'r:' + cryptoUtils.newToken(); - var expiresAt = this.config.generateSessionExpiresAt(); - var sessionData = { - sessionToken: token, - user: { - __type: 'Pointer', - className: '_User', - objectId: this.auth.user.id - }, - createdWith: { - 'action': 'create' - }, - restricted: true, - expiresAt: Parse._encode(expiresAt) - }; - for (var key in this.data) { - if (key == 'objectId') { - continue; - } - sessionData[key] = this.data[key]; - } - var create = new RestWrite(this.config, Auth.master(this.config), '_Session', null, sessionData); - return create.execute().then(function (results) { - if (!results.response) { - throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Error creating session.'); - } - sessionData['objectId'] = results.response['objectId']; - _this12.response = { - status: 201, - location: results.location, - response: sessionData - }; - }); - } -}; - -// Handles the _Installation class specialness. -// Does nothing if this isn't an installation object. -// If an installation is found, this can mutate this.query and turn a create -// into an update. -// Returns a promise for when we're done if it can't finish this tick. -RestWrite.prototype.handleInstallation = function () { - var _this13 = this; - - if (this.response || this.className !== '_Installation') { - return; - } - - 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'); - } - - // If the device token is 64 characters long, we assume it is for iOS - // and lowercase it. - if (this.data.deviceToken && this.data.deviceToken.length == 64) { - this.data.deviceToken = this.data.deviceToken.toLowerCase(); - } - - // We lowercase the installationId if present - if (this.data.installationId) { - this.data.installationId = this.data.installationId.toLowerCase(); - } - - var installationId = this.data.installationId; - - // If data.installationId is not set and we're not master, we can lookup in auth - if (!installationId && !this.auth.isMaster) { - installationId = this.auth.installationId; - } - - if (installationId) { - installationId = installationId.toLowerCase(); - } - - // Updating _Installation but not updating anything critical - if (this.query && !this.data.deviceToken && !installationId && !this.data.deviceType) { - return; - } - - var promise = Promise.resolve(); - - var idMatch; // Will be a match on either objectId or installationId - var objectIdMatch; - var installationIdMatch; - var deviceTokenMatches = []; - - // Instead of issuing 3 reads, let's do it with one OR. - var orQueries = []; - if (this.query && this.query.objectId) { - orQueries.push({ - objectId: this.query.objectId - }); - } - if (installationId) { - orQueries.push({ - 'installationId': installationId - }); - } - if (this.data.deviceToken) { - orQueries.push({ 'deviceToken': this.data.deviceToken }); - } - - if (orQueries.length == 0) { - return; - } - - promise = promise.then(function () { - return _this13.config.database.find('_Installation', { - '$or': orQueries - }, {}); - }).then(function (results) { - results.forEach(function (result) { - if (_this13.query && _this13.query.objectId && result.objectId == _this13.query.objectId) { - objectIdMatch = result; - } - if (result.installationId == installationId) { - installationIdMatch = result; - } - if (result.deviceToken == _this13.data.deviceToken) { - deviceTokenMatches.push(result); - } - }); - - // Sanity checks when running a query - if (_this13.query && _this13.query.objectId) { - if (!objectIdMatch) { - throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found for update.'); - } - if (_this13.data.installationId && objectIdMatch.installationId && _this13.data.installationId !== objectIdMatch.installationId) { - throw new Parse.Error(136, 'installationId may not be changed in this ' + 'operation'); - } - if (_this13.data.deviceToken && objectIdMatch.deviceToken && _this13.data.deviceToken !== objectIdMatch.deviceToken && !_this13.data.installationId && !objectIdMatch.installationId) { - throw new Parse.Error(136, 'deviceToken may not be changed in this ' + 'operation'); - } - if (_this13.data.deviceType && _this13.data.deviceType && _this13.data.deviceType !== objectIdMatch.deviceType) { - throw new Parse.Error(136, 'deviceType may not be changed in this ' + 'operation'); - } - } - - if (_this13.query && _this13.query.objectId && objectIdMatch) { - idMatch = objectIdMatch; - } - - if (installationId && installationIdMatch) { - idMatch = installationIdMatch; - } - // need to specify deviceType only if it's new - if (!_this13.query && !_this13.data.deviceType && !idMatch) { - throw new Parse.Error(135, 'deviceType must be specified in this operation'); - } - }).then(function () { - 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 (!_this13.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': _this13.data.deviceToken, - 'installationId': { - '$ne': installationId - } - }; - if (_this13.data.appIdentifier) { - delQuery['appIdentifier'] = _this13.data.appIdentifier; - } - _this13.config.database.destroy('_Installation', delQuery).catch(function (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 (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. - var _delQuery = { objectId: idMatch.objectId }; - return _this13.config.database.destroy('_Installation', _delQuery).then(function () { - return deviceTokenMatches[0]['objectId']; - }).catch(function (err) { - if (err.code == Parse.Error.OBJECT_NOT_FOUND) { - // no deletions were made. Can be ignored - return; - } - // rethrow the error - throw err; - }); - } else { - if (_this13.data.deviceToken && idMatch.deviceToken != _this13.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. - var _delQuery2 = { - 'deviceToken': _this13.data.deviceToken - }; - // We have a unique install Id, use that to preserve - // the interesting installation - if (_this13.data.installationId) { - _delQuery2['installationId'] = { - '$ne': _this13.data.installationId - }; - } else if (idMatch.objectId && _this13.data.objectId && idMatch.objectId == _this13.data.objectId) { - // we passed an objectId, preserve that instalation - _delQuery2['objectId'] = { - '$ne': idMatch.objectId - }; - } else { - // What to do here? can't really clean up everything... - return idMatch.objectId; - } - if (_this13.data.appIdentifier) { - _delQuery2['appIdentifier'] = _this13.data.appIdentifier; - } - _this13.config.database.destroy('_Installation', _delQuery2).catch(function (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; - } - } - }).then(function (objId) { - if (objId) { - _this13.query = { objectId: objId }; - delete _this13.data.objectId; - delete _this13.data.createdAt; - } - // TODO: Validate ops (add/remove on channels, $inc on badge, etc.) - }); - return promise; -}; - -// If we short-circuted the object response - then we need to make sure we expand all the files, -// since this might not have a query, meaning it won't return the full result back. -// TODO: (nlutsenko) This should die when we move to per-class based controllers on _Session/_User -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); - } -}; - -RestWrite.prototype.runDatabaseOperation = function () { - var _this14 = this; - - if (this.response) { - return; - } - - if (this.className === '_Role') { - this.config.cacheController.role.clear(); - } - - if (this.className === '_User' && this.query && !this.auth.couldUpdateUserId(this.query.objectId)) { - throw new Parse.Error(Parse.Error.SESSION_MISSING, 'Cannot modify user ' + this.query.objectId + '.'); - } - - if (this.className === '_Product' && this.data.download) { - this.data.downloadName = this.data.download.name; - } - - // TODO: Add better detection for ACL, ensuring a user can't be locked from - // their own user record. - if (this.data.ACL && this.data.ACL['*unresolved']) { - throw new Parse.Error(Parse.Error.INVALID_ACL, 'Invalid ACL.'); - } - - if (this.query) { - // Force the user to not lockout - // Matched with parse.com - if (this.className === '_User' && this.data.ACL) { - this.data.ACL[this.query.objectId] = { read: true, write: true }; - } - // update password timestamp if user password is being changed - 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 - delete this.data.createdAt; - - var 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(function (results) { - if (results.length != 1) { - throw undefined; - } - var user = results[0]; - var oldPasswords = []; - if (user._password_history) { - oldPasswords = _lodash2.default.take(user._password_history, _this14.config.passwordPolicy.maxPasswordHistory); - } - //n-1 passwords go into history including last password - while (oldPasswords.length > _this14.config.passwordPolicy.maxPasswordHistory - 2) { - oldPasswords.shift(); - } - oldPasswords.push(user.password); - _this14.data._password_history = oldPasswords; - }); - } - - return defer.then(function () { - // Run an update - return _this14.config.database.update(_this14.className, _this14.query, _this14.data, _this14.runOptions).then(function (response) { - response.updatedAt = _this14.updatedAt; - _this14._updateResponseWithData(response, _this14.data); - _this14.response = { response: response }; - }); - }); - } else { - // Set the default ACL and password timestamp for the new _User - if (this.className === '_User') { - var ACL = this.data.ACL; - // default public r/w ACL - if (!ACL) { - ACL = {}; - ACL['*'] = { read: true, write: false }; - } - // make sure the user is not locked down - ACL[this.data.objectId] = { read: true, write: true }; - this.data.ACL = ACL; - // password timestamp to be used when password expiry policy is enforced - 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).catch(function (error) { - if (_this14.className !== '_User' || error.code !== Parse.Error.DUPLICATE_VALUE) { - throw error; - } - // If this was a failed user creation due to username or email already taken, we need to - // check whether it was username or email and return the appropriate error. - - // TODO: See if we can later do this without additional queries by using named indexes. - return _this14.config.database.find(_this14.className, { username: _this14.data.username, objectId: { '$ne': _this14.objectId() } }, { limit: 1 }).then(function (results) { - if (results.length > 0) { - throw new Parse.Error(Parse.Error.USERNAME_TAKEN, 'Account already exists for this username.'); - } - return _this14.config.database.find(_this14.className, { email: _this14.data.email, objectId: { '$ne': _this14.objectId() } }, { limit: 1 }); - }).then(function (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.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided'); - }); - }).then(function (response) { - response.objectId = _this14.data.objectId; - response.createdAt = _this14.data.createdAt; - - if (_this14.responseShouldHaveUsername) { - response.username = _this14.data.username; - } - _this14._updateResponseWithData(response, _this14.data); - _this14.response = { - status: 201, - response: response, - location: _this14.location() - }; - }); - } -}; - -// Returns nothing - doesn't wait for the trigger. -RestWrite.prototype.runAfterTrigger = function () { - if (!this.response || !this.response.response) { - return; - } - - // Avoid doing any setup for triggers if there is no 'afterSave' trigger for this class. - var hasAfterSaveHook = triggers.triggerExists(this.className, triggers.Types.afterSave, this.config.applicationId); - var hasLiveQuery = this.config.liveQueryController.hasLiveQuery(this.className); - if (!hasAfterSaveHook && !hasLiveQuery) { - return Promise.resolve(); - } - - var extraData = { className: this.className }; - if (this.query && this.query.objectId) { - extraData.objectId = this.query.objectId; - } - - // Build the original object, we only do this for a update write. - var originalObject = void 0; - if (this.query && this.query.objectId) { - originalObject = triggers.inflate(extraData, this.originalData); - } - - // Build the inflated object, different from beforeSave, originalData is not empty - // since developers can change data in the beforeSave. - var updatedObject = triggers.inflate(extraData, this.originalData); - updatedObject.set(this.sanitizedData()); - updatedObject._handleSaveResponse(this.response.response, this.response.status || 200); - - // Notifiy LiveQueryServer if possible - this.config.liveQueryController.onAfterSave(updatedObject.className, updatedObject, originalObject); - - // Run afterSave trigger - return triggers.maybeRunTrigger(triggers.Types.afterSave, this.auth, updatedObject, originalObject, this.config); -}; - -// A helper to figure out what location this operation happens at. -RestWrite.prototype.location = function () { - var middle = this.className === '_User' ? '/users/' : '/classes/' + this.className + '/'; - return this.config.mount + middle + this.data.objectId; -}; - -// A helper to get the object id for this operation. -// Because it could be either on the query or on the data -RestWrite.prototype.objectId = function () { - return this.data.objectId || this.query.objectId; -}; - -// Returns a copy of the data and delete bad keys (_auth_data, _hashed_password...) -RestWrite.prototype.sanitizedData = function () { - var data = Object.keys(this.data).reduce(function (data, key) { - // Regexp comes from Parse.Object.prototype.validate - if (!/^[A-Za-z][0-9A-Za-z_]*$/.test(key)) { - delete data[key]; - } - return data; - }, deepcopy(this.data)); - return Parse._decode(undefined, data); -}; - -RestWrite.prototype.cleanUserAuthData = function () { - if (this.response && this.response.response && this.className === '_User') { - var user = this.response.response; - if (user.authData) { - Object.keys(user.authData).forEach(function (provider) { - if (user.authData[provider] === null) { - delete user.authData[provider]; - } - }); - if (Object.keys(user.authData).length == 0) { - delete user.authData; - } - } - } -}; - -RestWrite.prototype._updateResponseWithData = function (response, data) { - if (_lodash2.default.isEmpty(this.storage.fieldsChangedByTrigger)) { - return response; - } - var clientSupportsDelete = ClientSDK.supportsForwardDelete(this.clientSDK); - this.storage.fieldsChangedByTrigger.forEach(function (fieldName) { - var dataValue = data[fieldName]; - var responseValue = response[fieldName]; - - response[fieldName] = responseValue || dataValue; - - // Strips operations from responses - if (response[fieldName] && response[fieldName].__op) { - delete response[fieldName]; - if (clientSupportsDelete && dataValue.__op == 'Delete') { - response[fieldName] = dataValue; - } - } - }); - return response; -}; - -exports.default = RestWrite; - -module.exports = RestWrite; \ No newline at end of file diff --git a/lib/Routers/AnalyticsRouter.js b/lib/Routers/AnalyticsRouter.js deleted file mode 100644 index 323cd1d5eb..0000000000 --- a/lib/Routers/AnalyticsRouter.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.AnalyticsRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // AnalyticsRouter.js - - -function appOpened(req) { - var analyticsController = req.config.analyticsController; - return analyticsController.appOpened(req); -} - -function trackEvent(req) { - var analyticsController = req.config.analyticsController; - return analyticsController.trackEvent(req); -} - -var AnalyticsRouter = exports.AnalyticsRouter = function (_PromiseRouter) { - _inherits(AnalyticsRouter, _PromiseRouter); - - function AnalyticsRouter() { - _classCallCheck(this, AnalyticsRouter); - - return _possibleConstructorReturn(this, (AnalyticsRouter.__proto__ || Object.getPrototypeOf(AnalyticsRouter)).apply(this, arguments)); - } - - _createClass(AnalyticsRouter, [{ - key: 'mountRoutes', - value: function mountRoutes() { - this.route('POST', '/events/AppOpened', appOpened); - this.route('POST', '/events/:eventName', trackEvent); - } - }]); - - return AnalyticsRouter; -}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/ClassesRouter.js b/lib/Routers/ClassesRouter.js deleted file mode 100644 index e8599e0f6e..0000000000 --- a/lib/Routers/ClassesRouter.js +++ /dev/null @@ -1,281 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.ClassesRouter = undefined; - -var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _rest = require('../rest'); - -var _rest2 = _interopRequireDefault(_rest); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var ALLOWED_GET_QUERY_KEYS = ['keys', 'include']; - -var ClassesRouter = exports.ClassesRouter = function (_PromiseRouter) { - _inherits(ClassesRouter, _PromiseRouter); - - function ClassesRouter() { - _classCallCheck(this, ClassesRouter); - - return _possibleConstructorReturn(this, (ClassesRouter.__proto__ || Object.getPrototypeOf(ClassesRouter)).apply(this, arguments)); - } - - _createClass(ClassesRouter, [{ - key: 'handleFind', - value: function handleFind(req) { - var body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); - var options = {}; - var allowConstraints = ['skip', 'limit', 'order', 'count', 'keys', 'include', 'redirectClassNameForKey', 'where']; - - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = Object.keys(body)[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var key = _step.value; - - if (allowConstraints.indexOf(key) === -1) { - throw new _node2.default.Error(_node2.default.Error.INVALID_QUERY, 'Invalid parameter for query: ' + key); - } - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator.return) { - _iterator.return(); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - - if (body.skip) { - options.skip = Number(body.skip); - } - if (body.limit || body.limit === 0) { - options.limit = Number(body.limit); - } else { - options.limit = Number(100); - } - if (body.order) { - options.order = String(body.order); - } - if (body.count) { - options.count = true; - } - if (typeof body.keys == 'string') { - options.keys = body.keys; - } - if (body.include) { - options.include = String(body.include); - } - if (body.redirectClassNameForKey) { - options.redirectClassNameForKey = String(body.redirectClassNameForKey); - } - if (typeof body.where === 'string') { - body.where = JSON.parse(body.where); - } - return _rest2.default.find(req.config, req.auth, req.params.className, body.where, options, req.info.clientSDK).then(function (response) { - if (response && response.results) { - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = response.results[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var result = _step2.value; - - if (result.sessionToken) { - result.sessionToken = req.info.sessionToken || result.sessionToken; - } - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2.return) { - _iterator2.return(); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - } - return { response: response }; - }); - } - - // Returns a promise for a {response} object. - - }, { - key: 'handleGet', - value: function handleGet(req) { - var body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query)); - var options = {}; - - var _iteratorNormalCompletion3 = true; - var _didIteratorError3 = false; - var _iteratorError3 = undefined; - - try { - for (var _iterator3 = Object.keys(body)[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { - var key = _step3.value; - - if (ALLOWED_GET_QUERY_KEYS.indexOf(key) === -1) { - throw new _node2.default.Error(_node2.default.Error.INVALID_QUERY, 'Improper encode of parameter'); - } - } - } catch (err) { - _didIteratorError3 = true; - _iteratorError3 = err; - } finally { - try { - if (!_iteratorNormalCompletion3 && _iterator3.return) { - _iterator3.return(); - } - } finally { - if (_didIteratorError3) { - throw _iteratorError3; - } - } - } - - if (typeof body.keys == 'string') { - options.keys = body.keys; - } - if (body.include) { - options.include = String(body.include); - } - - return _rest2.default.get(req.config, req.auth, req.params.className, req.params.objectId, options, req.info.clientSDK).then(function (response) { - if (!response.results || response.results.length == 0) { - throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Object not found.'); - } - - if (req.params.className === "_User") { - - delete response.results[0].sessionToken; - - var user = response.results[0]; - - if (req.auth.user && user.objectId == req.auth.user.id) { - // Force the session token - response.results[0].sessionToken = req.info.sessionToken; - } - } - return { response: response.results[0] }; - }); - } - }, { - key: 'handleCreate', - value: function handleCreate(req) { - return _rest2.default.create(req.config, req.auth, req.params.className, req.body, req.info.clientSDK); - } - }, { - key: 'handleUpdate', - value: function handleUpdate(req) { - return _rest2.default.update(req.config, req.auth, req.params.className, req.params.objectId, req.body, req.info.clientSDK); - } - }, { - key: 'handleDelete', - value: function handleDelete(req) { - return _rest2.default.del(req.config, req.auth, req.params.className, req.params.objectId, req.info.clientSDK).then(function () { - return { response: {} }; - }); - } - }, { - key: 'mountRoutes', - value: function mountRoutes() { - var _this2 = this; - - this.route('GET', '/classes/:className', function (req) { - return _this2.handleFind(req); - }); - this.route('GET', '/classes/:className/:objectId', function (req) { - return _this2.handleGet(req); - }); - this.route('POST', '/classes/:className', function (req) { - return _this2.handleCreate(req); - }); - this.route('PUT', '/classes/:className/:objectId', function (req) { - return _this2.handleUpdate(req); - }); - this.route('DELETE', '/classes/:className/:objectId', function (req) { - return _this2.handleDelete(req); - }); - } - }], [{ - key: 'JSONFromQuery', - value: function JSONFromQuery(query) { - var json = {}; - var _iteratorNormalCompletion4 = true; - var _didIteratorError4 = false; - var _iteratorError4 = undefined; - - try { - for (var _iterator4 = _lodash2.default.entries(query)[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { - var _step4$value = _slicedToArray(_step4.value, 2), - key = _step4$value[0], - value = _step4$value[1]; - - try { - json[key] = JSON.parse(value); - } catch (e) { - json[key] = value; - } - } - } catch (err) { - _didIteratorError4 = true; - _iteratorError4 = err; - } finally { - try { - if (!_iteratorNormalCompletion4 && _iterator4.return) { - _iterator4.return(); - } - } finally { - if (_didIteratorError4) { - throw _iteratorError4; - } - } - } - - return json; - } - }]); - - return ClassesRouter; -}(_PromiseRouter3.default); - -exports.default = ClassesRouter; \ No newline at end of file diff --git a/lib/Routers/CloudCodeRouter.js b/lib/Routers/CloudCodeRouter.js deleted file mode 100644 index 9ca85d9203..0000000000 --- a/lib/Routers/CloudCodeRouter.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.CloudCodeRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var triggers = require('../triggers'); - -var CloudCodeRouter = exports.CloudCodeRouter = function (_PromiseRouter) { - _inherits(CloudCodeRouter, _PromiseRouter); - - function CloudCodeRouter() { - _classCallCheck(this, CloudCodeRouter); - - return _possibleConstructorReturn(this, (CloudCodeRouter.__proto__ || Object.getPrototypeOf(CloudCodeRouter)).apply(this, arguments)); - } - - _createClass(CloudCodeRouter, [{ - key: 'mountRoutes', - value: function mountRoutes() { - this.route('GET', '/cloud_code/jobs', CloudCodeRouter.getJobs); - } - }], [{ - key: 'getJobs', - value: function getJobs(req) { - var config = req.config; - var jobs = triggers.getJobs(config.applicationId) || {}; - return Promise.resolve({ - response: Object.keys(jobs).map(function (jobName) { - return { - jobName: jobName - }; - }) - }); - } - }]); - - return CloudCodeRouter; -}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/FeaturesRouter.js b/lib/Routers/FeaturesRouter.js deleted file mode 100644 index 683c9592b6..0000000000 --- a/lib/Routers/FeaturesRouter.js +++ /dev/null @@ -1,93 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.FeaturesRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _package = require('../../package.json'); - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _middlewares = require('../middlewares'); - -var middleware = _interopRequireWildcard(_middlewares); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var FeaturesRouter = exports.FeaturesRouter = function (_PromiseRouter) { - _inherits(FeaturesRouter, _PromiseRouter); - - function FeaturesRouter() { - _classCallCheck(this, FeaturesRouter); - - return _possibleConstructorReturn(this, (FeaturesRouter.__proto__ || Object.getPrototypeOf(FeaturesRouter)).apply(this, arguments)); - } - - _createClass(FeaturesRouter, [{ - key: 'mountRoutes', - value: function mountRoutes() { - this.route('GET', '/serverInfo', middleware.promiseEnforceMasterKeyAccess, function (req) { - var 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: false - }, - schemas: { - addField: true, - removeField: true, - addClass: true, - removeClass: true, - clearAllDataFromClass: true, - exportClass: false, - editClassLevelPermissions: true, - editPointerPermissions: true - } - }; - - return { response: { - features: features, - parseServerVersion: _package.version - } }; - }); - } - }]); - - return FeaturesRouter; -}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/FilesRouter.js b/lib/Routers/FilesRouter.js deleted file mode 100644 index e25c29a3b7..0000000000 --- a/lib/Routers/FilesRouter.js +++ /dev/null @@ -1,234 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.FilesRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _express = require('express'); - -var _express2 = _interopRequireDefault(_express); - -var _bodyParser = require('body-parser'); - -var _bodyParser2 = _interopRequireDefault(_bodyParser); - -var _middlewares = require('../middlewares'); - -var Middlewares = _interopRequireWildcard(_middlewares); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _Config = require('../Config'); - -var _Config2 = _interopRequireDefault(_Config); - -var _mime = require('mime'); - -var _mime2 = _interopRequireDefault(_mime); - -var _logger = require('../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var FilesRouter = exports.FilesRouter = function () { - function FilesRouter() { - _classCallCheck(this, FilesRouter); - } - - _createClass(FilesRouter, [{ - key: 'expressRouter', - value: function expressRouter() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - var router = _express2.default.Router(); - router.get('/files/:appId/:filename', this.getHandler); - - router.post('/files', function (req, res, next) { - next(new _node2.default.Error(_node2.default.Error.INVALID_FILE_NAME, 'Filename not provided.')); - }); - - router.post('/files/:filename', Middlewares.allowCrossDomain, _bodyParser2.default.raw({ type: function type() { - return true; - }, limit: options.maxUploadSize || '20mb' }), // Allow uploads without Content-Type, or with any Content-Type. - Middlewares.handleParseHeaders, this.createHandler); - - router.delete('/files/:filename', Middlewares.allowCrossDomain, Middlewares.handleParseHeaders, Middlewares.enforceMasterKeyAccess, this.deleteHandler); - return router; - } - }, { - key: 'getHandler', - value: function getHandler(req, res) { - var config = new _Config2.default(req.params.appId); - var filesController = config.filesController; - var filename = req.params.filename; - var contentType = _mime2.default.lookup(filename); - if (isFileStreamable(req, filesController)) { - filesController.getFileStream(config, filename).then(function (stream) { - handleFileStream(stream, req, res, contentType); - }).catch(function () { - res.status(404); - res.set('Content-Type', 'text/plain'); - res.end('File not found.'); - }); - } else { - filesController.getFileData(config, filename).then(function (data) { - res.status(200); - res.set('Content-Type', contentType); - res.set('Content-Length', data.length); - res.end(data); - }).catch(function () { - res.status(404); - res.set('Content-Type', 'text/plain'); - res.end('File not found.'); - }); - } - } - }, { - key: 'createHandler', - value: function createHandler(req, res, next) { - if (!req.body || !req.body.length) { - next(new _node2.default.Error(_node2.default.Error.FILE_SAVE_ERROR, 'Invalid file upload.')); - return; - } - - if (req.params.filename.length > 128) { - next(new _node2.default.Error(_node2.default.Error.INVALID_FILE_NAME, 'Filename too long.')); - return; - } - - if (!req.params.filename.match(/^[_a-zA-Z0-9][a-zA-Z0-9@\.\ ~_-]*$/)) { - next(new _node2.default.Error(_node2.default.Error.INVALID_FILE_NAME, 'Filename contains invalid characters.')); - return; - } - - var filename = req.params.filename; - var contentType = req.get('Content-type'); - var config = req.config; - var filesController = config.filesController; - - filesController.createFile(config, filename, req.body, contentType).then(function (result) { - res.status(201); - res.set('Location', result.url); - res.json(result); - }).catch(function (e) { - _logger2.default.error(e.message, e); - next(new _node2.default.Error(_node2.default.Error.FILE_SAVE_ERROR, 'Could not store file.')); - }); - } - }, { - key: 'deleteHandler', - value: function deleteHandler(req, res, next) { - var filesController = req.config.filesController; - filesController.deleteFile(req.config, req.params.filename).then(function () { - res.status(200); - // TODO: return useful JSON here? - res.end(); - }).catch(function () { - next(new _node2.default.Error(_node2.default.Error.FILE_DELETE_ERROR, 'Could not delete file.')); - }); - } - }]); - - return FilesRouter; -}(); - -function isFileStreamable(req, filesController) { - if (req.get('Range')) { - if (!(typeof filesController.adapter.getFileStream === 'function')) { - return false; - } - if (typeof filesController.adapter.constructor.name !== 'undefined') { - if (filesController.adapter.constructor.name == 'GridStoreAdapter') { - return true; - } - } - } - return false; -} - -// handleFileStream is licenced under Creative Commons Attribution 4.0 International License (https://creativecommons.org/licenses/by/4.0/). -// Author: LEROIB at weightingformypizza (https://weightingformypizza.wordpress.com/2015/06/24/stream-html5-media-content-like-video-audio-from-mongodb-using-express-and-gridstore/). -function handleFileStream(stream, req, res, contentType) { - var buffer_size = 1024 * 1024; //1024Kb - // Range request, partiall stream the file - var parts = req.get('Range').replace(/bytes=/, "").split("-"); - var partialstart = parts[0]; - var partialend = parts[1]; - var start = partialstart ? parseInt(partialstart, 10) : 0; - var end = partialend ? parseInt(partialend, 10) : stream.length - 1; - var chunksize = end - start + 1; - - if (chunksize == 1) { - start = 0; - partialend = false; - } - - if (!partialend) { - if (stream.length - 1 - start < buffer_size) { - end = stream.length - 1; - } else { - end = start + buffer_size; - } - chunksize = end - start + 1; - } - - if (start == 0 && end == 2) { - chunksize = 1; - } - - res.writeHead(206, { - 'Content-Range': 'bytes ' + start + '-' + end + '/' + stream.length, - 'Accept-Ranges': 'bytes', - 'Content-Length': chunksize, - 'Content-Type': contentType - }); - - stream.seek(start, function () { - // get gridFile stream - var gridFileStream = stream.stream(true); - var bufferAvail = 0; - var range = end - start + 1; - var totalbyteswanted = end - start + 1; - var totalbyteswritten = 0; - // write to response - gridFileStream.on('data', function (buff) { - bufferAvail += buff.length; - //Ok check if we have enough to cover our range - if (bufferAvail < range) { - //Not enough bytes to satisfy our full range - if (bufferAvail > 0) { - //Write full buffer - res.write(buff); - totalbyteswritten += buff.length; - range -= buff.length; - bufferAvail -= buff.length; - } - } else { - //Enough bytes to satisfy our full range! - if (bufferAvail > 0) { - var buffer = buff.slice(0, range); - res.write(buffer); - totalbyteswritten += buffer.length; - bufferAvail -= range; - } - } - if (totalbyteswritten >= totalbyteswanted) { - //totalbytes = 0; - stream.close(); - res.end(); - this.destroy(); - } - }); - }); -} \ No newline at end of file diff --git a/lib/Routers/FunctionsRouter.js b/lib/Routers/FunctionsRouter.js deleted file mode 100644 index 961c5c38fb..0000000000 --- a/lib/Routers/FunctionsRouter.js +++ /dev/null @@ -1,205 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.FunctionsRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _middlewares = require('../middlewares'); - -var _StatusHandler = require('../StatusHandler'); - -var _lodash = require('lodash'); - -var _lodash2 = _interopRequireDefault(_lodash); - -var _logger = require('../logger'); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -// FunctionsRouter.js - -var Parse = require('parse/node').Parse, - triggers = require('../triggers'); - -function parseObject(obj) { - if (Array.isArray(obj)) { - return obj.map(function (item) { - return parseObject(item); - }); - } else if (obj && obj.__type == 'Date') { - return Object.assign(new Date(obj.iso), obj); - } else if (obj && obj.__type == 'File') { - return Parse.File.fromJSON(obj); - } else if (obj && (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object') { - return parseParams(obj); - } else { - return obj; - } -} - -function parseParams(params) { - return _lodash2.default.mapValues(params, parseObject); -} - -var FunctionsRouter = exports.FunctionsRouter = function (_PromiseRouter) { - _inherits(FunctionsRouter, _PromiseRouter); - - function FunctionsRouter() { - _classCallCheck(this, FunctionsRouter); - - return _possibleConstructorReturn(this, (FunctionsRouter.__proto__ || Object.getPrototypeOf(FunctionsRouter)).apply(this, arguments)); - } - - _createClass(FunctionsRouter, [{ - key: 'mountRoutes', - value: function mountRoutes() { - this.route('POST', '/functions/:functionName', FunctionsRouter.handleCloudFunction); - this.route('POST', '/jobs/:jobName', _middlewares.promiseEnforceMasterKeyAccess, function (req) { - return FunctionsRouter.handleCloudJob(req); - }); - this.route('POST', '/jobs', _middlewares.promiseEnforceMasterKeyAccess, function (req) { - return FunctionsRouter.handleCloudJob(req); - }); - } - }], [{ - key: 'handleCloudJob', - value: function handleCloudJob(req) { - var jobName = req.params.jobName || req.body.jobName; - var applicationId = req.config.applicationId; - var jobHandler = (0, _StatusHandler.jobStatusHandler)(req.config); - var jobFunction = triggers.getJob(jobName, applicationId); - if (!jobFunction) { - throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid job.'); - } - var params = Object.assign({}, req.body, req.query); - params = parseParams(params); - var request = { - params: params, - log: req.config.loggerController, - headers: req.headers, - jobName: jobName - }; - var status = { - success: jobHandler.setSucceeded.bind(jobHandler), - error: jobHandler.setFailed.bind(jobHandler), - message: jobHandler.setMessage.bind(jobHandler) - }; - return jobHandler.setRunning(jobName, params).then(function (jobStatus) { - request.jobId = jobStatus.objectId; - // run the function async - process.nextTick(function () { - jobFunction(request, status); - }); - return { - headers: { - 'X-Parse-Job-Status-Id': jobStatus.objectId - }, - response: {} - }; - }); - } - }, { - key: 'createResponseObject', - value: function createResponseObject(resolve, reject, message) { - return { - success: function success(result) { - resolve({ - response: { - result: Parse._encode(result) - } - }); - }, - error: function error(code, message) { - if (!message) { - message = code; - code = Parse.Error.SCRIPT_FAILED; - } - reject(new Parse.Error(code, message)); - }, - message: message - }; - } - }, { - key: 'handleCloudFunction', - value: function handleCloudFunction(req) { - var functionName = req.params.functionName; - var applicationId = req.config.applicationId; - var theFunction = triggers.getFunction(functionName, applicationId); - var theValidator = triggers.getValidator(req.params.functionName, applicationId); - if (theFunction) { - var params = Object.assign({}, req.body, req.query); - params = parseParams(params); - var request = { - params: params, - master: req.auth && req.auth.isMaster, - user: req.auth && req.auth.user, - installationId: req.info.installationId, - log: req.config.loggerController, - headers: req.headers, - functionName: functionName - }; - - if (theValidator && typeof theValidator === "function") { - var result = theValidator(request); - if (!result) { - throw new Parse.Error(Parse.Error.VALIDATION_ERROR, 'Validation failed.'); - } - } - - return new Promise(function (resolve, reject) { - var userString = req.auth && req.auth.user ? req.auth.user.id : undefined; - var cleanInput = _logger.logger.truncateLogMessage(JSON.stringify(params)); - var response = FunctionsRouter.createResponseObject(function (result) { - try { - var cleanResult = _logger.logger.truncateLogMessage(JSON.stringify(result.response.result)); - _logger.logger.info('Ran cloud function ' + functionName + ' for user ' + userString + ' ' + ('with:\n Input: ' + cleanInput + '\n Result: ' + cleanResult), { - functionName: functionName, - params: params, - user: userString - }); - resolve(result); - } catch (e) { - reject(e); - } - }, function (error) { - try { - _logger.logger.error('Failed running cloud function ' + functionName + ' for ' + ('user ' + userString + ' with:\n Input: ' + cleanInput + '\n Error: ') + JSON.stringify(error), { - functionName: functionName, - error: error, - params: params, - user: userString - }); - reject(error); - } catch (e) { - reject(e); - } - }); - // Force the keys before the function calls. - Parse.applicationId = req.config.applicationId; - Parse.javascriptKey = req.config.javascriptKey; - Parse.masterKey = req.config.masterKey; - theFunction(request, response); - }); - } else { - throw new Parse.Error(Parse.Error.SCRIPT_FAILED, 'Invalid function: "' + functionName + '"'); - } - } - }]); - - return FunctionsRouter; -}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/GlobalConfigRouter.js b/lib/Routers/GlobalConfigRouter.js deleted file mode 100644 index 54ff2c3b8f..0000000000 --- a/lib/Routers/GlobalConfigRouter.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.GlobalConfigRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _middlewares = require('../middlewares'); - -var middleware = _interopRequireWildcard(_middlewares); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // global_config.js - -var GlobalConfigRouter = exports.GlobalConfigRouter = function (_PromiseRouter) { - _inherits(GlobalConfigRouter, _PromiseRouter); - - function GlobalConfigRouter() { - _classCallCheck(this, GlobalConfigRouter); - - return _possibleConstructorReturn(this, (GlobalConfigRouter.__proto__ || Object.getPrototypeOf(GlobalConfigRouter)).apply(this, arguments)); - } - - _createClass(GlobalConfigRouter, [{ - key: 'getGlobalConfig', - value: function getGlobalConfig(req) { - return req.config.database.find('_GlobalConfig', { objectId: "1" }, { limit: 1 }).then(function (results) { - if (results.length != 1) { - // If there is no config in the database - return empty config. - return { response: { params: {} } }; - } - var globalConfig = results[0]; - return { response: { params: globalConfig.params } }; - }); - } - }, { - key: 'updateGlobalConfig', - value: function updateGlobalConfig(req) { - var params = req.body.params; - // Transform in dot notation to make sure it works - var update = Object.keys(params).reduce(function (acc, key) { - acc['params.' + key] = params[key]; - return acc; - }, {}); - return req.config.database.update('_GlobalConfig', { objectId: "1" }, update, { upsert: true }).then(function () { - return { response: { result: true } }; - }); - } - }, { - key: 'mountRoutes', - value: function mountRoutes() { - var _this2 = this; - - this.route('GET', '/config', function (req) { - return _this2.getGlobalConfig(req); - }); - this.route('PUT', '/config', middleware.promiseEnforceMasterKeyAccess, function (req) { - return _this2.updateGlobalConfig(req); - }); - } - }]); - - return GlobalConfigRouter; -}(_PromiseRouter3.default); - -exports.default = GlobalConfigRouter; \ No newline at end of file diff --git a/lib/Routers/HooksRouter.js b/lib/Routers/HooksRouter.js deleted file mode 100644 index f196b5d73d..0000000000 --- a/lib/Routers/HooksRouter.js +++ /dev/null @@ -1,155 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.HooksRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _node = require('parse/node'); - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _middlewares = require('../middlewares'); - -var middleware = _interopRequireWildcard(_middlewares); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var HooksRouter = exports.HooksRouter = function (_PromiseRouter) { - _inherits(HooksRouter, _PromiseRouter); - - function HooksRouter() { - _classCallCheck(this, HooksRouter); - - return _possibleConstructorReturn(this, (HooksRouter.__proto__ || Object.getPrototypeOf(HooksRouter)).apply(this, arguments)); - } - - _createClass(HooksRouter, [{ - key: 'createHook', - value: function createHook(aHook, config) { - return config.hooksController.createHook(aHook).then(function (hook) { - return { response: hook }; - }); - } - }, { - key: 'updateHook', - value: function updateHook(aHook, config) { - return config.hooksController.updateHook(aHook).then(function (hook) { - return { response: hook }; - }); - } - }, { - key: 'handlePost', - value: function handlePost(req) { - return this.createHook(req.body, req.config); - } - }, { - key: 'handleGetFunctions', - value: function handleGetFunctions(req) { - var hooksController = req.config.hooksController; - if (req.params.functionName) { - return hooksController.getFunction(req.params.functionName).then(function (foundFunction) { - if (!foundFunction) { - throw new _node.Parse.Error(143, 'no function named: ' + req.params.functionName + ' is defined'); - } - return Promise.resolve({ response: foundFunction }); - }); - } - - return hooksController.getFunctions().then(function (functions) { - return { response: functions || [] }; - }, function (err) { - throw err; - }); - } - }, { - key: 'handleGetTriggers', - value: function handleGetTriggers(req) { - var hooksController = req.config.hooksController; - if (req.params.className && req.params.triggerName) { - - return hooksController.getTrigger(req.params.className, req.params.triggerName).then(function (foundTrigger) { - if (!foundTrigger) { - throw new _node.Parse.Error(143, 'class ' + req.params.className + ' does not exist'); - } - return Promise.resolve({ response: foundTrigger }); - }); - } - - return hooksController.getTriggers().then(function (triggers) { - return { response: triggers || [] }; - }); - } - }, { - key: 'handleDelete', - value: function handleDelete(req) { - var hooksController = req.config.hooksController; - if (req.params.functionName) { - return hooksController.deleteFunction(req.params.functionName).then(function () { - return { response: {} }; - }); - } else if (req.params.className && req.params.triggerName) { - return hooksController.deleteTrigger(req.params.className, req.params.triggerName).then(function () { - return { response: {} }; - }); - } - return Promise.resolve({ response: {} }); - } - }, { - key: 'handleUpdate', - value: function handleUpdate(req) { - var hook; - if (req.params.functionName && req.body.url) { - hook = {}; - hook.functionName = req.params.functionName; - hook.url = req.body.url; - } else if (req.params.className && req.params.triggerName && req.body.url) { - hook = {}; - hook.className = req.params.className; - hook.triggerName = req.params.triggerName; - hook.url = req.body.url; - } else { - throw new _node.Parse.Error(143, "invalid hook declaration"); - } - return this.updateHook(hook, req.config); - } - }, { - key: 'handlePut', - value: function handlePut(req) { - var body = req.body; - if (body.__op == "Delete") { - return this.handleDelete(req); - } else { - return this.handleUpdate(req); - } - } - }, { - key: 'mountRoutes', - value: function 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)); - } - }]); - - return HooksRouter; -}(_PromiseRouter3.default); - -exports.default = HooksRouter; \ No newline at end of file diff --git a/lib/Routers/IAPValidationRouter.js b/lib/Routers/IAPValidationRouter.js deleted file mode 100644 index 534cb7e7de..0000000000 --- a/lib/Routers/IAPValidationRouter.js +++ /dev/null @@ -1,147 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.IAPValidationRouter = undefined; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _PromiseRouter2 = require("../PromiseRouter"); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _node = require("parse/node"); - -var _node2 = _interopRequireDefault(_node); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var request = require("request"); -var rest = require("../rest"); - - -// TODO move validation logic in IAPValidationController -var IAP_SANDBOX_URL = "https://sandbox.itunes.apple.com/verifyReceipt"; -var IAP_PRODUCTION_URL = "https://buy.itunes.apple.com/verifyReceipt"; - -var 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." -}; - -function appStoreError(status) { - status = parseInt(status); - 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(); - } - // 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) { - var products = result.results; - if (!products || products.length != 1) { - // Error not found or too many - throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Object not found.'); - } - - var download = products[0].download; - return Promise.resolve({ response: download }); - }); -} - -var IAPValidationRouter = exports.IAPValidationRouter = function (_PromiseRouter) { - _inherits(IAPValidationRouter, _PromiseRouter); - - function IAPValidationRouter() { - _classCallCheck(this, IAPValidationRouter); - - return _possibleConstructorReturn(this, (IAPValidationRouter.__proto__ || Object.getPrototypeOf(IAPValidationRouter)).apply(this, arguments)); - } - - _createClass(IAPValidationRouter, [{ - key: "handleRequest", - value: function handleRequest(req) { - var receipt = req.body.receipt; - var productIdentifier = req.body.productIdentifier; - - if (!receipt || !productIdentifier) { - // TODO: Error, malformed request - throw new _node2.default.Error(_node2.default.Error.INVALID_JSON, "missing receipt or productIdentifier"); - } - - // Transform the object if there - // otherwise assume it's in Base64 already - if ((typeof receipt === "undefined" ? "undefined" : _typeof(receipt)) == "object") { - if (receipt["__type"] == "Bytes") { - receipt = receipt.base64; - } - } - - if (process.env.NODE_ENV == "test" && req.body.bypassAppStoreValidation) { - return getFileForProductIdentifier(productIdentifier, req); - } - - function successCallback() { - return getFileForProductIdentifier(productIdentifier, req); - } - - function errorCallback(error) { - return Promise.resolve({ response: appStoreError(error.status) }); - } - - return validateWithAppStore(IAP_PRODUCTION_URL, receipt).then(function () { - - return successCallback(); - }, function (error) { - if (error.status == 21007) { - return validateWithAppStore(IAP_SANDBOX_URL, receipt).then(function () { - return successCallback(); - }, function (error) { - return errorCallback(error); - }); - } - - return errorCallback(error); - }); - } - }, { - key: "mountRoutes", - value: function mountRoutes() { - this.route("POST", "/validate_purchase", this.handleRequest); - } - }]); - - return IAPValidationRouter; -}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/InstallationsRouter.js b/lib/Routers/InstallationsRouter.js deleted file mode 100644 index 6898c47238..0000000000 --- a/lib/Routers/InstallationsRouter.js +++ /dev/null @@ -1,113 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.InstallationsRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - -var _ClassesRouter2 = require('./ClassesRouter'); - -var _ClassesRouter3 = _interopRequireDefault(_ClassesRouter2); - -var _rest = require('../rest'); - -var _rest2 = _interopRequireDefault(_rest); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // InstallationsRouter.js - -var InstallationsRouter = exports.InstallationsRouter = function (_ClassesRouter) { - _inherits(InstallationsRouter, _ClassesRouter); - - function InstallationsRouter() { - _classCallCheck(this, InstallationsRouter); - - return _possibleConstructorReturn(this, (InstallationsRouter.__proto__ || Object.getPrototypeOf(InstallationsRouter)).apply(this, arguments)); - } - - _createClass(InstallationsRouter, [{ - key: 'handleFind', - value: function handleFind(req) { - var body = Object.assign(req.body, _ClassesRouter3.default.JSONFromQuery(req.query)); - var options = {}; - - if (body.skip) { - options.skip = Number(body.skip); - } - if (body.limit || body.limit === 0) { - options.limit = Number(body.limit); - } - if (body.order) { - options.order = String(body.order); - } - if (body.count) { - options.count = true; - } - if (body.include) { - options.include = String(body.include); - } - - return _rest2.default.find(req.config, req.auth, '_Installation', body.where, options, req.info.clientSDK).then(function (response) { - return { response: response }; - }); - } - }, { - key: 'handleGet', - value: function handleGet(req) { - req.params.className = '_Installation'; - return _get(InstallationsRouter.prototype.__proto__ || Object.getPrototypeOf(InstallationsRouter.prototype), 'handleGet', this).call(this, req); - } - }, { - key: 'handleCreate', - value: function handleCreate(req) { - req.params.className = '_Installation'; - return _get(InstallationsRouter.prototype.__proto__ || Object.getPrototypeOf(InstallationsRouter.prototype), 'handleCreate', this).call(this, req); - } - }, { - key: 'handleUpdate', - value: function handleUpdate(req) { - req.params.className = '_Installation'; - return _get(InstallationsRouter.prototype.__proto__ || Object.getPrototypeOf(InstallationsRouter.prototype), 'handleUpdate', this).call(this, req); - } - }, { - key: 'handleDelete', - value: function handleDelete(req) { - req.params.className = '_Installation'; - return _get(InstallationsRouter.prototype.__proto__ || Object.getPrototypeOf(InstallationsRouter.prototype), 'handleDelete', this).call(this, req); - } - }, { - key: 'mountRoutes', - value: function mountRoutes() { - var _this2 = this; - - this.route('GET', '/installations', function (req) { - return _this2.handleFind(req); - }); - this.route('GET', '/installations/:objectId', function (req) { - return _this2.handleGet(req); - }); - this.route('POST', '/installations', function (req) { - return _this2.handleCreate(req); - }); - this.route('PUT', '/installations/:objectId', function (req) { - return _this2.handleUpdate(req); - }); - this.route('DELETE', '/installations/:objectId', function (req) { - return _this2.handleDelete(req); - }); - } - }]); - - return InstallationsRouter; -}(_ClassesRouter3.default); - -exports.default = InstallationsRouter; \ No newline at end of file diff --git a/lib/Routers/LogsRouter.js b/lib/Routers/LogsRouter.js deleted file mode 100644 index e2378c1e81..0000000000 --- a/lib/Routers/LogsRouter.js +++ /dev/null @@ -1,96 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.LogsRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _node = require('parse/node'); - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _middlewares = require('../middlewares'); - -var middleware = _interopRequireWildcard(_middlewares); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var LogsRouter = exports.LogsRouter = function (_PromiseRouter) { - _inherits(LogsRouter, _PromiseRouter); - - function LogsRouter() { - _classCallCheck(this, LogsRouter); - - return _possibleConstructorReturn(this, (LogsRouter.__proto__ || Object.getPrototypeOf(LogsRouter)).apply(this, arguments)); - } - - _createClass(LogsRouter, [{ - key: 'mountRoutes', - value: function mountRoutes() { - var _this2 = this; - - this.route('GET', '/scriptlog', middleware.promiseEnforceMasterKeyAccess, this.validateRequest, function (req) { - return _this2.handleGET(req); - }); - } - }, { - key: 'validateRequest', - value: function validateRequest(req) { - if (!req.config || !req.config.loggerController) { - throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Logger adapter is not available'); - } - } - - // Returns a promise for a {response} object. - // query params: - // level (optional) Level of logging you want to query for (info || error) - // from (optional) Start time for the search. Defaults to 1 week ago. - // until (optional) End time for the search. Defaults to current time. - // order (optional) Direction of results returned, either “asc” or “desc”. Defaults to “desc”. - // size (optional) Number of rows returned by search. Defaults to 10 - // n same as size, overrides size if set - - }, { - key: 'handleGET', - value: function handleGET(req) { - var from = req.query.from; - var until = req.query.until; - var size = req.query.size; - if (req.query.n) { - size = req.query.n; - } - - var order = req.query.order; - var level = req.query.level; - var options = { - from: from, - until: until, - size: size, - order: order, - level: level - }; - - return req.config.loggerController.getLogs(options).then(function (result) { - return Promise.resolve({ - response: result - }); - }); - } - }]); - - return LogsRouter; -}(_PromiseRouter3.default); - -exports.default = LogsRouter; \ No newline at end of file diff --git a/lib/Routers/PublicAPIRouter.js b/lib/Routers/PublicAPIRouter.js deleted file mode 100644 index 9b62c7295b..0000000000 --- a/lib/Routers/PublicAPIRouter.js +++ /dev/null @@ -1,285 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.PublicAPIRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _Config = require('../Config'); - -var _Config2 = _interopRequireDefault(_Config); - -var _express = require('express'); - -var _express2 = _interopRequireDefault(_express); - -var _path = require('path'); - -var _path2 = _interopRequireDefault(_path); - -var _fs = require('fs'); - -var _fs2 = _interopRequireDefault(_fs); - -var _querystring = require('querystring'); - -var _querystring2 = _interopRequireDefault(_querystring); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var public_html = _path2.default.resolve(__dirname, "../../public_html"); -var views = _path2.default.resolve(__dirname, '../../views'); - -var PublicAPIRouter = exports.PublicAPIRouter = function (_PromiseRouter) { - _inherits(PublicAPIRouter, _PromiseRouter); - - function PublicAPIRouter() { - _classCallCheck(this, PublicAPIRouter); - - return _possibleConstructorReturn(this, (PublicAPIRouter.__proto__ || Object.getPrototypeOf(PublicAPIRouter)).apply(this, arguments)); - } - - _createClass(PublicAPIRouter, [{ - key: 'verifyEmail', - value: function verifyEmail(req) { - var _this2 = this; - - var _req$query = req.query, - token = _req$query.token, - username = _req$query.username; - - var appId = req.params.appId; - var config = new _Config2.default(appId); - - if (!config.publicServerURL) { - return this.missingPublicServerURL(); - } - - if (!token || !username) { - return this.invalidLink(req); - } - - var userController = config.userController; - return userController.verifyEmail(username, token).then(function () { - var params = _querystring2.default.stringify({ username: username }); - return Promise.resolve({ - status: 302, - location: config.verifyEmailSuccessURL + '?' + params - }); - }, function () { - return _this2.invalidVerificationLink(req); - }); - } - }, { - key: 'resendVerificationEmail', - value: function resendVerificationEmail(req) { - var username = req.body.username; - var appId = req.params.appId; - var config = new _Config2.default(appId); - - if (!config.publicServerURL) { - return this.missingPublicServerURL(); - } - - if (!username) { - return this.invalidLink(req); - } - - var userController = config.userController; - - return userController.resendVerificationEmail(username).then(function () { - return Promise.resolve({ - status: 302, - location: '' + config.linkSendSuccessURL - }); - }, function () { - return Promise.resolve({ - status: 302, - location: '' + config.linkSendFailURL - }); - }); - } - }, { - key: 'changePassword', - value: function changePassword(req) { - return new Promise(function (resolve, reject) { - var config = new _Config2.default(req.query.id); - if (!config.publicServerURL) { - return resolve({ - status: 404, - text: 'Not found.' - }); - } - // Should we keep the file in memory or leave like that? - _fs2.default.readFile(_path2.default.resolve(views, "choose_password"), 'utf-8', function (err, data) { - if (err) { - return reject(err); - } - data = data.replace("PARSE_SERVER_URL", '\'' + config.publicServerURL + '\''); - resolve({ - text: data - }); - }); - }); - } - }, { - key: 'requestResetPassword', - value: function requestResetPassword(req) { - var _this3 = this; - - var config = req.config; - - if (!config.publicServerURL) { - return this.missingPublicServerURL(); - } - - var _req$query2 = req.query, - username = _req$query2.username, - token = _req$query2.token; - - - if (!username || !token) { - return this.invalidLink(req); - } - - return config.userController.checkResetTokenValidity(username, token).then(function () { - var params = _querystring2.default.stringify({ token: token, id: config.applicationId, username: username, app: config.appName }); - return Promise.resolve({ - status: 302, - location: config.choosePasswordURL + '?' + params - }); - }, function () { - return _this3.invalidLink(req); - }); - } - }, { - key: 'resetPassword', - value: function resetPassword(req) { - - var config = req.config; - - if (!config.publicServerURL) { - return this.missingPublicServerURL(); - } - - var _req$body = req.body, - username = _req$body.username, - token = _req$body.token, - new_password = _req$body.new_password; - - - if (!username || !token || !new_password) { - return this.invalidLink(req); - } - - return config.userController.updatePassword(username, token, new_password).then(function () { - var params = _querystring2.default.stringify({ username: username }); - return Promise.resolve({ - status: 302, - location: config.passwordResetSuccessURL + '?' + params - }); - }, function (err) { - var params = _querystring2.default.stringify({ username: username, token: token, id: config.applicationId, error: err, app: config.appName }); - return Promise.resolve({ - status: 302, - location: config.choosePasswordURL + '?' + params - }); - }); - } - }, { - key: 'invalidLink', - value: function invalidLink(req) { - return Promise.resolve({ - status: 302, - location: req.config.invalidLinkURL - }); - } - }, { - key: 'invalidVerificationLink', - value: function invalidVerificationLink(req) { - var config = req.config; - if (req.query.username && req.params.appId) { - var params = _querystring2.default.stringify({ username: req.query.username, appId: req.params.appId }); - return Promise.resolve({ - status: 302, - location: config.invalidVerificationLinkURL + '?' + params - }); - } else { - return this.invalidLink(req); - } - } - }, { - key: 'missingPublicServerURL', - value: function missingPublicServerURL() { - return Promise.resolve({ - text: 'Not found.', - status: 404 - }); - } - }, { - key: 'setConfig', - value: function setConfig(req) { - req.config = new _Config2.default(req.params.appId); - return Promise.resolve(); - } - }, { - key: 'mountRoutes', - value: function mountRoutes() { - var _this4 = this; - - this.route('GET', '/apps/:appId/verify_email', function (req) { - _this4.setConfig(req); - }, function (req) { - return _this4.verifyEmail(req); - }); - - this.route('POST', '/apps/:appId/resend_verification_email', function (req) { - _this4.setConfig(req); - }, function (req) { - return _this4.resendVerificationEmail(req); - }); - - this.route('GET', '/apps/choose_password', function (req) { - return _this4.changePassword(req); - }); - - this.route('POST', '/apps/:appId/request_password_reset', function (req) { - _this4.setConfig(req); - }, function (req) { - return _this4.resetPassword(req); - }); - - this.route('GET', '/apps/:appId/request_password_reset', function (req) { - _this4.setConfig(req); - }, function (req) { - return _this4.requestResetPassword(req); - }); - } - }, { - key: 'expressRouter', - value: function expressRouter() { - var router = _express2.default.Router(); - router.use("/apps", _express2.default.static(public_html)); - router.use("/", _get(PublicAPIRouter.prototype.__proto__ || Object.getPrototypeOf(PublicAPIRouter.prototype), 'expressRouter', this).call(this)); - return router; - } - }]); - - return PublicAPIRouter; -}(_PromiseRouter3.default); - -exports.default = PublicAPIRouter; \ No newline at end of file diff --git a/lib/Routers/PurgeRouter.js b/lib/Routers/PurgeRouter.js deleted file mode 100644 index 1d7b853821..0000000000 --- a/lib/Routers/PurgeRouter.js +++ /dev/null @@ -1,64 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.PurgeRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _middlewares = require('../middlewares'); - -var middleware = _interopRequireWildcard(_middlewares); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var PurgeRouter = exports.PurgeRouter = function (_PromiseRouter) { - _inherits(PurgeRouter, _PromiseRouter); - - function PurgeRouter() { - _classCallCheck(this, PurgeRouter); - - return _possibleConstructorReturn(this, (PurgeRouter.__proto__ || Object.getPrototypeOf(PurgeRouter)).apply(this, arguments)); - } - - _createClass(PurgeRouter, [{ - key: 'handlePurge', - value: function handlePurge(req) { - return req.config.database.purgeCollection(req.params.className).then(function () { - var cacheAdapter = req.config.cacheController; - if (req.params.className == '_Session') { - cacheAdapter.user.clear(); - } else if (req.params.className == '_Role') { - cacheAdapter.role.clear(); - } - return { response: {} }; - }); - } - }, { - key: 'mountRoutes', - value: function mountRoutes() { - var _this2 = this; - - this.route('DELETE', '/purge/:className', middleware.promiseEnforceMasterKeyAccess, function (req) { - return _this2.handlePurge(req); - }); - } - }]); - - return PurgeRouter; -}(_PromiseRouter3.default); - -exports.default = PurgeRouter; \ No newline at end of file diff --git a/lib/Routers/PushRouter.js b/lib/Routers/PushRouter.js deleted file mode 100644 index e6d37e706b..0000000000 --- a/lib/Routers/PushRouter.js +++ /dev/null @@ -1,104 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.PushRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _PromiseRouter2 = require("../PromiseRouter"); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _middlewares = require("../middlewares"); - -var middleware = _interopRequireWildcard(_middlewares); - -var _node = require("parse/node"); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var PushRouter = exports.PushRouter = function (_PromiseRouter) { - _inherits(PushRouter, _PromiseRouter); - - function PushRouter() { - _classCallCheck(this, PushRouter); - - return _possibleConstructorReturn(this, (PushRouter.__proto__ || Object.getPrototypeOf(PushRouter)).apply(this, arguments)); - } - - _createClass(PushRouter, [{ - key: "mountRoutes", - value: function mountRoutes() { - this.route("POST", "/push", middleware.promiseEnforceMasterKeyAccess, PushRouter.handlePOST); - } - }], [{ - key: "handlePOST", - value: function handlePOST(req) { - var pushController = req.config.pushController; - if (!pushController) { - throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Push controller is not set'); - } - - var where = PushRouter.getQueryCondition(req); - var resolve = void 0; - var promise = new Promise(function (_resolve) { - resolve = _resolve; - }); - pushController.sendPush(req.body, where, req.config, req.auth, function (pushStatusId) { - resolve({ - headers: { - 'X-Parse-Push-Status-Id': pushStatusId - }, - response: { - result: true - } - }); - }); - return promise; - } - - /** - * Get query condition from the request body. - * @param {Object} req A request object - * @returns {Object} The query condition, the where field in a query api call - */ - - }, { - key: "getQueryCondition", - value: function getQueryCondition(req) { - var body = req.body || {}; - var hasWhere = typeof body.where !== 'undefined'; - var hasChannels = typeof body.channels !== 'undefined'; - - var where = void 0; - if (hasWhere && hasChannels) { - throw new _node.Parse.Error(_node.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 - } - }; - } else { - throw new _node.Parse.Error(_node.Parse.Error.PUSH_MISCONFIGURED, 'Sending a push requires either "channels" or a "where" query.'); - } - return where; - } - }]); - - return PushRouter; -}(_PromiseRouter3.default); - -exports.default = PushRouter; \ No newline at end of file diff --git a/lib/Routers/RolesRouter.js b/lib/Routers/RolesRouter.js deleted file mode 100644 index b9f6742511..0000000000 --- a/lib/Routers/RolesRouter.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.RolesRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - -var _ClassesRouter2 = require('./ClassesRouter'); - -var _ClassesRouter3 = _interopRequireDefault(_ClassesRouter2); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var RolesRouter = exports.RolesRouter = function (_ClassesRouter) { - _inherits(RolesRouter, _ClassesRouter); - - function RolesRouter() { - _classCallCheck(this, RolesRouter); - - return _possibleConstructorReturn(this, (RolesRouter.__proto__ || Object.getPrototypeOf(RolesRouter)).apply(this, arguments)); - } - - _createClass(RolesRouter, [{ - key: 'handleFind', - value: function handleFind(req) { - req.params.className = '_Role'; - return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleFind', this).call(this, req); - } - }, { - key: 'handleGet', - value: function handleGet(req) { - req.params.className = '_Role'; - return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleGet', this).call(this, req); - } - }, { - key: 'handleCreate', - value: function handleCreate(req) { - req.params.className = '_Role'; - return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleCreate', this).call(this, req); - } - }, { - key: 'handleUpdate', - value: function handleUpdate(req) { - req.params.className = '_Role'; - return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleUpdate', this).call(this, req); - } - }, { - key: 'handleDelete', - value: function handleDelete(req) { - req.params.className = '_Role'; - return _get(RolesRouter.prototype.__proto__ || Object.getPrototypeOf(RolesRouter.prototype), 'handleDelete', this).call(this, req); - } - }, { - key: 'mountRoutes', - value: function mountRoutes() { - var _this2 = this; - - this.route('GET', '/roles', function (req) { - return _this2.handleFind(req); - }); - this.route('GET', '/roles/:objectId', function (req) { - return _this2.handleGet(req); - }); - this.route('POST', '/roles', function (req) { - return _this2.handleCreate(req); - }); - this.route('PUT', '/roles/:objectId', function (req) { - return _this2.handleUpdate(req); - }); - this.route('DELETE', '/roles/:objectId', function (req) { - return _this2.handleDelete(req); - }); - } - }]); - - return RolesRouter; -}(_ClassesRouter3.default); - -exports.default = RolesRouter; \ No newline at end of file diff --git a/lib/Routers/SchemasRouter.js b/lib/Routers/SchemasRouter.js deleted file mode 100644 index 5a57202c24..0000000000 --- a/lib/Routers/SchemasRouter.js +++ /dev/null @@ -1,125 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.SchemasRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _PromiseRouter2 = require('../PromiseRouter'); - -var _PromiseRouter3 = _interopRequireDefault(_PromiseRouter2); - -var _middlewares = require('../middlewares'); - -var middleware = _interopRequireWildcard(_middlewares); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -// schemas.js - -var Parse = require('parse/node').Parse, - SchemaController = require('../Controllers/SchemaController'); - -function classNameMismatchResponse(bodyClass, pathClass) { - throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, 'Class name mismatch between ' + bodyClass + ' and ' + pathClass + '.'); -} - -function getAllSchemas(req) { - return req.config.database.loadSchema({ clearCache: true }).then(function (schemaController) { - return schemaController.getAllClasses(true); - }).then(function (schemas) { - return { response: { results: schemas } }; - }); -} - -function getOneSchema(req) { - var className = req.params.className; - return req.config.database.loadSchema({ clearCache: true }).then(function (schemaController) { - return schemaController.getOneSchema(className, true); - }).then(function (schema) { - return { response: schema }; - }).catch(function (error) { - if (error === undefined) { - 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.'); - } - }); -} - -function createSchema(req) { - if (req.params.className && req.body.className) { - if (req.params.className != req.body.className) { - return classNameMismatchResponse(req.body.className, req.params.className); - } - } - - var className = req.params.className || req.body.className; - if (!className) { - throw new Parse.Error(135, 'POST ' + req.path + ' needs a class name.'); - } - - return req.config.database.loadSchema({ clearCache: true }).then(function (schema) { - return schema.addClassIfNotExists(className, req.body.fields, req.body.classLevelPermissions); - }).then(function (schema) { - return { response: schema }; - }); -} - -function modifySchema(req) { - if (req.body.className && req.body.className != req.params.className) { - return classNameMismatchResponse(req.body.className, req.params.className); - } - - var submittedFields = req.body.fields || {}; - var className = req.params.className; - - return req.config.database.loadSchema({ clearCache: true }).then(function (schema) { - return schema.updateClass(className, submittedFields, req.body.classLevelPermissions, req.config.database); - }).then(function (result) { - return { response: result }; - }); -} - -var deleteSchema = function deleteSchema(req) { - if (!SchemaController.classNameIsValid(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).then(function () { - return { response: {} }; - }); -}; - -var SchemasRouter = exports.SchemasRouter = function (_PromiseRouter) { - _inherits(SchemasRouter, _PromiseRouter); - - function SchemasRouter() { - _classCallCheck(this, SchemasRouter); - - return _possibleConstructorReturn(this, (SchemasRouter.__proto__ || Object.getPrototypeOf(SchemasRouter)).apply(this, arguments)); - } - - _createClass(SchemasRouter, [{ - key: 'mountRoutes', - value: function 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); - } - }]); - - return SchemasRouter; -}(_PromiseRouter3.default); \ No newline at end of file diff --git a/lib/Routers/SessionsRouter.js b/lib/Routers/SessionsRouter.js deleted file mode 100644 index 817b8e88cf..0000000000 --- a/lib/Routers/SessionsRouter.js +++ /dev/null @@ -1,167 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.SessionsRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - -var _ClassesRouter2 = require('./ClassesRouter'); - -var _ClassesRouter3 = _interopRequireDefault(_ClassesRouter2); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _rest = require('../rest'); - -var _rest2 = _interopRequireDefault(_rest); - -var _Auth = require('../Auth'); - -var _Auth2 = _interopRequireDefault(_Auth); - -var _RestWrite = require('../RestWrite'); - -var _RestWrite2 = _interopRequireDefault(_RestWrite); - -var _cryptoUtils = require('../cryptoUtils'); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } - -var SessionsRouter = exports.SessionsRouter = function (_ClassesRouter) { - _inherits(SessionsRouter, _ClassesRouter); - - function SessionsRouter() { - _classCallCheck(this, SessionsRouter); - - return _possibleConstructorReturn(this, (SessionsRouter.__proto__ || Object.getPrototypeOf(SessionsRouter)).apply(this, arguments)); - } - - _createClass(SessionsRouter, [{ - key: 'handleFind', - value: function handleFind(req) { - req.params.className = '_Session'; - return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleFind', this).call(this, req); - } - }, { - key: 'handleGet', - value: function handleGet(req) { - req.params.className = '_Session'; - return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleGet', this).call(this, req); - } - }, { - key: 'handleCreate', - value: function handleCreate(req) { - req.params.className = '_Session'; - return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleCreate', this).call(this, req); - } - }, { - key: 'handleUpdate', - value: function handleUpdate(req) { - req.params.className = '_Session'; - return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleUpdate', this).call(this, req); - } - }, { - key: 'handleDelete', - value: function handleDelete(req) { - req.params.className = '_Session'; - return _get(SessionsRouter.prototype.__proto__ || Object.getPrototypeOf(SessionsRouter.prototype), 'handleDelete', this).call(this, req); - } - }, { - key: 'handleMe', - value: function handleMe(req) { - // TODO: Verify correct behavior - if (!req.info || !req.info.sessionToken) { - throw new _node2.default.Error(_node2.default.Error.INVALID_SESSION_TOKEN, 'Session token required.'); - } - return _rest2.default.find(req.config, _Auth2.default.master(req.config), '_Session', { sessionToken: req.info.sessionToken }, undefined, req.info.clientSDK).then(function (response) { - if (!response.results || response.results.length == 0) { - throw new _node2.default.Error(_node2.default.Error.INVALID_SESSION_TOKEN, 'Session token not found.'); - } - return { - response: response.results[0] - }; - }); - } - }, { - key: 'handleUpdateToRevocableSession', - value: function handleUpdateToRevocableSession(req) { - var config = req.config; - var masterAuth = _Auth2.default.master(config); - var user = req.auth.user; - // Issue #2720 - // Calling without a session token would result in a not found user - if (!user) { - throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'invalid session'); - } - var expiresAt = config.generateSessionExpiresAt(); - var sessionData = { - sessionToken: 'r:' + (0, _cryptoUtils.newToken)(), - user: { - __type: 'Pointer', - className: '_User', - objectId: user.id - }, - createdWith: { - 'action': 'upgrade' - }, - restricted: false, - installationId: req.auth.installationId, - expiresAt: _node2.default._encode(expiresAt) - }; - var create = new _RestWrite2.default(config, masterAuth, '_Session', null, sessionData); - return create.execute().then(function () { - // delete the session token, use the db to skip beforeSave - return config.database.update('_User', { - objectId: user.id - }, { - sessionToken: { __op: 'Delete' } - }); - }).then(function () { - return Promise.resolve({ response: sessionData }); - }); - } - }, { - key: 'mountRoutes', - value: function mountRoutes() { - var _this2 = this; - - this.route('GET', '/sessions/me', function (req) { - return _this2.handleMe(req); - }); - this.route('GET', '/sessions', function (req) { - return _this2.handleFind(req); - }); - this.route('GET', '/sessions/:objectId', function (req) { - return _this2.handleGet(req); - }); - this.route('POST', '/sessions', function (req) { - return _this2.handleCreate(req); - }); - this.route('PUT', '/sessions/:objectId', function (req) { - return _this2.handleUpdate(req); - }); - this.route('DELETE', '/sessions/:objectId', function (req) { - return _this2.handleDelete(req); - }); - this.route('POST', '/upgradeToRevocableSession', function (req) { - return _this2.handleUpdateToRevocableSession(req); - }); - } - }]); - - return SessionsRouter; -}(_ClassesRouter3.default); - -exports.default = SessionsRouter; \ No newline at end of file diff --git a/lib/Routers/UsersRouter.js b/lib/Routers/UsersRouter.js deleted file mode 100644 index 8f95101d22..0000000000 --- a/lib/Routers/UsersRouter.js +++ /dev/null @@ -1,367 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.UsersRouter = undefined; - -var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); - -var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } }; - -var _deepcopy = require('deepcopy'); - -var _deepcopy2 = _interopRequireDefault(_deepcopy); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _Config = require('../Config'); - -var _Config2 = _interopRequireDefault(_Config); - -var _AccountLockout = require('../AccountLockout'); - -var _AccountLockout2 = _interopRequireDefault(_AccountLockout); - -var _ClassesRouter2 = require('./ClassesRouter'); - -var _ClassesRouter3 = _interopRequireDefault(_ClassesRouter2); - -var _rest = require('../rest'); - -var _rest2 = _interopRequireDefault(_rest); - -var _Auth = require('../Auth'); - -var _Auth2 = _interopRequireDefault(_Auth); - -var _password = require('../password'); - -var _password2 = _interopRequireDefault(_password); - -var _RestWrite = require('../RestWrite'); - -var _RestWrite2 = _interopRequireDefault(_RestWrite); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // These methods handle the User-related routes. - -var cryptoUtils = require('../cryptoUtils'); - -var UsersRouter = exports.UsersRouter = function (_ClassesRouter) { - _inherits(UsersRouter, _ClassesRouter); - - function UsersRouter() { - _classCallCheck(this, UsersRouter); - - return _possibleConstructorReturn(this, (UsersRouter.__proto__ || Object.getPrototypeOf(UsersRouter)).apply(this, arguments)); - } - - _createClass(UsersRouter, [{ - key: 'handleFind', - value: function handleFind(req) { - req.params.className = '_User'; - return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleFind', this).call(this, req); - } - }, { - key: 'handleGet', - value: function handleGet(req) { - req.params.className = '_User'; - return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleGet', this).call(this, req); - } - }, { - key: 'handleCreate', - value: function handleCreate(req) { - var data = (0, _deepcopy2.default)(req.body); - req.body = data; - req.params.className = '_User'; - - return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleCreate', this).call(this, req); - } - }, { - key: 'handleUpdate', - value: function handleUpdate(req) { - req.params.className = '_User'; - return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleUpdate', this).call(this, req); - } - }, { - key: 'handleDelete', - value: function handleDelete(req) { - req.params.className = '_User'; - return _get(UsersRouter.prototype.__proto__ || Object.getPrototypeOf(UsersRouter.prototype), 'handleDelete', this).call(this, req); - } - }, { - key: 'handleMe', - value: function handleMe(req) { - if (!req.info || !req.info.sessionToken) { - throw new _node2.default.Error(_node2.default.Error.INVALID_SESSION_TOKEN, 'invalid session token'); - } - var sessionToken = req.info.sessionToken; - return _rest2.default.find(req.config, _Auth2.default.master(req.config), '_Session', { sessionToken: sessionToken }, { include: 'user' }, req.info.clientSDK).then(function (response) { - if (!response.results || response.results.length == 0 || !response.results[0].user) { - throw new _node2.default.Error(_node2.default.Error.INVALID_SESSION_TOKEN, 'invalid session token'); - } else { - var user = response.results[0].user; - // Send token back on the login, because SDKs expect that. - user.sessionToken = sessionToken; - - // Remove hidden properties. - for (var key in user) { - if (user.hasOwnProperty(key)) { - // Regexp comes from Parse.Object.prototype.validate - if (key !== "__type" && !/^[A-Za-z][0-9A-Za-z_]*$/.test(key)) { - delete user[key]; - } - } - } - - return { response: user }; - } - }); - } - }, { - key: 'handleLogIn', - value: function handleLogIn(req) { - // Use query parameters instead if provided in url - if (!req.body.username && req.query.username) { - req.body = req.query; - } - - // TODO: use the right error codes / descriptions. - if (!req.body.username) { - throw new _node2.default.Error(_node2.default.Error.USERNAME_MISSING, 'username is required.'); - } - if (!req.body.password) { - throw new _node2.default.Error(_node2.default.Error.PASSWORD_MISSING, 'password is required.'); - } - if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string') { - throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); - } - - var user = void 0; - var isValidPassword = false; - - return req.config.database.find('_User', { username: req.body.username }).then(function (results) { - if (!results.length) { - throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); - } - user = results[0]; - - if (req.config.verifyUserEmails && req.config.preventLoginWithUnverifiedEmail && !user.emailVerified) { - throw new _node2.default.Error(_node2.default.Error.EMAIL_NOT_FOUND, 'User email is not verified.'); - } - return _password2.default.compare(req.body.password, user.password); - }).then(function (correct) { - isValidPassword = correct; - var accountLockoutPolicy = new _AccountLockout2.default(user, req.config); - return accountLockoutPolicy.handleLoginAttempt(isValidPassword); - }).then(function () { - if (!isValidPassword) { - throw new _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Invalid username/password.'); - } - - // handle password expiry policy - if (req.config.passwordPolicy && req.config.passwordPolicy.maxPasswordAge) { - var 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: _node2.default._encode(changedAt) }); - } else { - // check whether the password has expired - if (changedAt.__type == 'Date') { - changedAt = new Date(changedAt.iso); - } - // Calculate the expiry time. - var _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 _node2.default.Error(_node2.default.Error.OBJECT_NOT_FOUND, 'Your password has expired. Please reset your password.'); - } - } - - var token = 'r:' + cryptoUtils.newToken(); - user.sessionToken = token; - delete user.password; - - // Sometimes the authData still has null on that keys - // https://github.com/ParsePlatform/parse-server/issues/935 - if (user.authData) { - Object.keys(user.authData).forEach(function (provider) { - if (user.authData[provider] === null) { - delete user.authData[provider]; - } - }); - if (Object.keys(user.authData).length == 0) { - delete user.authData; - } - } - - req.config.filesController.expandFilesInObject(req.config, user); - - var expiresAt = req.config.generateSessionExpiresAt(); - var sessionData = { - sessionToken: token, - user: { - __type: 'Pointer', - className: '_User', - objectId: user.objectId - }, - createdWith: { - 'action': 'login', - 'authProvider': 'password' - }, - restricted: false, - expiresAt: _node2.default._encode(expiresAt) - }; - - if (req.info.installationId) { - sessionData.installationId = req.info.installationId; - } - - var create = new _RestWrite2.default(req.config, _Auth2.default.master(req.config), '_Session', null, sessionData); - return create.execute(); - }).then(function () { - return { response: user }; - }); - } - }, { - key: 'handleLogOut', - value: function handleLogOut(req) { - var success = { response: {} }; - if (req.info && req.info.sessionToken) { - return _rest2.default.find(req.config, _Auth2.default.master(req.config), '_Session', { sessionToken: req.info.sessionToken }, undefined, req.info.clientSDK).then(function (records) { - if (records.results && records.results.length) { - return _rest2.default.del(req.config, _Auth2.default.master(req.config), '_Session', records.results[0].objectId).then(function () { - return Promise.resolve(success); - }); - } - return Promise.resolve(success); - }); - } - return Promise.resolve(success); - } - }, { - key: '_throwOnBadEmailConfig', - value: function _throwOnBadEmailConfig(req) { - try { - _Config2.default.validateEmailConfiguration({ - emailAdapter: req.config.userController.adapter, - appName: req.config.appName, - publicServerURL: req.config.publicServerURL, - 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 _node2.default.Error(_node2.default.Error.INTERNAL_SERVER_ERROR, 'An appName, publicServerURL, and emailAdapter are required for password reset and email verification functionality.'); - } else { - throw e; - } - } - } - }, { - key: 'handleResetRequest', - value: function handleResetRequest(req) { - this._throwOnBadEmailConfig(req); - - var email = req.body.email; - - if (!email) { - throw new _node2.default.Error(_node2.default.Error.EMAIL_MISSING, "you must provide an email"); - } - if (typeof email !== 'string') { - throw new _node2.default.Error(_node2.default.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string'); - } - var userController = req.config.userController; - return userController.sendPasswordResetEmail(email).then(function () { - return Promise.resolve({ - response: {} - }); - }, function (err) { - if (err.code === _node2.default.Error.OBJECT_NOT_FOUND) { - throw new _node2.default.Error(_node2.default.Error.EMAIL_NOT_FOUND, 'No user found with email ' + email + '.'); - } else { - throw err; - } - }); - } - }, { - key: 'handleVerificationEmailRequest', - value: function handleVerificationEmailRequest(req) { - this._throwOnBadEmailConfig(req); - - var email = req.body.email; - - if (!email) { - throw new _node2.default.Error(_node2.default.Error.EMAIL_MISSING, 'you must provide an email'); - } - if (typeof email !== 'string') { - throw new _node2.default.Error(_node2.default.Error.INVALID_EMAIL_ADDRESS, 'you must provide a valid email string'); - } - - return req.config.database.find('_User', { email: email }).then(function (results) { - if (!results.length || results.length < 1) { - throw new _node2.default.Error(_node2.default.Error.EMAIL_NOT_FOUND, 'No user found with email ' + email); - } - var user = results[0]; - - if (user.emailVerified) { - throw new _node2.default.Error(_node2.default.Error.OTHER_CAUSE, 'Email ' + email + ' is already verified.'); - } - - var userController = req.config.userController; - userController.sendVerificationEmail(user); - return { response: {} }; - }); - } - }, { - key: 'mountRoutes', - value: function mountRoutes() { - var _this2 = this; - - this.route('GET', '/users', function (req) { - return _this2.handleFind(req); - }); - this.route('POST', '/users', function (req) { - return _this2.handleCreate(req); - }); - this.route('GET', '/users/me', function (req) { - return _this2.handleMe(req); - }); - this.route('GET', '/users/:objectId', function (req) { - return _this2.handleGet(req); - }); - this.route('PUT', '/users/:objectId', function (req) { - return _this2.handleUpdate(req); - }); - this.route('DELETE', '/users/:objectId', function (req) { - return _this2.handleDelete(req); - }); - this.route('GET', '/login', function (req) { - return _this2.handleLogIn(req); - }); - this.route('POST', '/logout', function (req) { - return _this2.handleLogOut(req); - }); - this.route('POST', '/requestPasswordReset', function (req) { - return _this2.handleResetRequest(req); - }); - this.route('POST', '/verificationEmailRequest', function (req) { - return _this2.handleVerificationEmailRequest(req); - }); - } - }]); - - return UsersRouter; -}(_ClassesRouter3.default); - -exports.default = UsersRouter; \ No newline at end of file diff --git a/lib/StatusHandler.js b/lib/StatusHandler.js deleted file mode 100644 index 6512e7da1b..0000000000 --- a/lib/StatusHandler.js +++ /dev/null @@ -1,263 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -exports.flatten = flatten; -exports.jobStatusHandler = jobStatusHandler; -exports.pushStatusHandler = pushStatusHandler; - -var _cryptoUtils = require('./cryptoUtils'); - -var _logger = require('./logger'); - -var PUSH_STATUS_COLLECTION = '_PushStatus'; -var JOB_STATUS_COLLECTION = '_JobStatus'; - -var incrementOp = function incrementOp() { - var object = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - var key = arguments[1]; - var amount = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 1; - - if (!object[key]) { - object[key] = { __op: 'Increment', amount: amount }; - } else { - object[key].amount += amount; - } - return object[key]; -}; - -function flatten(array) { - var flattened = []; - for (var i = 0; i < array.length; i++) { - if (Array.isArray(array[i])) { - flattened = flattened.concat(flatten(array[i])); - } else { - flattened.push(array[i]); - } - } - return flattened; -} - -function statusHandler(className, database) { - var lastPromise = Promise.resolve(); - - function create(object) { - lastPromise = lastPromise.then(function () { - return database.create(className, object).then(function () { - return Promise.resolve(object); - }); - }); - return lastPromise; - } - - function update(where, object) { - lastPromise = lastPromise.then(function () { - return database.update(className, where, object); - }); - return lastPromise; - } - - return Object.freeze({ - create: create, - update: update - }); -} - -function jobStatusHandler(config) { - var jobStatus = void 0; - var objectId = (0, _cryptoUtils.newObjectId)(); - var database = config.database; - var handler = statusHandler(JOB_STATUS_COLLECTION, database); - var setRunning = function setRunning(jobName, params) { - var now = new Date(); - jobStatus = { - objectId: objectId, - jobName: jobName, - params: params, - status: 'running', - source: 'api', - createdAt: now, - // lockdown! - ACL: {} - }; - - return handler.create(jobStatus); - }; - - var setMessage = function setMessage(message) { - if (!message || typeof message !== 'string') { - return Promise.resolve(); - } - return handler.update({ objectId: objectId }, { message: message }); - }; - - var setSucceeded = function setSucceeded(message) { - return setFinalStatus('succeeded', message); - }; - - var setFailed = function setFailed(message) { - return setFinalStatus('failed', message); - }; - - var setFinalStatus = function setFinalStatus(status) { - var message = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : undefined; - - var finishedAt = new Date(); - var update = { status: status, finishedAt: finishedAt }; - if (message && typeof message === 'string') { - update.message = message; - } - return handler.update({ objectId: objectId }, update); - }; - - return Object.freeze({ - setRunning: setRunning, - setSucceeded: setSucceeded, - setMessage: setMessage, - setFailed: setFailed - }); -} - -function pushStatusHandler(config) { - var objectId = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : (0, _cryptoUtils.newObjectId)(); - - - var pushStatus = void 0; - var database = config.database; - var handler = statusHandler(PUSH_STATUS_COLLECTION, database); - var setInitial = function setInitial() { - var body = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - var where = arguments[1]; - var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : { source: 'rest' }; - - var now = new Date(); - var pushTime = new Date(); - var status = 'pending'; - if (body.hasOwnProperty('push_time')) { - if (config.hasPushScheduledSupport) { - pushTime = body.push_time; - status = 'scheduled'; - } else { - _logger.logger.warn('Trying to schedule a push while server is not configured.'); - _logger.logger.warn('Push will be sent immediately'); - } - } - - var data = body.data || {}; - var payloadString = JSON.stringify(data); - var pushHash = void 0; - if (typeof data.alert === 'string') { - pushHash = (0, _cryptoUtils.md5Hash)(data.alert); - } else if (_typeof(data.alert) === 'object') { - pushHash = (0, _cryptoUtils.md5Hash)(JSON.stringify(data.alert)); - } else { - pushHash = 'd41d8cd98f00b204e9800998ecf8427e'; - } - var object = { - objectId: objectId, - createdAt: now, - pushTime: pushTime.toISOString(), - query: JSON.stringify(where), - payload: payloadString, - source: options.source, - title: options.title, - expiry: body.expiration_time, - status: status, - numSent: 0, - pushHash: pushHash, - // lockdown! - ACL: {} - }; - - return handler.create(object).then(function () { - pushStatus = { - objectId: objectId - }; - return Promise.resolve(pushStatus); - }); - }; - - var setRunning = function setRunning(count) { - _logger.logger.verbose('_PushStatus ' + objectId + ': sending push to %d installations', count); - return handler.update({ status: "pending", objectId: objectId }, { status: "running", updatedAt: new Date(), count: count }); - }; - - var trackSent = function trackSent(results) { - var _this = this; - - var update = { - updatedAt: new Date(), - numSent: 0, - numFailed: 0 - }; - if (Array.isArray(results)) { - results = flatten(results); - results.reduce(function (memo, result) { - // Cannot handle that - if (!result || !result.device || !result.device.deviceType) { - return memo; - } - var deviceType = result.device.deviceType; - var key = result.transmitted ? 'sentPerType.' + deviceType : 'failedPerType.' + deviceType; - memo[key] = incrementOp(memo, key); - if (result.transmitted) { - memo.numSent++; - } else { - memo.numFailed++; - } - return memo; - }, update); - incrementOp(update, 'count', -results.length); - } - - _logger.logger.verbose('_PushStatus ' + objectId + ': sent push! %d success, %d failures', update.numSent, update.numFailed); - - ['numSent', 'numFailed'].forEach(function (key) { - if (update[key] > 0) { - update[key] = { - __op: 'Increment', - amount: update[key] - }; - } else { - delete update[key]; - } - }); - - return handler.update({ objectId: objectId }, update).then(function (res) { - if (res && res.count === 0) { - return _this.complete(); - } - }); - }; - - var complete = function complete() { - return handler.update({ objectId: objectId }, { - status: 'succeeded', - count: { __op: 'Delete' }, - updatedAt: new Date() - }); - }; - - var fail = function fail(err) { - var update = { - errorMessage: JSON.stringify(err), - status: 'failed', - updatedAt: new Date() - }; - _logger.logger.warn('_PushStatus ' + objectId + ': error while sending push', err); - return handler.update({ objectId: objectId }, update); - }; - - return Object.freeze({ - objectId: objectId, - setInitial: setInitial, - setRunning: setRunning, - trackSent: trackSent, - complete: complete, - fail: fail - }); -} \ No newline at end of file diff --git a/lib/TestUtils.js b/lib/TestUtils.js deleted file mode 100644 index 4741bd088d..0000000000 --- a/lib/TestUtils.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.destroyAllDataPermanently = destroyAllDataPermanently; - -var _cache = require('./cache'); - -var _cache2 = _interopRequireDefault(_cache); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -//Used by tests -function destroyAllDataPermanently() { - if (!process.env.TESTING) { - throw 'Only supported in test environment'; - } - return Promise.all(Object.keys(_cache2.default.cache).map(function (appId) { - var app = _cache2.default.get(appId); - if (app.databaseController) { - return app.databaseController.deleteEverything(); - } else { - return Promise.resolve(); - } - })); -} \ No newline at end of file diff --git a/lib/batch.js b/lib/batch.js deleted file mode 100644 index 2774575d07..0000000000 --- a/lib/batch.js +++ /dev/null @@ -1,98 +0,0 @@ -'use strict'; - -var Parse = require('parse/node').Parse; -var url = require('url'); -var path = require('path'); -// These methods handle batch requests. -var batchPath = '/batch'; - -// Mounts a batch-handler onto a PromiseRouter. -function mountOnto(router) { - router.route('POST', batchPath, function (req) { - return handleBatch(router, req); - }); -} - -function parseURL(URL) { - if (typeof URL === 'string') { - return url.parse(URL); - } - return undefined; -} - -function makeBatchRoutingPathFunction(originalUrl, serverURL, publicServerURL) { - serverURL = serverURL ? parseURL(serverURL) : undefined; - publicServerURL = publicServerURL ? parseURL(publicServerURL) : undefined; - - var apiPrefixLength = originalUrl.length - batchPath.length; - var apiPrefix = originalUrl.slice(0, apiPrefixLength); - - var makeRoutablePath = function makeRoutablePath(requestPath) { - // The routablePath is the path minus the api prefix - if (requestPath.slice(0, apiPrefix.length) != apiPrefix) { - throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot route batch path ' + requestPath); - } - return path.posix.join('/', requestPath.slice(apiPrefix.length)); - }; - - if (serverURL && publicServerURL && serverURL.path != publicServerURL.path) { - var localPath = serverURL.path; - var publicPath = publicServerURL.path; - // Override the api prefix - apiPrefix = localPath; - return function (requestPath) { - // Build the new path by removing the public path - // and joining with the local path - var newPath = path.posix.join('/', localPath, '/', requestPath.slice(publicPath.length)); - // Use the method for local routing - return makeRoutablePath(newPath); - }; - } - - return makeRoutablePath; -} - -// Returns a promise for a {response} object. -// 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'); - } - - // The batch paths are all from the root of our domain. - // That means they include the API prefix, that the API is mounted - // to. However, our promise router does not route the api prefix. So - // we need to figure out the API prefix, so that we can strip it - // from all the subrequests. - if (!req.originalUrl.endsWith(batchPath)) { - throw 'internal routing problem - expected url to end with batch'; - } - - var makeRoutablePath = makeBatchRoutingPathFunction(req.originalUrl, req.config.serverURL, req.config.publicServerURL); - - var promises = req.body.requests.map(function (restRequest) { - var routablePath = makeRoutablePath(restRequest.path); - // Construct a request that we can send to a handler - var request = { - body: restRequest.body, - config: req.config, - auth: req.auth, - info: req.info - }; - - return router.tryRouteRequest(restRequest.method, routablePath, request).then(function (response) { - return { success: response.response }; - }, function (error) { - return { error: { code: error.code, error: error.message } }; - }); - }); - - return Promise.all(promises).then(function (results) { - return { response: results }; - }); -} - -module.exports = { - mountOnto: mountOnto, - makeBatchRoutingPathFunction: makeBatchRoutingPathFunction -}; \ No newline at end of file diff --git a/lib/cache.js b/lib/cache.js deleted file mode 100644 index 51641150c3..0000000000 --- a/lib/cache.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.AppCache = undefined; - -var _InMemoryCache = require('./Adapters/Cache/InMemoryCache'); - -var AppCache = exports.AppCache = new _InMemoryCache.InMemoryCache({ ttl: NaN }); -exports.default = AppCache; \ No newline at end of file diff --git a/lib/cli/definitions/parse-live-query-server.js b/lib/cli/definitions/parse-live-query-server.js deleted file mode 100644 index 637fa9fd3f..0000000000 --- a/lib/cli/definitions/parse-live-query-server.js +++ /dev/null @@ -1,45 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _parsers = require("../utils/parsers"); - -exports.default = { - "appId": { - required: true, - help: "Required. 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": { - required: true, - help: "Required. 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": { - required: true, - help: "Required. 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." - }, - "redisURL": { - help: "Optional. 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." - }, - "keyPairs": { - help: "Optional. 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." - }, - "websocketTimeout": { - help: "Optional. 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: (0, _parsers.numberParser)("websocketTimeout") - }, - "cacheTimeout": { - help: "Optional. 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: (0, _parsers.numberParser)("cacheTimeout") - }, - "logLevel": { - help: "Optional. This string defines the log level of the LiveQuery server. We support VERBOSE, INFO, ERROR, NONE. Defaults to INFO." - }, - "port": { - env: "PORT", - help: "The port to run the ParseServer. defaults to 1337.", - default: 1337, - action: (0, _parsers.numberParser)("port") - } -}; \ No newline at end of file diff --git a/lib/cli/definitions/parse-server.js b/lib/cli/definitions/parse-server.js deleted file mode 100644 index b79a961cf7..0000000000 --- a/lib/cli/definitions/parse-server.js +++ /dev/null @@ -1,257 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _parsers = require("../utils/parsers"); - -exports.default = { - "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 - }, - "port": { - env: "PORT", - help: "The port to run the ParseServer. defaults to 1337.", - default: 1337, - action: (0, _parsers.numberParser)("port") - }, - "host": { - env: "PARSE_SERVER_HOST", - help: "The host to serve ParseServer on. defaults to 0.0.0.0", - default: '0.0.0.0' - }, - "databaseURI": { - env: "PARSE_SERVER_DATABASE_URI", - help: "The full URI to your mongodb database" - }, - "databaseOptions": { - env: "PARSE_SERVER_DATABASE_OPTIONS", - help: "Options to pass to the mongodb client", - action: _parsers.objectParser - }, - "collectionPrefix": { - env: "PARSE_SERVER_COLLECTION_PREFIX", - help: 'A collection prefix for the classes' - }, - "serverURL": { - env: "PARSE_SERVER_URL", - help: "URL to your parse server with http:// or https://." - }, - "publicServerURL": { - env: "PARSE_PUBLIC_SERVER_URL", - help: "Public URL to your parse server with http:// or https://." - }, - "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" - }, - "restAPIKey": { - env: "PARSE_SERVER_REST_API_KEY", - help: "Key for REST calls" - }, - "dotNetKey": { - env: "PARSE_SERVER_DOT_NET_KEY", - help: "Key for Unity and .Net SDK" - }, - "webhookKey": { - env: "PARSE_SERVER_WEBHOOK_KEY", - help: "Key sent with outgoing webhook calls" - }, - "cloud": { - env: "PARSE_SERVER_CLOUD_CODE_MAIN", - help: "Full path to your cloud code main.js" - }, - "push": { - env: "PARSE_SERVER_PUSH", - help: "Configuration for push, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Push", - action: _parsers.objectParser - }, - "scheduledPush": { - env: "PARSE_SERVER_SCHEDULED_PUSH", - help: "Configuration for push scheduling. Defaults to false.", - action: _parsers.booleanParser - }, - "oauth": { - env: "PARSE_SERVER_OAUTH_PROVIDERS", - help: "[DEPRECATED (use auth option)] Configuration for your oAuth providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth", - action: _parsers.objectParser - }, - "auth": { - env: "PARSE_SERVER_AUTH_PROVIDERS", - help: "Configuration for your authentication providers, as stringified JSON. See https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide#oauth", - action: _parsers.objectParser - }, - "fileKey": { - env: "PARSE_SERVER_FILE_KEY", - help: "Key for your files" - }, - "facebookAppIds": { - env: "PARSE_SERVER_FACEBOOK_APP_IDS", - help: "[DEPRECATED (use auth option)]", - action: function action() { - throw 'facebookAppIds is deprecated, please use { auth: \ - {facebook: \ - { appIds: [] } \ - }\ - }\ - }'; - } - }, - "enableAnonymousUsers": { - env: "PARSE_SERVER_ENABLE_ANON_USERS", - help: "Enable (or disable) anon users, defaults to true", - action: _parsers.booleanParser - }, - "allowClientClassCreation": { - env: "PARSE_SERVER_ALLOW_CLIENT_CLASS_CREATION", - help: "Enable (or disable) client class creation, defaults to true", - action: _parsers.booleanParser - }, - "mountPath": { - env: "PARSE_SERVER_MOUNT_PATH", - help: "Mount path for the server, defaults to /parse", - default: "/parse" - }, - "filesAdapter": { - env: "PARSE_SERVER_FILES_ADAPTER", - help: "Adapter module for the files sub-system", - action: _parsers.moduleOrObjectParser - }, - "emailAdapter": { - env: "PARSE_SERVER_EMAIL_ADAPTER", - help: "Adapter module for the email sending", - action: _parsers.moduleOrObjectParser - }, - "verifyUserEmails": { - env: "PARSE_SERVER_VERIFY_USER_EMAILS", - help: "Enable (or disable) user email validation, defaults to false", - action: _parsers.booleanParser - }, - "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 - }, - "emailVerifyTokenValidityDuration": { - env: "PARSE_SERVER_EMAIL_VERIFY_TOKEN_VALIDITY_DURATION", - help: "Email verification token validity duration", - action: (0, _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 - }, - "appName": { - env: "PARSE_SERVER_APP_NAME", - help: "Sets the app name" - }, - "loggerAdapter": { - env: "PARSE_SERVER_LOGGER_ADAPTER", - help: "Adapter module for the logging sub-system", - action: _parsers.moduleOrObjectParser - }, - "customPages": { - env: "PARSE_SERVER_CUSTOM_PAGES", - help: "custom pages for password validation and reset", - action: _parsers.objectParser - }, - "maxUploadSize": { - env: "PARSE_SERVER_MAX_UPLOAD_SIZE", - help: "Max file size for uploads.", - default: "20mb" - }, - "userSensitiveFields": { - help: "Personally identifiable information fields in the user table the should be removed for non-authorized users.", - default: ["email"] - }, - "sessionLength": { - env: "PARSE_SERVER_SESSION_LENGTH", - help: "Session duration, defaults to 1 year", - action: (0, _parsers.numberParser)("sessionLength") - }, - "verbose": { - env: "VERBOSE", - help: "Set the logging to verbose" - }, - "jsonLogs": { - env: "JSON_LOGS", - help: "Log as structured JSON objects" - }, - "logLevel": { - env: "PARSE_SERVER_LOG_LEVEL", - help: "Sets the level for logs" - }, - "logsFolder": { - env: "PARSE_SERVER_LOGS_FOLDER", - help: "Folder for the logs (defaults to './logs'); set to null to disable file based logging", - action: _parsers.nullParser - }, - "silent": { - help: "Disables console output" - }, - "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 - }, - "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 0; disabled.", - action: (0, _parsers.numberParser)("schemaCacheTTL") - }, - "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 - }, - "cluster": { - env: "PARSE_SERVER_CLUSTER", - help: "Run with cluster, optionally set the number of processes default to os.cpus().length", - action: (0, _parsers.numberOrBoolParser)("cluster") - }, - "liveQuery": { - env: "PARSE_SERVER_LIVE_QUERY_OPTIONS", - help: "parse-server's LiveQuery configuration object", - action: _parsers.objectParser - }, - "liveQuery.classNames": { - help: "parse-server's LiveQuery classNames", - action: _parsers.arrayParser - }, - "liveQuery.redisURL": { - help: "parse-server's LiveQuery redisURL" - }, - "startLiveQueryServer": { - help: "Starts the liveQuery server", - action: _parsers.booleanParser - }, - "liveQueryPort": { - help: 'Specific port to start the live query server', - action: (0, _parsers.numberParser)("liveQueryPort") - }, - "liveQueryServerOptions": { - help: "Live query server configuration options (will start the liveQuery server)", - action: _parsers.objectParser - }, - "middleware": { - help: "middleware for express server, can be string or function" - } -}; \ No newline at end of file diff --git a/lib/cli/parse-live-query-server.js b/lib/cli/parse-live-query-server.js deleted file mode 100644 index dbe2e52a8c..0000000000 --- a/lib/cli/parse-live-query-server.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -var _parseLiveQueryServer = require('./definitions/parse-live-query-server'); - -var _parseLiveQueryServer2 = _interopRequireDefault(_parseLiveQueryServer); - -var _runner = require('./utils/runner'); - -var _runner2 = _interopRequireDefault(_runner); - -var _index = require('../index'); - -var _express = require('express'); - -var _express2 = _interopRequireDefault(_express); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -(0, _runner2.default)({ - definitions: _parseLiveQueryServer2.default, - start: function start(program, options, logOptions) { - logOptions(); - var app = (0, _express2.default)(); - var httpServer = require('http').createServer(app); - httpServer.listen(options.port); - _index.ParseServer.createLiveQueryServer(httpServer, options); - } -}); \ No newline at end of file diff --git a/lib/cli/parse-server.js b/lib/cli/parse-server.js deleted file mode 100755 index ec671e97fc..0000000000 --- a/lib/cli/parse-server.js +++ /dev/null @@ -1,169 +0,0 @@ -'use strict'; - -var _express = require('express'); - -var _express2 = _interopRequireDefault(_express); - -var _index = require('../index'); - -var _index2 = _interopRequireDefault(_index); - -var _parseServer = require('./definitions/parse-server'); - -var _parseServer2 = _interopRequireDefault(_parseServer); - -var _cluster = require('cluster'); - -var _cluster2 = _interopRequireDefault(_cluster); - -var _os = require('os'); - -var _os2 = _interopRequireDefault(_os); - -var _runner = require('./utils/runner'); - -var _runner2 = _interopRequireDefault(_runner); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -/* eslint-disable no-console */ -var path = require("path"); - -var help = function help() { - console.log(' Get Started guide:'); - console.log(''); - console.log(' Please have a look at the get started guide!'); - console.log(' https://github.com/ParsePlatform/parse-server/wiki/Parse-Server-Guide'); - console.log(''); - console.log(''); - 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(''); - 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(''); -}; - -function startServer(options, callback) { - var app = (0, _express2.default)(); - if (options.middleware) { - var middleware = void 0; - if (typeof options.middleware == 'function') { - middleware = options.middleware; - }if (typeof options.middleware == 'string') { - middleware = require(path.resolve(process.cwd(), options.middleware)); - } else { - throw "middleware should be a string or a function"; - } - app.use(middleware); - } - - var parseServer = new _index2.default(options); - var sockets = {}; - app.use(options.mountPath, parseServer.app); - - var server = app.listen(options.port, options.host, callback); - server.on('connection', initializeConnections); - - if (options.startLiveQueryServer || options.liveQueryServerOptions) { - var liveQueryServer = server; - if (options.liveQueryPort) { - liveQueryServer = (0, _express2.default)().listen(options.liveQueryPort, function () { - console.log('ParseLiveQuery listening on ' + options.liveQueryPort); - }); - } - _index2.default.createLiveQueryServer(liveQueryServer, options.liveQueryServerOptions); - } - - function initializeConnections(socket) { - /* 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. */ - - var socketId = socket.remoteAddress + ':' + socket.remotePort; - sockets[socketId] = socket; - - socket.on('close', function () { - delete sockets[socketId]; - }); - } - - function destroyAliveConnections() { - for (var socketId in sockets) { - try { - sockets[socketId].destroy(); - } catch (e) {/* */} - } - } - - var handleShutdown = function handleShutdown() { - console.log('Termination signal received. Shutting down.'); - destroyAliveConnections(); - server.close(); - parseServer.handleShutdown(); - }; - process.on('SIGTERM', handleShutdown); - process.on('SIGINT', handleShutdown); -} - -(0, _runner2.default)({ - definitions: _parseServer2.default, - help: help, - usage: '[options] ', - start: function start(program, options, logOptions) { - if (!options.serverURL) { - options.serverURL = 'http://localhost:' + options.port + options.mountPath; - } - - if (!options.appId || !options.masterKey || !options.serverURL) { - program.outputHelp(); - console.error(""); - console.error('\x1B[31mERROR: appId and masterKey are required\x1B[0m'); - console.error(""); - process.exit(1); - } - - if (options["liveQuery.classNames"]) { - options.liveQuery = options.liveQuery || {}; - options.liveQuery.classNames = options["liveQuery.classNames"]; - delete options["liveQuery.classNames"]; - } - if (options["liveQuery.redisURL"]) { - options.liveQuery = options.liveQuery || {}; - options.liveQuery.redisURL = options["liveQuery.redisURL"]; - delete options["liveQuery.redisURL"]; - } - - if (options.cluster) { - var numCPUs = typeof options.cluster === 'number' ? options.cluster : _os2.default.cpus().length; - if (_cluster2.default.isMaster) { - logOptions(); - for (var i = 0; i < numCPUs; i++) { - _cluster2.default.fork(); - } - _cluster2.default.on('exit', function (worker, code) { - console.log('worker ' + worker.process.pid + ' died (' + code + ')... Restarting'); - _cluster2.default.fork(); - }); - } else { - startServer(options, function () { - console.log('[' + process.pid + '] parse-server running on ' + options.serverURL); - }); - } - } else { - startServer(options, function () { - logOptions(); - console.log(''); - console.log('[' + process.pid + '] parse-server running on ' + options.serverURL); - }); - } - } -}); - -/* eslint-enable no-console */ \ No newline at end of file diff --git a/lib/cli/utils/commander.js b/lib/cli/utils/commander.js deleted file mode 100644 index daded29687..0000000000 --- a/lib/cli/utils/commander.js +++ /dev/null @@ -1,150 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; /* eslint-disable no-console */ - - -var _commander = require('commander'); - -var _path = require('path'); - -var _path2 = _interopRequireDefault(_path); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var _definitions = void 0; -var _reverseDefinitions = void 0; -var _defaults = void 0; - -_commander.Command.prototype.loadDefinitions = function (definitions) { - _definitions = definitions; - - Object.keys(definitions).reduce(function (program, opt) { - if (_typeof(definitions[opt]) == "object") { - var additionalOptions = definitions[opt]; - if (additionalOptions.required === true) { - return program.option('--' + opt + ' <' + opt + '>', additionalOptions.help, additionalOptions.action); - } else { - return program.option('--' + opt + ' [' + opt + ']', additionalOptions.help, additionalOptions.action); - } - } - return program.option('--' + opt + ' [' + opt + ']'); - }, this); - - _defaults = Object.keys(definitions).reduce(function (defs, opt) { - if (_definitions[opt].default) { - defs[opt] = _definitions[opt].default; - } - return defs; - }, {}); - - _reverseDefinitions = Object.keys(definitions).reduce(function (object, key) { - var value = definitions[key]; - if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) == "object") { - value = value.env; - } - if (value) { - object[value] = key; - } - return object; - }, {}); - - /* istanbul ignore next */ - this.on('--help', function () { - console.log(' Configure From Environment:'); - console.log(''); - Object.keys(_reverseDefinitions).forEach(function (key) { - console.log(' $ ' + key + '=\'' + _reverseDefinitions[key] + '\''); - }); - console.log(''); - }); -}; - -function parseEnvironment() { - var env = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - - return Object.keys(_reverseDefinitions).reduce(function (options, key) { - if (env[key]) { - var originalKey = _reverseDefinitions[key]; - var action = function action(option) { - return option; - }; - if (_typeof(_definitions[originalKey]) === "object") { - action = _definitions[originalKey].action || action; - } - options[_reverseDefinitions[key]] = action(env[key]); - } - return options; - }, {}); -} - -function parseConfigFile(program) { - var options = {}; - if (program.args.length > 0) { - var jsonPath = program.args[0]; - jsonPath = _path2.default.resolve(jsonPath); - var jsonConfig = require(jsonPath); - if (jsonConfig.apps) { - if (jsonConfig.apps.length > 1) { - throw 'Multiple apps are not supported'; - } - options = jsonConfig.apps[0]; - } else { - options = jsonConfig; - } - Object.keys(options).forEach(function (key) { - var value = options[key]; - if (!_definitions[key]) { - throw 'error: unknown option ' + key; - } - var action = _definitions[key].action; - if (action) { - options[key] = action(value); - } - }); - console.log('Configuration loaded from ' + jsonPath); - } - return options; -} - -_commander.Command.prototype.setValuesIfNeeded = function (options) { - var _this = this; - - Object.keys(options).forEach(function (key) { - if (!_this.hasOwnProperty(key)) { - _this[key] = options[key]; - } - }); -}; - -_commander.Command.prototype._parse = _commander.Command.prototype.parse; - -_commander.Command.prototype.parse = function (args, env) { - this._parse(args); - // Parse the environment first - var envOptions = parseEnvironment(env); - var fromFile = parseConfigFile(this); - // Load the env if not passed from command line - this.setValuesIfNeeded(envOptions); - // Load from file to override - this.setValuesIfNeeded(fromFile); - // Last set the defaults - this.setValuesIfNeeded(_defaults); -}; - -_commander.Command.prototype.getOptions = function () { - var _this2 = this; - - return Object.keys(_definitions).reduce(function (options, key) { - if (typeof _this2[key] !== 'undefined') { - options[key] = _this2[key]; - } - return options; - }, {}); -}; - -exports.default = new _commander.Command(); -/* eslint-enable no-console */ \ No newline at end of file diff --git a/lib/cli/utils/parsers.js b/lib/cli/utils/parsers.js deleted file mode 100644 index fabcec314e..0000000000 --- a/lib/cli/utils/parsers.js +++ /dev/null @@ -1,80 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -exports.numberParser = numberParser; -exports.numberOrBoolParser = numberOrBoolParser; -exports.objectParser = objectParser; -exports.arrayParser = arrayParser; -exports.moduleOrObjectParser = moduleOrObjectParser; -exports.booleanParser = booleanParser; -exports.nullParser = nullParser; -function numberParser(key) { - return function (opt) { - var intOpt = parseInt(opt); - if (!Number.isInteger(intOpt)) { - throw new Error('Key ' + key + ' has invalid value ' + opt); - } - return intOpt; - }; -} - -function numberOrBoolParser(key) { - return function (opt) { - if (typeof opt === 'boolean') { - return opt; - } - if (opt === 'true') { - return true; - } - if (opt === 'false') { - return false; - } - return numberParser(key)(opt); - }; -} - -function objectParser(opt) { - if ((typeof opt === 'undefined' ? 'undefined' : _typeof(opt)) == 'object') { - return opt; - } - return JSON.parse(opt); -} - -function arrayParser(opt) { - if (Array.isArray(opt)) { - return opt; - } else if (typeof opt === 'string') { - return opt.split(','); - } else { - throw new Error(opt + ' should be a comma separated string or an array'); - } -} - -function moduleOrObjectParser(opt) { - if ((typeof opt === 'undefined' ? 'undefined' : _typeof(opt)) == 'object') { - return opt; - } - try { - return JSON.parse(opt); - } catch (e) {/* */} - return opt; -} - -function booleanParser(opt) { - if (opt == true || opt == 'true' || opt == '1') { - return true; - } - return false; -} - -function nullParser(opt) { - if (opt == 'null') { - return null; - } - return opt; -} \ No newline at end of file diff --git a/lib/cli/utils/runner.js b/lib/cli/utils/runner.js deleted file mode 100644 index f4371a242e..0000000000 --- a/lib/cli/utils/runner.js +++ /dev/null @@ -1,49 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -exports.default = function (_ref) { - var definitions = _ref.definitions, - help = _ref.help, - usage = _ref.usage, - start = _ref.start; - - _commander2.default.loadDefinitions(definitions); - if (usage) { - _commander2.default.usage(usage); - } - if (help) { - _commander2.default.on('--help', help); - } - _commander2.default.parse(process.argv, process.env); - - var options = _commander2.default.getOptions(); - start(_commander2.default, options, function () { - logStartupOptions(options); - }); -}; - -var _commander = require("./commander"); - -var _commander2 = _interopRequireDefault(_commander); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function logStartupOptions(options) { - for (var key in options) { - var value = options[key]; - if (key == "masterKey") { - value = "***REDACTED***"; - } - if ((typeof value === "undefined" ? "undefined" : _typeof(value)) === 'object') { - value = JSON.stringify(value); - } - /* eslint-disable no-console */ - console.log(key + ": " + value); - /* eslint-enable no-console */ - } -} \ No newline at end of file diff --git a/lib/cloud-code/HTTPResponse.js b/lib/cloud-code/HTTPResponse.js deleted file mode 100644 index 7eb5b66d1d..0000000000 --- a/lib/cloud-code/HTTPResponse.js +++ /dev/null @@ -1,65 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -var HTTPResponse = function HTTPResponse(response, body) { - var _this = this; - - _classCallCheck(this, HTTPResponse); - - var _text = void 0, - _data = void 0; - this.status = response.statusCode; - this.headers = response.headers || {}; - this.cookies = this.headers["set-cookie"]; - - if (typeof body == 'string') { - _text = body; - } else if (Buffer.isBuffer(body)) { - this.buffer = body; - } else if ((typeof body === 'undefined' ? 'undefined' : _typeof(body)) == 'object') { - _data = body; - } - - var getText = function getText() { - if (!_text && _this.buffer) { - _text = _this.buffer.toString('utf-8'); - } else if (!_text && _data) { - _text = JSON.stringify(_data); - } - return _text; - }; - - var getData = function getData() { - if (!_data) { - try { - _data = JSON.parse(getText()); - } catch (e) {/* */} - } - return _data; - }; - - Object.defineProperty(this, 'body', { - get: function get() { - return body; - } - }); - - Object.defineProperty(this, 'text', { - enumerable: true, - get: getText - }); - - Object.defineProperty(this, 'data', { - enumerable: true, - get: getData - }); -}; - -exports.default = HTTPResponse; \ No newline at end of file diff --git a/lib/cloud-code/Parse.Cloud.js b/lib/cloud-code/Parse.Cloud.js deleted file mode 100644 index 7aa74f4b9f..0000000000 --- a/lib/cloud-code/Parse.Cloud.js +++ /dev/null @@ -1,76 +0,0 @@ -'use strict'; - -var _node = require('parse/node'); - -var _triggers = require('../triggers'); - -var triggers = _interopRequireWildcard(_triggers); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function validateClassNameForTriggers(className) { - var restrictedClassNames = ['_Session']; - if (restrictedClassNames.indexOf(className) != -1) { - throw 'Triggers are not supported for ' + className + ' class.'; - } - return className; -} - -function getClassName(parseClass) { - if (parseClass && parseClass.className) { - return validateClassNameForTriggers(parseClass.className); - } - return validateClassNameForTriggers(parseClass); -} - -var ParseCloud = {}; -ParseCloud.define = function (functionName, handler, validationHandler) { - triggers.addFunction(functionName, handler, validationHandler, _node.Parse.applicationId); -}; - -ParseCloud.job = function (functionName, handler) { - triggers.addJob(functionName, handler, _node.Parse.applicationId); -}; - -ParseCloud.beforeSave = function (parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.beforeSave, className, handler, _node.Parse.applicationId); -}; - -ParseCloud.beforeDelete = function (parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.beforeDelete, className, handler, _node.Parse.applicationId); -}; - -ParseCloud.afterSave = function (parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.afterSave, className, handler, _node.Parse.applicationId); -}; - -ParseCloud.afterDelete = function (parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.afterDelete, className, handler, _node.Parse.applicationId); -}; - -ParseCloud.beforeFind = function (parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.beforeFind, className, handler, _node.Parse.applicationId); -}; - -ParseCloud.afterFind = function (parseClass, handler) { - var className = getClassName(parseClass); - triggers.addTrigger(triggers.Types.afterFind, className, handler, _node.Parse.applicationId); -}; - -ParseCloud._removeAllHooks = function () { - triggers._unregisterAll(); -}; - -ParseCloud.useMasterKey = function () { - // 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: https://github.com/ParsePlatform/parse-server/wiki/Compatibility-with-Hosted-Parse#cloud-code"); -}; - -ParseCloud.httpRequest = require("./httpRequest"); - -module.exports = ParseCloud; \ No newline at end of file diff --git a/lib/cloud-code/httpRequest.js b/lib/cloud-code/httpRequest.js deleted file mode 100644 index 869ebec3e1..0000000000 --- a/lib/cloud-code/httpRequest.js +++ /dev/null @@ -1,107 +0,0 @@ -'use strict'; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var _request = require('request'); - -var _request2 = _interopRequireDefault(_request); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _HTTPResponse = require('./HTTPResponse'); - -var _HTTPResponse2 = _interopRequireDefault(_HTTPResponse); - -var _querystring = require('querystring'); - -var _querystring2 = _interopRequireDefault(_querystring); - -var _logger = require('../logger'); - -var _logger2 = _interopRequireDefault(_logger); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var encodeBody = function encodeBody(_ref) { - var body = _ref.body, - _ref$headers = _ref.headers, - headers = _ref$headers === undefined ? {} : _ref$headers; - - if ((typeof body === 'undefined' ? 'undefined' : _typeof(body)) !== 'object') { - return { body: body, headers: headers }; - } - var contentTypeKeys = Object.keys(headers).filter(function (key) { - return key.match(/content-type/i) != null; - }); - - if (contentTypeKeys.length == 0) { - // no content type - // As per https://parse.com/docs/cloudcode/guide#cloud-code-advanced-sending-a-post-request the default encoding is supposedly x-www-form-urlencoded - - body = _querystring2.default.stringify(body); - headers['Content-Type'] = 'application/x-www-form-urlencoded'; - } else { - /* istanbul ignore next */ - if (contentTypeKeys.length > 1) { - _logger2.default.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)) { - body = _querystring2.default.stringify(body); - } - } - return { body: body, headers: headers }; -}; - -module.exports = function (options) { - var promise = new _node2.default.Promise(); - var callbacks = { - success: options.success, - error: options.error - }; - delete options.success; - delete options.error; - delete options.uri; // not supported - options = Object.assign(options, encodeBody(options)); - // set follow redirects to false by default - options.followRedirect = options.followRedirects == true; - // support params options - if (_typeof(options.params) === 'object') { - options.qs = options.params; - } else if (typeof options.params === 'string') { - options.qs = _querystring2.default.parse(options.params); - } - // force the response as a buffer - options.encoding = null; - - (0, _request2.default)(options, function (error, response, body) { - if (error) { - if (callbacks.error) { - callbacks.error(error); - } - return promise.reject(error); - } - var httpResponse = new _HTTPResponse2.default(response, body); - - // Consider <200 && >= 400 as errors - if (httpResponse.status < 200 || httpResponse.status >= 400) { - if (callbacks.error) { - callbacks.error(httpResponse); - } - return promise.reject(httpResponse); - } else { - if (callbacks.success) { - callbacks.success(httpResponse); - } - return promise.resolve(httpResponse); - } - }); - return promise; -}; - -module.exports.encodeBody = encodeBody; \ No newline at end of file diff --git a/lib/cryptoUtils.js b/lib/cryptoUtils.js deleted file mode 100644 index ae36817935..0000000000 --- a/lib/cryptoUtils.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.randomHexString = randomHexString; -exports.randomString = randomString; -exports.newObjectId = newObjectId; -exports.newToken = newToken; -exports.md5Hash = md5Hash; - -var _crypto = require('crypto'); - -// Returns a new random hex string of the given even size. -function randomHexString(size) { - if (size === 0) { - throw new Error('Zero-length randomHexString is useless.'); - } - if (size % 2 !== 0) { - throw new Error('randomHexString size must be divisible by 2.'); - } - return (0, _crypto.randomBytes)(size / 2).toString('hex'); -} - -// Returns a new random alphanumeric string of the given size. -// -// Note: to simplify implementation, the result has slight modulo bias, -// because chars length of 62 doesn't divide the number of all bytes -// (256) evenly. Such bias is acceptable for most cases when the output -// length is long enough and doesn't need to be uniform. - - -function randomString(size) { - if (size === 0) { - throw new Error('Zero-length randomString is useless.'); - } - var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 'abcdefghijklmnopqrstuvwxyz' + '0123456789'; - var objectId = ''; - var bytes = (0, _crypto.randomBytes)(size); - for (var i = 0; i < bytes.length; ++i) { - objectId += chars[bytes.readUInt8(i) % chars.length]; - } - return objectId; -} - -// Returns a new random alphanumeric string suitable for object ID. -function newObjectId() { - //TODO: increase length to better protect against collisions. - return randomString(10); -} - -// Returns a new random hex string suitable for secure tokens. -function newToken() { - return randomHexString(32); -} - -function md5Hash(string) { - return (0, _crypto.createHash)('md5').update(string).digest('hex'); -} \ No newline at end of file diff --git a/lib/defaults.js b/lib/defaults.js deleted file mode 100644 index 148f6cf533..0000000000 --- a/lib/defaults.js +++ /dev/null @@ -1,44 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -var _parsers = require('./cli/utils/parsers'); - -var logsFolder = function () { - var folder = './logs/'; - if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { - folder = './test_logs/'; - } - if (process.env.PARSE_SERVER_LOGS_FOLDER) { - folder = (0, _parsers.nullParser)(process.env.PARSE_SERVER_LOGS_FOLDER); - } - return folder; -}(); - -var _ref = function () { - var verbose = process.env.VERBOSE ? true : false; - return { verbose: verbose, level: verbose ? 'verbose' : undefined }; -}(), - verbose = _ref.verbose, - level = _ref.level; - -exports.default = { - DefaultMongoURI: 'mongodb://localhost:27017/parse', - jsonLogs: process.env.JSON_LOGS || false, - logsFolder: logsFolder, - verbose: verbose, - level: level, - silent: false, - enableAnonymousUsers: true, - allowClientClassCreation: true, - maxUploadSize: '20mb', - verifyUserEmails: false, - preventLoginWithUnverifiedEmail: false, - sessionLength: 31536000, - expireInactiveSessions: true, - revokeSessionOnPasswordReset: true, - schemaCacheTTL: 5000, // in ms - userSensitiveFields: ['email'] -}; \ No newline at end of file diff --git a/lib/deprecated.js b/lib/deprecated.js deleted file mode 100644 index 3371f7cc51..0000000000 --- a/lib/deprecated.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.useExternal = useExternal; -function useExternal(name, moduleName) { - return function () { - throw name + " is not provided by parse-server anymore; please install " + moduleName; - }; -} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index f9fc68faa0..0000000000 --- a/lib/index.js +++ /dev/null @@ -1,69 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.ParseServer = exports.PushWorker = exports.TestUtils = exports.RedisCacheAdapter = exports.NullCacheAdapter = exports.InMemoryCacheAdapter = exports.FileSystemAdapter = exports.GCSAdapter = exports.S3Adapter = undefined; - -var _ParseServer2 = require('./ParseServer'); - -var _ParseServer3 = _interopRequireDefault(_ParseServer2); - -var _parseServerS3Adapter = require('parse-server-s3-adapter'); - -var _parseServerS3Adapter2 = _interopRequireDefault(_parseServerS3Adapter); - -var _parseServerFsAdapter = require('parse-server-fs-adapter'); - -var _parseServerFsAdapter2 = _interopRequireDefault(_parseServerFsAdapter); - -var _InMemoryCacheAdapter = require('./Adapters/Cache/InMemoryCacheAdapter'); - -var _InMemoryCacheAdapter2 = _interopRequireDefault(_InMemoryCacheAdapter); - -var _NullCacheAdapter = require('./Adapters/Cache/NullCacheAdapter'); - -var _NullCacheAdapter2 = _interopRequireDefault(_NullCacheAdapter); - -var _RedisCacheAdapter = require('./Adapters/Cache/RedisCacheAdapter'); - -var _RedisCacheAdapter2 = _interopRequireDefault(_RedisCacheAdapter); - -var _TestUtils = require('./TestUtils'); - -var TestUtils = _interopRequireWildcard(_TestUtils); - -var _deprecated = require('./deprecated'); - -var _logger = require('./logger'); - -var _PushWorker = require('./Push/PushWorker'); - -function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -// Factory function -var _ParseServer = function _ParseServer(options) { - var server = new _ParseServer3.default(options); - return server.app; -}; -// Mount the create liveQueryServer -_ParseServer.createLiveQueryServer = _ParseServer3.default.createLiveQueryServer; - -var GCSAdapter = (0, _deprecated.useExternal)('GCSAdapter', 'parse-server-gcs-adapter'); - -Object.defineProperty(module.exports, 'logger', { - get: _logger.getLogger -}); - -exports.default = _ParseServer3.default; -exports.S3Adapter = _parseServerS3Adapter2.default; -exports.GCSAdapter = GCSAdapter; -exports.FileSystemAdapter = _parseServerFsAdapter2.default; -exports.InMemoryCacheAdapter = _InMemoryCacheAdapter2.default; -exports.NullCacheAdapter = _NullCacheAdapter2.default; -exports.RedisCacheAdapter = _RedisCacheAdapter2.default; -exports.TestUtils = TestUtils; -exports.PushWorker = _PushWorker.PushWorker; -exports.ParseServer = _ParseServer; \ No newline at end of file diff --git a/lib/logger.js b/lib/logger.js deleted file mode 100644 index 641704b2da..0000000000 --- a/lib/logger.js +++ /dev/null @@ -1,46 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.setLogger = setLogger; -exports.getLogger = getLogger; - -var _defaults = require('./defaults'); - -var _defaults2 = _interopRequireDefault(_defaults); - -var _WinstonLoggerAdapter = require('./Adapters/Logger/WinstonLoggerAdapter'); - -var _LoggerController = require('./Controllers/LoggerController'); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -function defaultLogger() { - var adapter = new _WinstonLoggerAdapter.WinstonLoggerAdapter({ - logsFolder: _defaults2.default.logsFolder, - jsonLogs: _defaults2.default.jsonLogs, - verbose: _defaults2.default.verbose, - silent: _defaults2.default.silent }); - return new _LoggerController.LoggerController(adapter); -} - -var logger = defaultLogger(); - -function setLogger(aLogger) { - logger = aLogger; -} - -function getLogger() { - return logger; -} - -// for: `import logger from './logger'` -Object.defineProperty(module.exports, 'default', { - get: getLogger -}); - -// for: `import { logger } from './logger'` -Object.defineProperty(module.exports, 'logger', { - get: getLogger -}); \ No newline at end of file diff --git a/lib/middlewares.js b/lib/middlewares.js deleted file mode 100644 index 75bca9cb31..0000000000 --- a/lib/middlewares.js +++ /dev/null @@ -1,310 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.handleParseHeaders = handleParseHeaders; -exports.allowCrossDomain = allowCrossDomain; -exports.allowMethodOverride = allowMethodOverride; -exports.handleParseErrors = handleParseErrors; -exports.enforceMasterKeyAccess = enforceMasterKeyAccess; -exports.promiseEnforceMasterKeyAccess = promiseEnforceMasterKeyAccess; - -var _cache = require('./cache'); - -var _cache2 = _interopRequireDefault(_cache); - -var _logger = require('./logger'); - -var _logger2 = _interopRequireDefault(_logger); - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _Auth = require('./Auth'); - -var _Auth2 = _interopRequireDefault(_Auth); - -var _Config = require('./Config'); - -var _Config2 = _interopRequireDefault(_Config); - -var _ClientSDK = require('./ClientSDK'); - -var _ClientSDK2 = _interopRequireDefault(_ClientSDK); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -// Checks that the request is authorized for this app and checks user -// auth too. -// The bodyparser should run before this middleware. -// Adds info to the request: -// req.config - the Config for this app -// req.auth - the Auth for this request -function handleParseHeaders(req, res, next) { - var mountPathLength = req.originalUrl.length - req.url.length; - var mountPath = req.originalUrl.slice(0, mountPathLength); - var mount = req.protocol + '://' + req.get('host') + mountPath; - - var info = { - appId: req.get('X-Parse-Application-Id'), - sessionToken: req.get('X-Parse-Session-Token'), - masterKey: req.get('X-Parse-Master-Key'), - installationId: req.get('X-Parse-Installation-Id'), - clientKey: req.get('X-Parse-Client-Key'), - 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') - }; - - var basicAuth = httpAuth(req); - - if (basicAuth) { - var basicAuthAppId = basicAuth.appId; - if (_cache2.default.get(basicAuthAppId)) { - info.appId = basicAuthAppId; - info.masterKey = basicAuth.masterKey || info.masterKey; - info.javascriptKey = basicAuth.javascriptKey || info.javascriptKey; - } - } - - if (req.body) { - // Unity SDK sends a _noBody key which needs to be removed. - // Unclear at this point if action needs to be taken. - delete req.body._noBody; - } - - var fileViaJSON = false; - - if (!info.appId || !_cache2.default.get(info.appId)) { - // See if we can find the app id on the body. - if (req.body instanceof Buffer) { - // The only chance to find the app id is if this is a file - // upload that actually is a JSON body. So try to parse it. - req.body = JSON.parse(req.body); - fileViaJSON = true; - } - - if (req.body) { - delete req.body._RevocableSession; - } - - if (req.body && req.body._ApplicationId && _cache2.default.get(req.body._ApplicationId) && (!info.masterKey || _cache2.default.get(req.body._ApplicationId).masterKey === info.masterKey)) { - info.appId = req.body._ApplicationId; - info.javascriptKey = req.body._JavaScriptKey || ''; - delete req.body._ApplicationId; - delete req.body._JavaScriptKey; - // TODO: test that the REST API formats generated by the other - // SDKs are handled ok - if (req.body._ClientVersion) { - info.clientVersion = req.body._ClientVersion; - delete req.body._ClientVersion; - } - if (req.body._InstallationId) { - info.installationId = req.body._InstallationId; - delete req.body._InstallationId; - } - if (req.body._SessionToken) { - info.sessionToken = req.body._SessionToken; - delete req.body._SessionToken; - } - if (req.body._MasterKey) { - info.masterKey = req.body._MasterKey; - delete req.body._MasterKey; - } - if (req.body._ContentType) { - req.headers['content-type'] = req.body._ContentType; - delete req.body._ContentType; - } - } else { - return invalidRequest(req, res); - } - } - - if (info.clientVersion) { - info.clientSDK = _ClientSDK2.default.fromString(info.clientVersion); - } - - if (fileViaJSON) { - // We need to repopulate req.body with a buffer - var base64 = req.body.base64; - req.body = new Buffer(base64, 'base64'); - } - - info.app = _cache2.default.get(info.appId); - req.config = new _Config2.default(info.appId, mount); - req.info = info; - - var isMaster = info.masterKey === req.config.masterKey; - - if (isMaster) { - req.auth = new _Auth2.default.Auth({ config: req.config, installationId: info.installationId, isMaster: 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. - var keys = ["clientKey", "javascriptKey", "dotNetKey", "restAPIKey"]; - var oneKeyConfigured = keys.some(function (key) { - return req.config[key] !== undefined; - }); - var oneKeyMatches = keys.some(function (key) { - return req.config[key] !== undefined && info[key] === req.config[key]; - }); - - if (oneKeyConfigured && !oneKeyMatches) { - return invalidRequest(req, res); - } - - if (req.url == "/login") { - delete info.sessionToken; - } - - if (!info.sessionToken) { - req.auth = new _Auth2.default.Auth({ config: req.config, installationId: info.installationId, isMaster: false }); - next(); - return; - } - - return Promise.resolve().then(function () { - // handle the upgradeToRevocableSession path on it's own - if (info.sessionToken && req.url === '/upgradeToRevocableSession' && info.sessionToken.indexOf('r:') != 0) { - return _Auth2.default.getAuthForLegacySessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }); - } else { - return _Auth2.default.getAuthForSessionToken({ config: req.config, installationId: info.installationId, sessionToken: info.sessionToken }); - } - }).then(function (auth) { - if (auth) { - req.auth = auth; - next(); - } - }).catch(function (error) { - if (error instanceof _node2.default.Error) { - next(error); - return; - } else { - // TODO: Determine the correct error scenario. - _logger2.default.error('error getting auth for sessionToken', error); - throw new _node2.default.Error(_node2.default.Error.UNKNOWN_ERROR, error); - } - }); -} - -function httpAuth(req) { - if (!(req.req || req).headers.authorization) return; - - var header = (req.req || req).headers.authorization; - var appId, masterKey, javascriptKey; - - // parse header - var authPrefix = 'basic '; - - var match = header.toLowerCase().indexOf(authPrefix); - - if (match == 0) { - var encodedAuth = header.substring(authPrefix.length, header.length); - var credentials = decodeBase64(encodedAuth).split(':'); - - if (credentials.length == 2) { - appId = credentials[0]; - var key = credentials[1]; - - var jsKeyPrefix = 'javascript-key='; - - var matchKey = key.indexOf(jsKeyPrefix); - if (matchKey == 0) { - javascriptKey = key.substring(jsKeyPrefix.length, key.length); - } else { - masterKey = key; - } - } - } - - return { appId: appId, masterKey: masterKey, javascriptKey: javascriptKey }; -} - -function decodeBase64(str) { - return new Buffer(str, 'base64').toString(); -} - -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'); - - // intercept OPTIONS method - if ('OPTIONS' == req.method) { - res.sendStatus(200); - } else { - next(); - } -} - -function allowMethodOverride(req, res, next) { - if (req.method === 'POST' && req.body._method) { - req.originalMethod = req.method; - req.method = req.body._method; - delete req.body._method; - } - next(); -} - -function handleParseErrors(err, req, res, next) { - if (err instanceof _node2.default.Error) { - var httpStatus = void 0; - // TODO: fill out this mapping - switch (err.code) { - case _node2.default.Error.INTERNAL_SERVER_ERROR: - httpStatus = 500; - break; - case _node2.default.Error.OBJECT_NOT_FOUND: - httpStatus = 404; - break; - default: - httpStatus = 400; - } - - res.status(httpStatus); - res.json({ code: err.code, error: err.message }); - _logger2.default.error(err.message, err); - } else if (err.status && err.message) { - res.status(err.status); - res.json({ error: err.message }); - next(err); - } else { - _logger2.default.error('Uncaught internal server error.', err, err.stack); - res.status(500); - res.json({ - code: _node2.default.Error.INTERNAL_SERVER_ERROR, - message: 'Internal server error.' - }); - next(err); - } -} - -function enforceMasterKeyAccess(req, res, next) { - if (!req.auth.isMaster) { - res.status(403); - res.end('{"error":"unauthorized: master key is required"}'); - return; - } - next(); -} - -function promiseEnforceMasterKeyAccess(request) { - if (!request.auth.isMaster) { - var error = new Error(); - error.status = 403; - error.message = "unauthorized: master key is required"; - throw error; - } - return Promise.resolve(); -} - -function invalidRequest(req, res) { - res.status(403); - res.end('{"error":"unauthorized"}'); -} \ No newline at end of file diff --git a/lib/password.js b/lib/password.js deleted file mode 100644 index 729a07b272..0000000000 --- a/lib/password.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -// Tools for encrypting and decrypting passwords. -// Basically promise-friendly wrappers for bcrypt. -var bcrypt = require('bcryptjs'); - -try { - bcrypt = require('bcrypt'); -} catch (e) {} /* */ - -// Returns a promise for a hashed password string. -function hash(password) { - return bcrypt.hash(password, 10); -} - -// Returns a promise for whether this password compares to equal this -// hashed password. -function compare(password, hashedPassword) { - // Cannot bcrypt compare when one is undefined - if (!password || !hashedPassword) { - return Promise.resolve(false); - } - return bcrypt.compare(password, hashedPassword); -} - -module.exports = { - hash: hash, - compare: compare -}; \ No newline at end of file diff --git a/lib/requiredParameter.js b/lib/requiredParameter.js deleted file mode 100644 index 6b8272e016..0000000000 --- a/lib/requiredParameter.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; - -Object.defineProperty(exports, "__esModule", { - value: true -}); - -exports.default = function (errorMessage) { - throw errorMessage; -}; \ No newline at end of file diff --git a/lib/rest.js b/lib/rest.js deleted file mode 100644 index dea46ad966..0000000000 --- a/lib/rest.js +++ /dev/null @@ -1,157 +0,0 @@ -'use strict'; - -var _Auth = require('./Auth'); - -var _Auth2 = _interopRequireDefault(_Auth); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -// This file contains helpers for running operations in REST format. -// The goal is that handlers that explicitly handle an express route -// should just be shallow wrappers around things in this file, but -// these functions should not explicitly depend on the request -// object. -// This means that one of these handlers can support multiple -// routes. That's useful for the routes that do really similar -// things. - -var Parse = require('parse/node').Parse; - - -var RestQuery = require('./RestQuery'); -var RestWrite = require('./RestWrite'); -var triggers = require('./triggers'); - -function checkTriggers(className, config, types) { - return types.some(function (triggerType) { - return triggers.getTrigger(className, triggers.Types[triggerType], config.applicationId); - }); -} - -function checkLiveQuery(className, config) { - return config.liveQueryController && config.liveQueryController.hasLiveQuery(className); -} - -// Returns a promise for an object with optional keys 'results' and 'count'. -function find(config, auth, className, restWhere, restOptions, clientSDK) { - enforceRoleSecurity('find', className, auth); - return triggers.maybeRunQueryTrigger(triggers.Types.beforeFind, className, restWhere, restOptions, config, auth).then(function (result) { - restWhere = result.restWhere || restWhere; - restOptions = result.restOptions || restOptions; - var query = new RestQuery(config, auth, className, restWhere, restOptions, clientSDK); - return query.execute(); - }); -} - -// get is just like find but only queries an objectId. -var get = function get(config, auth, className, objectId, restOptions, clientSDK) { - enforceRoleSecurity('get', className, auth); - var query = new RestQuery(config, auth, className, { objectId: objectId }, 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'); - } - - if (className === '_User' && !auth.couldUpdateUserId(objectId)) { - throw new Parse.Error(Parse.Error.SESSION_MISSING, 'insufficient auth to delete user'); - } - - enforceRoleSecurity('delete', className, auth); - - var inflatedObject; - - return Promise.resolve().then(function () { - var hasTriggers = checkTriggers(className, config, ['beforeDelete', 'afterDelete']); - var hasLiveQuery = checkLiveQuery(className, config); - if (hasTriggers || hasLiveQuery || className == '_Session') { - return find(config, _Auth2.default.master(config), className, { objectId: objectId }).then(function (response) { - if (response && response.results && response.results.length) { - response.results[0].className = className; - - var cacheAdapter = config.cacheController; - cacheAdapter.user.del(response.results[0].sessionToken); - inflatedObject = Parse.Object.fromJSON(response.results[0]); - // 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(function () { - if (!auth.isMaster) { - return auth.getUserRoles(); - } else { - return; - } - }).then(function () { - 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(function () { - return triggers.maybeRunTrigger(triggers.Types.afterDelete, auth, inflatedObject, null, config); - }); -} - -// 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); - return write.execute(); -} - -// Returns a promise that contains the fields of the update that the -// REST API is supposed to return. -// Usually, this is just updatedAt. -function update(config, auth, className, objectId, restObject, clientSDK) { - enforceRoleSecurity('update', className, auth); - - return Promise.resolve().then(function () { - var hasTriggers = checkTriggers(className, config, ['beforeSave', 'afterSave']); - var hasLiveQuery = checkLiveQuery(className, config); - if (hasTriggers || hasLiveQuery) { - return find(config, _Auth2.default.master(config), className, { objectId: objectId }); - } - return Promise.resolve({}); - }).then(function (response) { - var originalRestObject; - if (response && response.results && response.results.length) { - originalRestObject = response.results[0]; - } - - var write = new RestWrite(config, auth, className, { objectId: objectId }, restObject, originalRestObject, clientSDK); - return write.execute(); - }); -} - -// 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') { - var error = 'Clients aren\'t allowed to perform the ' + method + ' operation on the installation collection.'; - throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, error); - } - } -} - -module.exports = { - create: create, - del: del, - find: find, - get: get, - update: update -}; \ No newline at end of file diff --git a/lib/triggers.js b/lib/triggers.js deleted file mode 100644 index e483034bcf..0000000000 --- a/lib/triggers.js +++ /dev/null @@ -1,437 +0,0 @@ -'use strict'; - -Object.defineProperty(exports, "__esModule", { - value: true -}); -exports.Types = undefined; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; // triggers.js - - -exports.addFunction = addFunction; -exports.addJob = addJob; -exports.addTrigger = addTrigger; -exports.removeFunction = removeFunction; -exports.removeJob = removeJob; -exports.removeTrigger = removeTrigger; -exports._unregister = _unregister; -exports._unregisterAll = _unregisterAll; -exports.getTrigger = getTrigger; -exports.triggerExists = triggerExists; -exports.getFunction = getFunction; -exports.getJob = getJob; -exports.getJobs = getJobs; -exports.getValidator = getValidator; -exports.getRequestObject = getRequestObject; -exports.getRequestQueryObject = getRequestQueryObject; -exports.getResponseObject = getResponseObject; -exports.maybeRunAfterFindTrigger = maybeRunAfterFindTrigger; -exports.maybeRunQueryTrigger = maybeRunQueryTrigger; -exports.maybeRunTrigger = maybeRunTrigger; -exports.inflate = inflate; - -var _node = require('parse/node'); - -var _node2 = _interopRequireDefault(_node); - -var _logger = require('./logger'); - -function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } - -var Types = exports.Types = { - beforeSave: 'beforeSave', - afterSave: 'afterSave', - beforeDelete: 'beforeDelete', - afterDelete: 'afterDelete', - beforeFind: 'beforeFind', - afterFind: 'afterFind' -}; - -var baseStore = function baseStore() { - var Validators = {}; - var Functions = {}; - var Jobs = {}; - var Triggers = Object.keys(Types).reduce(function (base, key) { - base[key] = {}; - return base; - }, {}); - - return Object.freeze({ - Functions: Functions, - Jobs: Jobs, - Validators: Validators, - Triggers: Triggers - }); -}; - -var _triggerStore = {}; - -function addFunction(functionName, handler, validationHandler, applicationId) { - applicationId = applicationId || _node2.default.applicationId; - _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); - _triggerStore[applicationId].Functions[functionName] = handler; - _triggerStore[applicationId].Validators[functionName] = validationHandler; -} - -function addJob(jobName, handler, applicationId) { - applicationId = applicationId || _node2.default.applicationId; - _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); - _triggerStore[applicationId].Jobs[jobName] = handler; -} - -function addTrigger(type, className, handler, applicationId) { - applicationId = applicationId || _node2.default.applicationId; - _triggerStore[applicationId] = _triggerStore[applicationId] || baseStore(); - _triggerStore[applicationId].Triggers[type][className] = handler; -} - -function removeFunction(functionName, applicationId) { - applicationId = applicationId || _node2.default.applicationId; - delete _triggerStore[applicationId].Functions[functionName]; -} - -function removeJob(jobName, applicationId) { - applicationId = applicationId || _node2.default.applicationId; - delete _triggerStore[applicationId].Jobs[jobName]; -} - -function removeTrigger(type, className, applicationId) { - applicationId = applicationId || _node2.default.applicationId; - delete _triggerStore[applicationId].Triggers[type][className]; -} - -function _unregister(appId, category, className, type) { - if (type) { - removeTrigger(className, type, appId); - delete _triggerStore[appId][category][className][type]; - } else { - delete _triggerStore[appId][category][className]; - } -} - -function _unregisterAll() { - Object.keys(_triggerStore).forEach(function (appId) { - return delete _triggerStore[appId]; - }); -} - -function getTrigger(className, triggerType, applicationId) { - if (!applicationId) { - throw "Missing ApplicationID"; - } - var manager = _triggerStore[applicationId]; - if (manager && manager.Triggers && manager.Triggers[triggerType] && manager.Triggers[triggerType][className]) { - return manager.Triggers[triggerType][className]; - } - return undefined; -} - -function triggerExists(className, type, applicationId) { - return getTrigger(className, type, applicationId) != undefined; -} - -function getFunction(functionName, applicationId) { - var manager = _triggerStore[applicationId]; - if (manager && manager.Functions) { - return manager.Functions[functionName]; - } - return undefined; -} - -function getJob(jobName, applicationId) { - var manager = _triggerStore[applicationId]; - if (manager && manager.Jobs) { - return manager.Jobs[jobName]; - } - return undefined; -} - -function getJobs(applicationId) { - var manager = _triggerStore[applicationId]; - if (manager && manager.Jobs) { - return manager.Jobs; - } - return undefined; -} - -function getValidator(functionName, applicationId) { - var manager = _triggerStore[applicationId]; - if (manager && manager.Validators) { - return manager.Validators[functionName]; - } - return undefined; -} - -function getRequestObject(triggerType, auth, parseObject, originalParseObject, config) { - var request = { - triggerName: triggerType, - object: parseObject, - master: false, - log: config.loggerController - }; - - if (originalParseObject) { - request.original = originalParseObject; - } - - if (!auth) { - return request; - } - if (auth.isMaster) { - request['master'] = true; - } - if (auth.user) { - request['user'] = auth.user; - } - if (auth.installationId) { - request['installationId'] = auth.installationId; - } - return request; -} - -function getRequestQueryObject(triggerType, auth, query, count, config) { - var request = { - triggerName: triggerType, - query: query, - master: false, - count: count, - log: config.loggerController - }; - - if (!auth) { - return request; - } - if (auth.isMaster) { - request['master'] = true; - } - if (auth.user) { - request['user'] = auth.user; - } - if (auth.installationId) { - request['installationId'] = auth.installationId; - } - return request; -} - -// Creates the response object, and uses the request object to pass data -// The API will call this with REST API formatted objects, this will -// transform them to Parse.Object instances expected by Cloud Code. -// Any changes made to the object in a beforeSave will be included. -function getResponseObject(request, resolve, reject) { - return { - success: function success(response) { - if (request.triggerName === Types.afterFind) { - if (!response) { - response = request.objects; - } - response = response.map(function (object) { - return object.toJSON(); - }); - return resolve(response); - } - // Use the JSON response - if (response && !request.object.equals(response) && request.triggerName === Types.beforeSave) { - return resolve(response); - } - response = {}; - if (request.triggerName === Types.beforeSave) { - response['object'] = request.object._getSaveJSON(); - } - return resolve(response); - }, - error: function error(code, message) { - if (!message) { - message = code; - code = _node2.default.Error.SCRIPT_FAILED; - } - var scriptError = new _node2.default.Error(code, message); - return reject(scriptError); - } - }; -} - -function userIdForLog(auth) { - return auth && auth.user ? auth.user.id : undefined; -} - -function logTriggerAfterHook(triggerType, className, input, auth) { - var cleanInput = _logger.logger.truncateLogMessage(JSON.stringify(input)); - _logger.logger.info(triggerType + ' triggered for ' + className + ' for user ' + userIdForLog(auth) + ':\n Input: ' + cleanInput, { - className: className, - triggerType: triggerType, - user: userIdForLog(auth) - }); -} - -function logTriggerSuccessBeforeHook(triggerType, className, input, result, auth) { - var cleanInput = _logger.logger.truncateLogMessage(JSON.stringify(input)); - var cleanResult = _logger.logger.truncateLogMessage(JSON.stringify(result)); - _logger.logger.info(triggerType + ' triggered for ' + className + ' for user ' + userIdForLog(auth) + ':\n Input: ' + cleanInput + '\n Result: ' + cleanResult, { - className: className, - triggerType: triggerType, - user: userIdForLog(auth) - }); -} - -function logTriggerErrorBeforeHook(triggerType, className, input, auth, error) { - var cleanInput = _logger.logger.truncateLogMessage(JSON.stringify(input)); - _logger.logger.error(triggerType + ' failed for ' + className + ' for user ' + userIdForLog(auth) + ':\n Input: ' + cleanInput + '\n Error: ' + JSON.stringify(error), { - className: className, - triggerType: triggerType, - error: error, - user: userIdForLog(auth) - }); -} - -function maybeRunAfterFindTrigger(triggerType, auth, className, objects, config) { - return new Promise(function (resolve, reject) { - var trigger = getTrigger(className, triggerType, config.applicationId); - if (!trigger) { - return resolve(); - } - var request = getRequestObject(triggerType, auth, null, null, config); - var response = getResponseObject(request, function (object) { - resolve(object); - }, function (error) { - reject(error); - }); - logTriggerSuccessBeforeHook(triggerType, className, 'AfterFind', JSON.stringify(objects), auth); - request.objects = objects.map(function (object) { - //setting the class name to transform into parse object - object.className = className; - return _node2.default.Object.fromJSON(object); - }); - var triggerPromise = trigger(request, response); - if (triggerPromise && typeof triggerPromise.then === "function") { - return triggerPromise.then(function (promiseResults) { - if (promiseResults) { - resolve(promiseResults); - } else { - return reject(new _node2.default.Error(_node2.default.Error.SCRIPT_FAILED, "AfterFind expect results to be returned in the promise")); - } - }); - } - }).then(function (results) { - logTriggerAfterHook(triggerType, className, JSON.stringify(results), auth); - return results; - }); -} - -function maybeRunQueryTrigger(triggerType, className, restWhere, restOptions, config, auth) { - var trigger = getTrigger(className, triggerType, config.applicationId); - if (!trigger) { - return Promise.resolve({ - restWhere: restWhere, - restOptions: restOptions - }); - } - - var parseQuery = new _node2.default.Query(className); - if (restWhere) { - parseQuery._where = restWhere; - } - var count = false; - if (restOptions) { - if (restOptions.include && restOptions.include.length > 0) { - parseQuery._include = restOptions.include.split(','); - } - if (restOptions.skip) { - parseQuery._skip = restOptions.skip; - } - if (restOptions.limit) { - parseQuery._limit = restOptions.limit; - } - count = !!restOptions.count; - } - var requestObject = getRequestQueryObject(triggerType, auth, parseQuery, count, config); - return Promise.resolve().then(function () { - return trigger(requestObject); - }).then(function (result) { - var queryResult = parseQuery; - if (result && result instanceof _node2.default.Query) { - queryResult = result; - } - var 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; - } - return { - restWhere: restWhere, - restOptions: restOptions - }; - }, function (err) { - if (typeof err === 'string') { - throw new _node2.default.Error(1, err); - } else { - throw err; - } - }); -} - -// To be used as part of the promise chain when saving/deleting an object -// Will resolve successfully if no trigger is configured -// 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 -function maybeRunTrigger(triggerType, auth, parseObject, originalParseObject, config) { - if (!parseObject) { - return Promise.resolve({}); - } - 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); - var response = getResponseObject(request, function (object) { - logTriggerSuccessBeforeHook(triggerType, parseObject.className, parseObject.toJSON(), object, auth); - resolve(object); - }, function (error) { - logTriggerErrorBeforeHook(triggerType, parseObject.className, parseObject.toJSON(), auth, error); - reject(error); - }); - // Force the current Parse app before the trigger - _node2.default.applicationId = config.applicationId; - _node2.default.javascriptKey = config.javascriptKey || ''; - _node2.default.masterKey = config.masterKey; - - // 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. - var triggerPromise = trigger(request, response); - if (triggerType === Types.afterSave || triggerType === Types.afterDelete) { - logTriggerAfterHook(triggerType, parseObject.className, parseObject.toJSON(), auth); - if (triggerPromise && typeof triggerPromise.then === "function") { - return triggerPromise.then(resolve, resolve); - } else { - return resolve(); - } - } - }); -} - -// Converts a REST-format object to a Parse.Object -// data is either className or an object -function inflate(data, restObject) { - var copy = (typeof data === 'undefined' ? 'undefined' : _typeof(data)) == 'object' ? data : { className: data }; - for (var key in restObject) { - copy[key] = restObject[key]; - } - return _node2.default.Object.fromJSON(copy); -} \ No newline at end of file diff --git a/lib/vendor/README.md b/lib/vendor/README.md deleted file mode 100644 index d51e8ea4ec..0000000000 --- a/lib/vendor/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# mongoUrl - -A fork of node's `url` module, with the modification that commas and colons are -allowed in hostnames. While this results in a slightly incorrect parsed result, -as the hostname field for a mongodb should be an array of replica sets, it's -good enough to let us pull out and escape the auth portion of the URL. - -See also: https://github.com/ParsePlatform/parse-server/pull/986 diff --git a/lib/vendor/mongodbUrl.js b/lib/vendor/mongodbUrl.js deleted file mode 100644 index 5b0e3b2db9..0000000000 --- a/lib/vendor/mongodbUrl.js +++ /dev/null @@ -1,928 +0,0 @@ -// A slightly patched version of node's url module, with support for mongodb:// -// uris. -// -// See https://github.com/nodejs/node/blob/master/LICENSE for licensing -// information - -'use strict'; - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; - -var punycode = require('punycode'); - -exports.parse = urlParse; -exports.resolve = urlResolve; -exports.resolveObject = urlResolveObject; -exports.format = urlFormat; - -exports.Url = Url; - -function Url() { - this.protocol = null; - this.slashes = null; - this.auth = null; - this.host = null; - this.port = null; - this.hostname = null; - this.hash = null; - this.search = null; - this.query = null; - this.pathname = null; - this.path = null; - this.href = null; -} - -// Reference: RFC 3986, RFC 1808, RFC 2396 - -// define these here so at least they only have to be -// compiled once on the first module load. -var protocolPattern = /^([a-z0-9.+-]+:)/i; -var portPattern = /:[0-9]*$/; - -// Special case for a simple path URL -var simplePathPattern = /^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/; - -var hostnameMaxLen = 255; -// protocols that can allow "unsafe" and "unwise" chars. -var unsafeProtocol = { - 'javascript': true, - 'javascript:': true -}; -// protocols that never have a hostname. -var hostlessProtocol = { - 'javascript': true, - 'javascript:': true -}; -// protocols that always contain a // bit. -var slashedProtocol = { - 'http': true, - 'http:': true, - 'https': true, - 'https:': true, - 'ftp': true, - 'ftp:': true, - 'gopher': true, - 'gopher:': true, - 'file': true, - 'file:': true -}; -var querystring = require('querystring'); - -/* istanbul ignore next: improve coverage */ -function urlParse(url, parseQueryString, slashesDenoteHost) { - if (url instanceof Url) return url; - - var u = new Url(); - u.parse(url, parseQueryString, slashesDenoteHost); - return u; -} - -/* istanbul ignore next: improve coverage */ -Url.prototype.parse = function (url, parseQueryString, slashesDenoteHost) { - if (typeof url !== 'string') { - throw new TypeError('Parameter "url" must be a string, not ' + (typeof url === 'undefined' ? 'undefined' : _typeof(url))); - } - - // Copy chrome, IE, opera backslash-handling behavior. - // Back slashes before the query string get converted to forward slashes - // See: https://code.google.com/p/chromium/issues/detail?id=25916 - var hasHash = false; - var start = -1; - var end = -1; - var rest = ''; - var lastPos = 0; - var i = 0; - for (var inWs = false, split = false; i < url.length; ++i) { - var code = url.charCodeAt(i); - - // Find first and last non-whitespace characters for trimming - var 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; - lastPos = start = i; - } else { - if (inWs) { - if (!isWs) { - end = -1; - inWs = false; - } - } else if (isWs) { - end = i; - inWs = true; - } - } - - // Only convert backslashes while we haven't seen a split character - if (!split) { - switch (code) { - 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; - } - } else if (!hasHash && code === 35 /*#*/) { - hasHash = true; - } - } - - // Check if string was non-empty (including strings with only whitespace) - if (start !== -1) { - if (lastPos === start) { - // We didn't convert any backslashes - - if (end === -1) { - if (start === 0) rest = url;else rest = url.slice(start); - } else { - rest = url.slice(start, end); - } - } else if (end === -1 && lastPos < url.length) { - // We converted some backslashes and have only part of the entire string - rest += url.slice(lastPos); - } else if (end !== -1 && lastPos < end) { - // We converted some backslashes and have only part of the entire string - rest += url.slice(lastPos, end); - } - } - - if (!slashesDenoteHost && !hasHash) { - // Try fast path regexp - var simplePath = simplePathPattern.exec(rest); - if (simplePath) { - this.path = rest; - this.href = rest; - this.pathname = simplePath[1]; - if (simplePath[2]) { - this.search = simplePath[2]; - if (parseQueryString) { - this.query = querystring.parse(this.search.slice(1)); - } else { - this.query = this.search.slice(1); - } - } else if (parseQueryString) { - this.search = ''; - this.query = {}; - } - return this; - } - } - - var proto = protocolPattern.exec(rest); - if (proto) { - proto = proto[0]; - var lowerProto = proto.toLowerCase(); - this.protocol = lowerProto; - rest = rest.slice(proto.length); - } - - // figure out if it's got a host - // user@server is *always* interpreted as a hostname, and url - // 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 /*/*/; - if (slashes && !(proto && hostlessProtocol[proto])) { - rest = rest.slice(2); - this.slashes = true; - } - } - - if (!hostlessProtocol[proto] && (slashes || proto && !slashedProtocol[proto])) { - - // there's a hostname. - // the first instance of /, ?, ;, or # ends the host. - // - // If there is an @ in the hostname, then non-host chars *are* allowed - // to the left of the last @ sign, unless some host-ending character - // comes *before* the @-sign. - // URLs are obnoxious. - // - // ex: - // http://a@b@c/ => user:a@b host:c - // http://a@b?@c => user:a host:b path:/?@c - - // v0.12 TODO(isaacs): This is not quite how Chrome does things. - // Review our test case against browsers more comprehensively. - - var hostEnd = -1; - var atSign = -1; - 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; - } - if (hostEnd !== -1) break; - } - start = 0; - if (atSign !== -1) { - this.auth = decodeURIComponent(rest.slice(0, atSign)); - start = atSign + 1; - } - if (nonHost === -1) { - this.host = rest.slice(start); - rest = ''; - } else { - this.host = rest.slice(start, nonHost); - rest = rest.slice(nonHost); - } - - // pull out port. - this.parseHost(); - - // 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 = ''; - - 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 /*]*/; - - // validate a little. - if (!ipv6Hostname) { - var result = validateHostname(this, rest, hostname); - if (result !== undefined) rest = result; - } - - if (this.hostname.length > hostnameMaxLen) { - this.hostname = ''; - } else { - // hostnames are always lower case. - this.hostname = this.hostname.toLowerCase(); - } - - if (!ipv6Hostname) { - // IDNA Support: Returns a punycoded representation of "domain". - // It only converts parts of the domain name that - // have non-ASCII characters, i.e. it doesn't matter if - // you call it with a domain that already is ASCII-only. - this.hostname = punycode.toASCII(this.hostname); - } - - var p = this.port ? ':' + this.port : ''; - var h = this.hostname || ''; - this.host = h + p; - - // strip [ and ] from the hostname - // the host field still retains them, though - if (ipv6Hostname) { - this.hostname = this.hostname.slice(1, -1); - if (rest[0] !== '/') { - rest = '/' + rest; - } - } - } - - // now rest is set to the post-host stuff. - // chop off any delim chars. - if (!unsafeProtocol[lowerProto]) { - // First, make 100% sure that any "autoEscape" chars get - // escaped, even if encodeURIComponent doesn't think they - // need to be. - var _result = autoEscapeStr(rest); - if (_result !== undefined) rest = _result; - } - - var questionIdx = -1; - var hashIdx = -1; - for (i = 0; i < rest.length; ++i) { - var _code = rest.charCodeAt(i); - if (_code === 35 /*#*/) { - this.hash = rest.slice(i); - hashIdx = i; - break; - } else if (_code === 63 /*?*/ && questionIdx === -1) { - questionIdx = i; - } - } - - if (questionIdx !== -1) { - if (hashIdx === -1) { - this.search = rest.slice(questionIdx); - this.query = rest.slice(questionIdx + 1); - } else { - this.search = rest.slice(questionIdx, hashIdx); - this.query = rest.slice(questionIdx + 1, hashIdx); - } - if (parseQueryString) { - this.query = querystring.parse(this.query); - } - } else if (parseQueryString) { - // no query string, but parseQueryString still requested - this.search = ''; - this.query = {}; - } - - var firstIdx = questionIdx !== -1 && (hashIdx === -1 || questionIdx < hashIdx) ? questionIdx : hashIdx; - if (firstIdx === -1) { - if (rest.length > 0) this.pathname = rest; - } else if (firstIdx > 0) { - this.pathname = rest.slice(0, firstIdx); - } - if (slashedProtocol[lowerProto] && this.hostname && !this.pathname) { - this.pathname = '/'; - } - - // to support http.request - if (this.pathname || this.search) { - var _p = this.pathname || ''; - var s = this.search || ''; - this.path = _p + s; - } - - // finally, reconstruct the href based on what has been validated. - this.href = this.format(); - return this; -}; - -/* istanbul ignore next: improve coverage */ -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 - lastPos > 0) { - if (i - lastPos > 63) { - self.hostname = hostname.slice(0, lastPos + 63); - return '/' + hostname.slice(lastPos + 63) + rest; - } - } - 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) { - continue; - } - // Invalid host character - self.hostname = hostname.slice(0, i); - if (i < hostname.length) return '/' + hostname.slice(i) + rest; - break; - } -} - -/* istanbul ignore next: improve coverage */ -function autoEscapeStr(rest) { - var newRest = ''; - var lastPos = 0; - for (var i = 0; i < rest.length; ++i) { - // 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; - } - } - if (lastPos === 0) return; - if (lastPos < rest.length) return newRest + rest.slice(lastPos);else return newRest; -} - -// format a parsed object into a url string -/* istanbul ignore next: improve coverage */ -function urlFormat(obj) { - // ensure it's an object, and not a string url. - // If it's an obj, this is a no-op. - // 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 === 'undefined' ? 'undefined' : _typeof(obj)) !== 'object' || obj === null) throw new TypeError('Parameter "urlObj" must be an object, not ' + obj === null ? 'null' : typeof obj === 'undefined' ? 'undefined' : _typeof(obj));else if (!(obj instanceof Url)) return Url.prototype.format.call(obj); - - return obj.format(); -} - -/* istanbul ignore next: improve coverage */ -Url.prototype.format = function () { - var auth = this.auth || ''; - if (auth) { - auth = encodeAuth(auth); - auth += '@'; - } - - var protocol = this.protocol || ''; - var pathname = this.pathname || ''; - var hash = this.hash || ''; - var host = false; - var query = ''; - - if (this.host) { - host = auth + this.host; - } else if (this.hostname) { - host = auth + (this.hostname.indexOf(':') === -1 ? this.hostname : '[' + this.hostname + ']'); - if (this.port) { - host += ':' + this.port; - } - } - - if (this.query !== null && _typeof(this.query) === 'object') query = querystring.stringify(this.query); - - var search = this.search || query && '?' + query || ''; - - 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; - } - } - if (lastPos > 0) { - if (lastPos !== pathname.length) pathname = newPathname + pathname.slice(lastPos);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) { - host = '//' + (host || ''); - if (pathname && pathname.charCodeAt(0) !== 47 /*/*/) pathname = '/' + pathname; - } else if (!host) { - host = ''; - } - - search = search.replace('#', '%23'); - - if (hash && hash.charCodeAt(0) !== 35 /*#*/) hash = '#' + hash; - if (search && search.charCodeAt(0) !== 63 /*?*/) search = '?' + search; - - return protocol + host + pathname + search + hash; -}; - -/* istanbul ignore next: improve coverage */ -function urlResolve(source, relative) { - return urlParse(source, false, true).resolve(relative); -} - -/* istanbul ignore next: improve coverage */ -Url.prototype.resolve = function (relative) { - return this.resolveObject(urlParse(relative, false, true)).format(); -}; - -/* istanbul ignore next: improve coverage */ -function urlResolveObject(source, relative) { - if (!source) return relative; - return urlParse(source, false, true).resolveObject(relative); -} - -/* istanbul ignore next: improve coverage */ -Url.prototype.resolveObject = function (relative) { - if (typeof relative === 'string') { - var rel = new Url(); - rel.parse(relative, false, true); - relative = rel; - } - - var result = new Url(); - var tkeys = Object.keys(this); - for (var tk = 0; tk < tkeys.length; tk++) { - var tkey = tkeys[tk]; - result[tkey] = this[tkey]; - } - - // hash is always overridden, no matter what. - // even href="" will remove it. - result.hash = relative.hash; - - // if the relative url is empty, then there's nothing left to do here. - if (relative.href === '') { - result.href = result.format(); - return result; - } - - // hrefs like //foo/bar always cut to the protocol. - if (relative.slashes && !relative.protocol) { - // take everything except the protocol from 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]; - } - - //urlParse appends trailing / to urls like http://www.example.com - if (slashedProtocol[result.protocol] && result.hostname && !result.pathname) { - result.path = result.pathname = '/'; - } - - result.href = result.format(); - return result; - } - - if (relative.protocol && relative.protocol !== result.protocol) { - // if it's a known url protocol, then changing - // the protocol does weird things - // first, if it's not file:, then we MUST have a host, - // and if there was a path - // to begin with, then we MUST have a path. - // if it is file:, then the host is dropped, - // because that's known to be hostless. - // anything else is assumed to be absolute. - if (!slashedProtocol[relative.protocol]) { - var keys = Object.keys(relative); - for (var v = 0; v < keys.length; v++) { - var k = keys[v]; - result[k] = relative[k]; - } - result.href = result.format(); - return result; - } - - result.protocol = relative.protocol; - if (!relative.host && !/^file:?$/.test(relative.protocol) && !hostlessProtocol[relative.protocol]) { - var _relPath = (relative.pathname || '').split('/'); - while (_relPath.length && !(relative.host = _relPath.shift())) {} - if (!relative.host) relative.host = ''; - if (!relative.hostname) relative.hostname = ''; - if (_relPath[0] !== '') _relPath.unshift(''); - if (_relPath.length < 2) _relPath.unshift(''); - result.pathname = _relPath.join('/'); - } else { - result.pathname = relative.pathname; - } - result.search = relative.search; - result.query = relative.query; - result.host = relative.host || ''; - result.auth = relative.auth; - result.hostname = relative.hostname || relative.host; - result.port = relative.port; - // to support http.request - if (result.pathname || result.search) { - var p = result.pathname || ''; - var s = result.search || ''; - result.path = p + s; - } - result.slashes = result.slashes || relative.slashes; - result.href = result.format(); - 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 removeAllDots = mustEndAbs; - 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 - // links like ../.. should be able - // to crawl up to the hostname, as well. This is strange. - // result.protocol has already been set by now. - // Later on, put the first path part into the host field. - if (psychotic) { - result.hostname = ''; - result.port = null; - if (result.host) { - if (srcPath[0] === '') srcPath[0] = result.host;else srcPath.unshift(result.host); - } - result.host = ''; - if (relative.protocol) { - relative.hostname = null; - relative.port = null; - if (relative.host) { - if (relPath[0] === '') relPath[0] = relative.host;else relPath.unshift(relative.host); - } - relative.host = null; - } - mustEndAbs = mustEndAbs && (relPath[0] === '' || srcPath[0] === ''); - } - - 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.search = relative.search; - result.query = relative.query; - srcPath = relPath; - // fall through to the dot-handling below. - } else if (relPath.length) { - // it's relative - // throw away the existing file, and take the new path instead. - if (!srcPath) srcPath = []; - srcPath.pop(); - srcPath = srcPath.concat(relPath); - result.search = relative.search; - result.query = relative.query; - } else if (relative.search !== null && relative.search !== undefined) { - // just pull out the search. - // like href='?foo'. - // Put this after the other two cases because it simplifies the booleans - if (psychotic) { - result.hostname = result.host = srcPath.shift(); - //occasionally the auth can get stuck only in host - //this especially happens in cases like - //url.resolveObject('mailto:local1@domain1', 'local2@domain2') - var authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false; - if (authInHost) { - result.auth = authInHost.shift(); - result.host = result.hostname = authInHost.shift(); - } - } - result.search = relative.search; - 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.href = result.format(); - return result; - } - - if (!srcPath.length) { - // no path at all. easy. - // we've already handled the other stuff above. - result.pathname = null; - //to support http.request - if (result.search) { - result.path = '/' + result.search; - } else { - result.path = null; - } - result.href = result.format(); - return result; - } - - // if a url ENDs in . or .., then it must get a trailing slash. - // 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 === ''; - - // strip single dots, resolve double dots to parent dir - // if the path tries to go above the root, `up` ends up > 0 - var up = 0; - for (var i = srcPath.length; i >= 0; i--) { - last = srcPath[i]; - if (last === '.') { - spliceOne(srcPath, i); - } else if (last === '..') { - spliceOne(srcPath, i); - up++; - } else if (up) { - spliceOne(srcPath, i); - up--; - } - } - - // if the path is allowed to go above the root, restore leading ..s - if (!mustEndAbs && !removeAllDots) { - for (; up--; up) { - srcPath.unshift('..'); - } - } - - if (mustEndAbs && srcPath[0] !== '' && (!srcPath[0] || srcPath[0].charAt(0) !== '/')) { - srcPath.unshift(''); - } - - if (hasTrailingSlash && srcPath.join('/').substr(-1) !== '/') { - srcPath.push(''); - } - - 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() : ''; - //occasionally the auth can get stuck only in host - //this especially happens in cases like - //url.resolveObject('mailto:local1@domain1', 'local2@domain2') - var _authInHost = result.host && result.host.indexOf('@') > 0 ? result.host.split('@') : false; - if (_authInHost) { - result.auth = _authInHost.shift(); - result.host = result.hostname = _authInHost.shift(); - } - } - - mustEndAbs = mustEndAbs || result.host && srcPath.length; - - if (mustEndAbs && !isAbsolute) { - srcPath.unshift(''); - } - - if (!srcPath.length) { - result.pathname = null; - result.path = null; - } else { - result.pathname = srcPath.join('/'); - } - - //to support request.http - if (result.pathname !== null || result.search !== null) { - result.path = (result.pathname ? result.pathname : '') + (result.search ? result.search : ''); - } - result.auth = relative.auth || result.auth; - result.slashes = result.slashes || relative.slashes; - result.href = result.format(); - return result; -}; - -/* istanbul ignore next: improve coverage */ -Url.prototype.parseHost = function () { - var host = this.host; - var port = portPattern.exec(host); - if (port) { - port = port[0]; - if (port !== ':') { - this.port = port.slice(1); - } - host = host.slice(0, host.length - port.length); - } - if (host) this.hostname = host; -}; - -// About 1.5x faster than the two-arg version of Array#splice(). -/* istanbul ignore next: improve coverage */ -function spliceOne(list, index) { - for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) { - list[i] = list[k]; - }list.pop(); -} - -var hexTable = new Array(256); -for (var i = 0; i < 256; ++i) { - hexTable[i] = '%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase(); -} /* istanbul ignore next: improve coverage */ -function encodeAuth(str) { - // faster encodeURIComponent alternative for encoding auth uri components - var out = ''; - var lastPos = 0; - for (var i = 0; i < str.length; ++i) { - var c = str.charCodeAt(i); - - // These characters do not need escaping: - // ! - . _ ~ - // ' ( ) * : - // 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) { - continue; - } - - if (i - lastPos > 0) out += str.slice(lastPos, i); - - lastPos = i + 1; - - // Other ASCII characters - if (c < 0x80) { - out += hexTable[c]; - continue; - } - - // Multi-byte characters ... - if (c < 0x800) { - 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]; - 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); - return out; -} \ No newline at end of file From e20908717d9de30100bca3220ce0a202ec46dac3 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Fri, 26 May 2017 09:44:19 +0200 Subject: [PATCH 04/29] feat: Add $regex test in $all array --- spec/MongoTransform.spec.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index c432e7960e..50f87753c5 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -319,6 +319,18 @@ describe('parseObjectToMongoObjectForCreate', () => { expect(output.ts.iso).toEqual('2017-01-18T00:00:00.000Z'); done(); }); + + it('$regex in $all list', (done) => { + var input = { + arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$regex: '^\\Qtwo\\E'}, {$regex: '^\\Qthree\\E'}]}, + }; + var outputValue = { + arrayField: {'$all': [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/]}, + }; + var output = transform.transformWhere(null, input); + jequal(outputValue.arrayField, output.arrayField); + done(); + }); }); describe('transformUpdate', () => { From d7194c7869dee98d924fbc2502593a198385dba1 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sun, 28 May 2017 22:00:17 +0200 Subject: [PATCH 05/29] test: Test regex with $all only in MongoDB --- spec/MongoTransform.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 50f87753c5..12fa1305cd 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -320,6 +320,9 @@ describe('parseObjectToMongoObjectForCreate', () => { done(); }); +}); + +describe_only_db('mongo')('parseObjectToMongoObjectForCreate', () => { it('$regex in $all list', (done) => { var input = { arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$regex: '^\\Qtwo\\E'}, {$regex: '^\\Qthree\\E'}]}, From 59a57f70f17a3f7ea54b80883c275b1a0cfed16a Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sun, 28 May 2017 22:53:14 +0200 Subject: [PATCH 06/29] Revert "test: Test regex with $all only in MongoDB" This reverts commit d7194c7869dee98d924fbc2502593a198385dba1. --- spec/MongoTransform.spec.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 12fa1305cd..50f87753c5 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -320,9 +320,6 @@ describe('parseObjectToMongoObjectForCreate', () => { done(); }); -}); - -describe_only_db('mongo')('parseObjectToMongoObjectForCreate', () => { it('$regex in $all list', (done) => { var input = { arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$regex: '^\\Qtwo\\E'}, {$regex: '^\\Qthree\\E'}]}, From 132b0d629a8c628307ef7adfdfc0efa025464bce Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Thu, 15 Jun 2017 10:15:21 +0200 Subject: [PATCH 07/29] feat: Add tests for containsAllStartingWith --- package.json | 2 +- spec/ParseQuery.spec.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 64d413baa8..5fd97f5b62 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "mime": "1.3.6", "mongodb": "2.2.26", "multer": "1.3.0", - "parse": "1.9.2", + "parse": "eduardbosch/Parse-SDK-JS#contains-all-starting-with", "parse-server-fs-adapter": "1.0.1", "parse-server-push-adapter": "1.3.0", "parse-server-s3-adapter": "1.0.6", diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index b238e24e63..5c0ffcbb25 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -343,6 +343,41 @@ describe('Parse.Query testing', () => { }); }); + it('containsAllStartingWith should match all strings that starts with string', (done) => { + + var object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); + var object2 = new Parse.Object('Object'); + object2.set('strings', ['the', 'brown', 'fox', 'jumps']); + var object3 = new Parse.Object('Object'); + object3.set('strings', ['over', 'the', 'lazy', 'dog']); + + var objectList = [object, object2, object3]; + + Parse.Object.saveAll(objectList).then(() => { + equal(objectList.length, 3); + + new Parse.Query('Object') + .containsAllStartingWith('strings', ['the', 'fox', 'lazy']) + .find() + .then(function (results) { + equal(results.length, 1); + arrayContains(results, object); + + return new Parse.Query('Object') + .containsAllStartingWith('strings', ['the', 'lazy']) + .find(); + }) + .then(function (results) { + equal(results.length, 2); + arrayContains(results, object); + arrayContains(results, object3); + + done(); + }); + }); + }); + var BoxedNumber = Parse.Object.extend({ className: "BoxedNumber" }); From ee84d53553dbd5f62adda28c8233daeaeeee8849 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Fri, 7 Jul 2017 17:23:42 +0200 Subject: [PATCH 08/29] feat: Add postgres support Thanks to @dplewis --- spec/ParseQuery.spec.js | 11 +++++++++-- .../Storage/Postgres/PostgresStorageAdapter.js | 11 ++++++++++- .../Storage/Postgres/sql/array/contains-all-regex.sql | 11 +++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 src/Adapters/Storage/Postgres/sql/array/contains-all-regex.sql diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 5c0ffcbb25..e457852a83 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -354,8 +354,8 @@ describe('Parse.Query testing', () => { var objectList = [object, object2, object3]; - Parse.Object.saveAll(objectList).then(() => { - equal(objectList.length, 3); + Parse.Object.saveAll(objectList).then((results) => { + equal(objectList.length, results.length); new Parse.Query('Object') .containsAllStartingWith('strings', ['the', 'fox', 'lazy']) @@ -373,6 +373,13 @@ describe('Parse.Query testing', () => { arrayContains(results, object); arrayContains(results, object3); + return new Parse.Query('Object') + .containsAllStartingWith('strings', ['he', 'lazy']) + .find(); + }) + .then(function (results) { + equal(results.length, 0); + done(); }); }); diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index a01ce25b31..ed0ec43d04 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -307,7 +307,15 @@ const buildWhereClause = ({ schema, query, index }) => { } if (Array.isArray(fieldValue.$all) && isArrayField) { - patterns.push(`array_contains_all($${index}:name, $${index + 1}::jsonb)`); + if (fieldValue.$all[0].$regex) { + for (let i = 0; i < fieldValue.$all.length; i += 1) { + const value = processRegexPattern(fieldValue.$all[i].$regex); + fieldValue.$all[i] = value.substring(1) + '%'; + } + patterns.push(`array_contains_all_regex($${index}:name, $${index + 1}::jsonb)`); + } else { + patterns.push(`array_contains_all($${index}:name, $${index + 1}::jsonb)`); + } values.push(fieldName, JSON.stringify(fieldValue.$all)); index += 2; } @@ -1185,6 +1193,7 @@ export class PostgresStorageAdapter { t.none(sql.array.addUnique), t.none(sql.array.remove), t.none(sql.array.containsAll), + t.none(sql.array.containsAllRegex), t.none(sql.array.contains) ]); }); diff --git a/src/Adapters/Storage/Postgres/sql/array/contains-all-regex.sql b/src/Adapters/Storage/Postgres/sql/array/contains-all-regex.sql new file mode 100644 index 0000000000..1b94d33499 --- /dev/null +++ b/src/Adapters/Storage/Postgres/sql/array/contains-all-regex.sql @@ -0,0 +1,11 @@ +CREATE OR REPLACE FUNCTION array_contains_all_regex( + "array" jsonb, + "values" jsonb +) + RETURNS boolean + LANGUAGE sql + IMMUTABLE + STRICT +AS $function$ + SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements_text("array") as elt WHERE elt LIKE ANY (SELECT jsonb_array_elements_text("values"))) as RES; +$function$; \ No newline at end of file From 52a394ecfb533a45f9370b4c2e5076d654428ecd Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 8 Jul 2017 01:30:59 +0200 Subject: [PATCH 09/29] feat: Check that all values in $all must be regex or none --- src/Controllers/DatabaseController.js | 33 +++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 40cbeddf0b..b81e219129 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -44,6 +44,30 @@ const transformObjectACL = ({ ACL, ...result }) => { const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count']; +const isStartsWith = value => { + if (!value || typeof value !== 'object' || !value.$regex) { + return false; + } + + const matches = value.$regex.match(/\^\\\\Q(.*)\\\\E/); + return !matches || matches.length == 3; +} + +const isAllValuesRegexOrNone = values => { + if (!values || !(values instanceof Array) || values.length == 0) { + return true; + } + + var startsWith = isStartsWith(values[0]); + for (var i = 1, length = values.length; i < length; ++i) { + if (startsWith != isStartsWith(values[i])) { + return false; + } + } + + return true; +} + const isSpecialQueryKey = key => { return specialQuerykeys.indexOf(key) >= 0; } @@ -104,12 +128,17 @@ const validateQuery = query => { } Object.keys(query).forEach(key => { - if (query && query[key] && query[key].$regex) { - if (typeof query[key].$options === 'string') { + if (query && query[key]) { + if (query[key].$regex && 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}`); } } + + if (!isAllValuesRegexOrNone(query[key].$all)) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $all values. All values must be of the same type: ' + + query[key].$all); + } } 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}`); From 3432fc81f696f1c89f4179a56e4d02f61221b5b0 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 8 Jul 2017 01:31:37 +0200 Subject: [PATCH 10/29] test: Check that $all vaules must be regex or none --- spec/ParseQuery.spec.js | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index e457852a83..5664779e05 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -380,6 +380,50 @@ describe('Parse.Query testing', () => { .then(function (results) { equal(results.length, 0); + return require('request-promise').get({ + url: Parse.serverURL + "/classes/Object", + json: { + where: { + strings: { + $all: [ + {$regex: '\^\\Qthe\\E'}, + {$regex: '\^\\Qlazy\\E'}, + {$regex: '\^\\Qfox\\E'}, + ] + } + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }) + .then(function (results) { + equal(results.results.length, 1); + + 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(); }); }); From 81ecf4df511d22a85cf8f6c1b5f7e4e3a613ea58 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 8 Jul 2017 10:04:50 +0200 Subject: [PATCH 11/29] feat: Update tests to use only REST API --- package.json | 2 +- spec/ParseQuery.spec.js | 92 +++++++++++++++++++++++++++-------------- 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 5fd97f5b62..64d413baa8 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "mime": "1.3.6", "mongodb": "2.2.26", "multer": "1.3.0", - "parse": "eduardbosch/Parse-SDK-JS#contains-all-starting-with", + "parse": "1.9.2", "parse-server-fs-adapter": "1.0.1", "parse-server-push-adapter": "1.3.0", "parse-server-s3-adapter": "1.0.6", diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 5664779e05..3cbacf4ef2 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -357,28 +357,27 @@ describe('Parse.Query testing', () => { Parse.Object.saveAll(objectList).then((results) => { equal(objectList.length, results.length); - new Parse.Query('Object') - .containsAllStartingWith('strings', ['the', 'fox', 'lazy']) - .find() - .then(function (results) { - equal(results.length, 1); - arrayContains(results, object); - - return new Parse.Query('Object') - .containsAllStartingWith('strings', ['the', 'lazy']) - .find(); - }) - .then(function (results) { - equal(results.length, 2); - arrayContains(results, object); - arrayContains(results, object3); - - return new Parse.Query('Object') - .containsAllStartingWith('strings', ['he', 'lazy']) - .find(); - }) + 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.length, 0); + equal(results.results.length, 1); + arrayContains(results.results, object); return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", @@ -387,8 +386,7 @@ describe('Parse.Query testing', () => { strings: { $all: [ {$regex: '\^\\Qthe\\E'}, - {$regex: '\^\\Qlazy\\E'}, - {$regex: '\^\\Qfox\\E'}, + {$regex: '\^\\Qlazy\\E'} ] } } @@ -400,7 +398,9 @@ describe('Parse.Query testing', () => { }); }) .then(function (results) { - equal(results.results.length, 1); + equal(results.results.length, 2); + arrayContains(results.results, object); + arrayContains(results.results, object3); return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", @@ -408,10 +408,8 @@ describe('Parse.Query testing', () => { where: { strings: { $all: [ - {$regex: '\^\\Qthe\\E'}, - {$regex: '\^\\Qlazy\\E'}, - {$regex: '\^\\Qfox\\E'}, - {$unknown: /unknown/} + {$regex: '\^\\Qhe\\E'}, + {$regex: '\^\\Qlazy\\E'} ] } } @@ -422,13 +420,47 @@ describe('Parse.Query testing', () => { } }); }) - .then(function () { - }, function () { + .then(function (results) { + equal(results.results.length, 0); + done(); }); }); }); + it('containsAllStartingWith values must be all of type starting with regex', (done) => { + + var object = new Parse.Object('Object'); + + 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(); + }); + }); + var BoxedNumber = Parse.Object.extend({ className: "BoxedNumber" }); From a891a0c3e0a0a03c503149a00f3de5dd74d5e51e Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 8 Jul 2017 10:42:07 +0200 Subject: [PATCH 12/29] refactor: Move $all regex check to adapter --- src/Adapters/Storage/Mongo/MongoTransform.js | 30 ++++++++++++++++++ src/Controllers/DatabaseController.js | 33 ++------------------ 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 567aaf66a5..517dcc9b5b 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -111,6 +111,30 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc return {key, value}; } +const isStartsWith = value => { + if (!value || !(value instanceof RegExp)) { + return false; + } + + const matches = value.toString().match(/\/\^\\Q.*\\E\//); + return !!matches; +} + +const isAllValuesRegexOrNone = values => { + if (values.length == 0) { + return true; + } + + var startsWith = isStartsWith(values[0]); + for (var i = 1, length = values.length; i < length; ++i) { + if (startsWith != isStartsWith(values[i])) { + return false; + } + } + + return true; +} + 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"); @@ -564,6 +588,12 @@ function transformConstraint(constraint, inArray) { 'bad ' + key + ' value'); } answer[key] = arr.map(transformInteriorAtom); + + if (!isAllValuesRegexOrNone(answer[key])) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'All $all values must be of regex type or none: ' + + answer[key]); + } + break; } case '$regex': diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index b81e219129..40cbeddf0b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -44,30 +44,6 @@ const transformObjectACL = ({ ACL, ...result }) => { const specialQuerykeys = ['$and', '$or', '_rperm', '_wperm', '_perishable_token', '_email_verify_token', '_email_verify_token_expires_at', '_account_lockout_expires_at', '_failed_login_count']; -const isStartsWith = value => { - if (!value || typeof value !== 'object' || !value.$regex) { - return false; - } - - const matches = value.$regex.match(/\^\\\\Q(.*)\\\\E/); - return !matches || matches.length == 3; -} - -const isAllValuesRegexOrNone = values => { - if (!values || !(values instanceof Array) || values.length == 0) { - return true; - } - - var startsWith = isStartsWith(values[0]); - for (var i = 1, length = values.length; i < length; ++i) { - if (startsWith != isStartsWith(values[i])) { - return false; - } - } - - return true; -} - const isSpecialQueryKey = key => { return specialQuerykeys.indexOf(key) >= 0; } @@ -128,17 +104,12 @@ const validateQuery = query => { } Object.keys(query).forEach(key => { - if (query && query[key]) { - if (query[key].$regex && typeof query[key].$options === 'string') { + 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}`); } } - - if (!isAllValuesRegexOrNone(query[key].$all)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Bad $all values. All values must be of the same type: ' - + query[key].$all); - } } 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}`); From 0dcdf831ccb55edb650d820b4c4291ec772e0e08 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 8 Jul 2017 10:55:13 +0200 Subject: [PATCH 13/29] feat: Check for valid $all values in progres --- .../Postgres/PostgresStorageAdapter.js | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index ed0ec43d04..be9feab2c2 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -307,7 +307,12 @@ const buildWhereClause = ({ schema, query, index }) => { } if (Array.isArray(fieldValue.$all) && isArrayField) { - if (fieldValue.$all[0].$regex) { + if (isAnyValueRegexStartsWith(fieldValue.$all)) { + if (!isAllValuesRegexOrNone(fieldValue.$all)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, 'All $all values must be of regex type or none: ' + + fieldValue.$all); + } + for (let i = 0; i < fieldValue.$all.length; i += 1) { const value = processRegexPattern(fieldValue.$all[i].$regex); fieldValue.$all[i] = value.substring(1) + '%'; @@ -1238,6 +1243,36 @@ function processRegexPattern(s) { return literalizeRegexPart(s); } +function isStartsWithRegex(value) { + if (!value || !(value instanceof RegExp)) { + return false; + } + + const matches = value.toString().match(/\/\^\\Q.*\\E\//); + return !!matches; +} + +function isAllValuesRegexOrNone(values) { + if (values.length == 0) { + return true; + } + + var startsWith = isStartsWithRegex(values[0]); + for (var i = 1, length = values.length; i < length; ++i) { + if (startsWith != isStartsWithRegex(values[i])) { + return false; + } + } + + return true; +} + +function isAnyValueRegexStartsWith(values) { + return values.some(function (value) { + return isStartsWithRegex(value); + }); +} + function createLiteralRegex(remaining) { return remaining.split('').map(c => { if (c.match(/[0-9a-zA-Z]/) !== null) { From d6763d36251619b60bab3f83f3441394390c1c98 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 8 Jul 2017 10:55:23 +0200 Subject: [PATCH 14/29] refactor: Update function name --- src/Adapters/Storage/Mongo/MongoTransform.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 517dcc9b5b..1f0c8b4ec3 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -111,7 +111,7 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc return {key, value}; } -const isStartsWith = value => { +const isStartsWithRegex = value => { if (!value || !(value instanceof RegExp)) { return false; } @@ -125,9 +125,9 @@ const isAllValuesRegexOrNone = values => { return true; } - var startsWith = isStartsWith(values[0]); + var startsWith = isStartsWithRegex(values[0]); for (var i = 1, length = values.length; i < length; ++i) { - if (startsWith != isStartsWith(values[i])) { + if (startsWith != isStartsWithRegex(values[i])) { return false; } } From 556787b73142471f1a4941fd6cf045afd01f651e Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 8 Jul 2017 11:25:30 +0200 Subject: [PATCH 15/29] fix: Postgres $all values regex checking --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index be9feab2c2..3f577193cc 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1248,7 +1248,7 @@ function isStartsWithRegex(value) { return false; } - const matches = value.toString().match(/\/\^\\Q.*\\E\//); + const matches = value.toString().match(/\^\\Q.*\\E/); return !!matches; } @@ -1257,9 +1257,9 @@ function isAllValuesRegexOrNone(values) { return true; } - var startsWith = isStartsWithRegex(values[0]); + var startsWith = isStartsWithRegex(values[0].$regex); for (var i = 1, length = values.length; i < length; ++i) { - if (startsWith != isStartsWithRegex(values[i])) { + if (startsWith != isStartsWithRegex(values[i].$regex)) { return false; } } From 7ca81285f0df9cb7bbe00200fcfddea6ddf9cd49 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 8 Jul 2017 12:01:11 +0200 Subject: [PATCH 16/29] fix: Check starts with as string --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 3f577193cc..f0de072276 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1244,11 +1244,11 @@ function processRegexPattern(s) { } function isStartsWithRegex(value) { - if (!value || !(value instanceof RegExp)) { + if (!value || typeof value !== 'string' || !value.startsWith('^')) { return false; } - const matches = value.toString().match(/\^\\Q.*\\E/); + const matches = value.match(/\^\\Q.*\\E/); return !!matches; } From b88d2e734859e018ef8079112aaeb17ba00e6a1e Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sun, 9 Jul 2017 12:59:02 +0200 Subject: [PATCH 17/29] fix: Define contains all regex sql function --- src/Adapters/Storage/Postgres/sql/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Adapters/Storage/Postgres/sql/index.js b/src/Adapters/Storage/Postgres/sql/index.js index 5ddfb036cf..6bcd560d13 100644 --- a/src/Adapters/Storage/Postgres/sql/index.js +++ b/src/Adapters/Storage/Postgres/sql/index.js @@ -9,6 +9,7 @@ module.exports = { addUnique: sql('array/add-unique.sql'), contains: sql('array/contains.sql'), containsAll: sql('array/contains-all.sql'), + containsAllRegex: sql('array/contains-all-regex.sql'), remove: sql('array/remove.sql') }, misc: { From bbb7e67c4c7d5be3adbccea3a89002945281559a Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sun, 9 Jul 2017 13:04:31 +0200 Subject: [PATCH 18/29] fix: Wrong value check --- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index f0de072276..769875981e 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1269,7 +1269,7 @@ function isAllValuesRegexOrNone(values) { function isAnyValueRegexStartsWith(values) { return values.some(function (value) { - return isStartsWithRegex(value); + return isStartsWithRegex(value.$regex); }); } From c36854b06db3b20d5e4c442d69f2c28448f6d807 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sun, 4 Feb 2018 20:31:44 +0100 Subject: [PATCH 19/29] fix: Check valid data --- src/Adapters/Storage/Mongo/MongoTransform.js | 2 +- src/Adapters/Storage/Postgres/PostgresStorageAdapter.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 36ed088f1f..7c95393a32 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -133,7 +133,7 @@ const isStartsWithRegex = value => { } const isAllValuesRegexOrNone = values => { - if (values.length == 0) { + if (!values || !Array.isArray(values) || values.length === 0) { return true; } diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 6c5d4f3f50..e7df95cfcc 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1799,7 +1799,7 @@ function isStartsWithRegex(value) { } function isAllValuesRegexOrNone(values) { - if (values.length == 0) { + if (!values || !Array.isArray(values) || values.length === 0) { return true; } From 1c54ede4478b29dcce1b091b09d5e3e11d2fecbb Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sun, 4 Feb 2018 20:32:09 +0100 Subject: [PATCH 20/29] fix: Check regex when there is only one value --- src/Adapters/Storage/Mongo/MongoTransform.js | 10 +++++++--- .../Storage/Postgres/PostgresStorageAdapter.js | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 7c95393a32..67969d8070 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -137,9 +137,13 @@ const isAllValuesRegexOrNone = values => { return true; } - var startsWith = isStartsWithRegex(values[0]); - for (var i = 1, length = values.length; i < length; ++i) { - if (startsWith != isStartsWithRegex(values[i])) { + const firstValuesIsRegex = isStartsWithRegex(values[0].$regex); + if (values.length === 1) { + return firstValuesIsRegex; + } + + for (let i = 1, length = values.length; i < length; ++i) { + if (firstValuesIsRegex !== isStartsWithRegex(values[i].$regex)) { return false; } } diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index e7df95cfcc..a94b25308f 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -1803,9 +1803,13 @@ function isAllValuesRegexOrNone(values) { return true; } - var startsWith = isStartsWithRegex(values[0].$regex); - for (var i = 1, length = values.length; i < length; ++i) { - if (startsWith != isStartsWithRegex(values[i].$regex)) { + const firstValuesIsRegex = isStartsWithRegex(values[0].$regex); + if (values.length === 1) { + return firstValuesIsRegex; + } + + for (let i = 1, length = values.length; i < length; ++i) { + if (firstValuesIsRegex !== isStartsWithRegex(values[i].$regex)) { return false; } } From c8fb446d4a14b43d5c06bd5faa2358dcccd19978 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sun, 4 Feb 2018 20:49:26 +0100 Subject: [PATCH 21/29] fix: Constains all starting with string returns empty with bad params --- spec/ParseQuery.spec.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 189d49a8fd..37b5adb332 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -585,6 +585,7 @@ describe('Parse.Query testing', () => { it('containsAllStartingWith values must be all of type starting with regex', (done) => { var object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); object.save().then(() => { equal(object.isNew(), false); @@ -609,9 +610,11 @@ describe('Parse.Query testing', () => { } }); }) - .then(function () { + .then(function (result) { + if (result.results.length === 0) { + done(); + } }, function () { - done(); }); }); From d6b8a74f618fa73fe4c7a586316167fb6fc8ecb6 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sun, 4 Feb 2018 21:26:13 +0100 Subject: [PATCH 22/29] fix: Pass correct regex value --- spec/ParseQuery.spec.js | 6 ++---- src/Adapters/Storage/Mongo/MongoTransform.js | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 37b5adb332..eb4d6e8031 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -610,11 +610,9 @@ describe('Parse.Query testing', () => { } }); }) - .then(function (result) { - if (result.results.length === 0) { - done(); - } + .then(function () { }, function () { + done(); }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 67969d8070..038881d87d 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -137,13 +137,13 @@ const isAllValuesRegexOrNone = values => { return true; } - const firstValuesIsRegex = isStartsWithRegex(values[0].$regex); + const firstValuesIsRegex = isStartsWithRegex(values[0]); if (values.length === 1) { return firstValuesIsRegex; } for (let i = 1, length = values.length; i < length; ++i) { - if (firstValuesIsRegex !== isStartsWithRegex(values[i].$regex)) { + if (firstValuesIsRegex !== isStartsWithRegex(values[i])) { return false; } } From 80772cf92964ad2b95689f3596fda8647b868583 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Fri, 16 Feb 2018 18:27:39 +0100 Subject: [PATCH 23/29] feat: Add missing tests --- spec/ParseQuery.spec.js | 130 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index eb4d6e8031..ceecb9c9c1 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -616,6 +616,136 @@ describe('Parse.Query testing', () => { }); }); + it('containsAllStartingWith empty array values should return empty results', (done) => { + + var object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); + + object.save().then(() => { + equal(object.isNew(), false); + + return require('request-promise').get({ + url: Parse.serverURL + "/classes/Object", + json: { + where: { + strings: { + $all: [] + } + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }) + .then(function (results) { + equal(results.results.length, 0); + done(); + }, function () { + }); + }); + + it('containsAllStartingWith single empty values must throw an error', (done) => { + + var object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); + + object.save().then(() => { + equal(object.isNew(), false); + + return require('request-promise').get({ + url: Parse.serverURL + "/classes/Object", + json: { + where: { + strings: { + $all: [ {} ] + } + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }) + .then(function () { + }, function () { + done(); + }); + }); + + it('containsAllStartingWith single regex value should return corresponding matching results', (done) => { + + var object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); + var object2 = new Parse.Object('Object'); + object2.set('strings', ['the', 'brown', 'fox', 'jumps']); + var object3 = new Parse.Object('Object'); + object3.set('strings', ['over', 'the', 'lazy', 'dog']); + + var objectList = [object, object2, object3]; + + Parse.Object.saveAll(objectList).then((results) => { + equal(objectList.length, results.length); + + return require('request-promise').get({ + url: Parse.serverURL + "/classes/Object", + json: { + where: { + strings: { + $all: [ {$regex: '\^\\Qlazy\\E'} ] + } + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }) + .then(function (results) { + equal(results.results.length, 2); + done(); + }, function () { + }); + }); + + it('containsAllStartingWith single invalid regex should throw an exception', (done) => { + + var object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); + var object2 = new Parse.Object('Object'); + object2.set('strings', ['the', 'brown', 'fox', 'jumps']); + var object3 = new Parse.Object('Object'); + object3.set('strings', ['over', 'the', 'lazy', 'dog']); + + var 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: [ {$unknown: '\^\\Qlazy\\E'} ] + } + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }) + .then(function () { + }, function () { + done(); + }); + }); + var BoxedNumber = Parse.Object.extend({ className: "BoxedNumber" }); From 4a75023c1355393b4a76bfb7d1171e29466e32df Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Fri, 16 Feb 2018 19:02:44 +0100 Subject: [PATCH 24/29] feat: Add missing tests --- spec/ParseQuery.spec.js | 112 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index ceecb9c9c1..13b8e66b0a 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -616,7 +616,7 @@ describe('Parse.Query testing', () => { }); }); - it('containsAllStartingWith empty array values should return empty results', (done) => { + it_exclude_dbs(['postgres'])('containsAllStartingWith empty array values should return empty results', (done) => { var object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); @@ -646,7 +646,43 @@ describe('Parse.Query testing', () => { }); }); - it('containsAllStartingWith single empty values must throw an error', (done) => { + it_only_db('postgres')('containsAllStartingWith empty array values should return empty results', (done) => { + + var object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); + var object2 = new Parse.Object('Object'); + object2.set('strings', ['the', 'brown', 'fox', 'jumps']); + var object3 = new Parse.Object('Object'); + object3.set('strings', ['over', 'the', 'lazy', 'dog']); + + var 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: [] + } + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }) + .then(function (results) { + equal(results.results.length, objectList.length); + done(); + }, function () { + }); + }); + + it_exclude_dbs(['postgres'])('containsAllStartingWith single empty value must throw an exception', (done) => { var object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); @@ -675,6 +711,36 @@ describe('Parse.Query testing', () => { }); }); + it_only_db('postgres')('containsAllStartingWith single empty value must throw an exception', (done) => { + + var object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); + + object.save().then(() => { + equal(object.isNew(), false); + + return require('request-promise').get({ + url: Parse.serverURL + "/classes/Object", + json: { + where: { + strings: { + $all: [ {} ] + } + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }) + .then(function (results) { + equal(results.results.length, 0); + done(); + }, function () { + }); + }); + it('containsAllStartingWith single regex value should return corresponding matching results', (done) => { var object = new Parse.Object('Object'); @@ -711,19 +777,13 @@ describe('Parse.Query testing', () => { }); }); - it('containsAllStartingWith single invalid regex should throw an exception', (done) => { + it_exclude_dbs(['postgres'])('containsAllStartingWith single invalid regex should throw an exception', (done) => { var object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - var object2 = new Parse.Object('Object'); - object2.set('strings', ['the', 'brown', 'fox', 'jumps']); - var object3 = new Parse.Object('Object'); - object3.set('strings', ['over', 'the', 'lazy', 'dog']); - var objectList = [object, object2, object3]; - - Parse.Object.saveAll(objectList).then((results) => { - equal(objectList.length, results.length); + object.save().then(() => { + equal(object.isNew(), false); return require('request-promise').get({ url: Parse.serverURL + "/classes/Object", @@ -746,6 +806,36 @@ describe('Parse.Query testing', () => { }); }); + it_only_db('postgres')('containsAllStartingWith single invalid regex should throw an exception', (done) => { + + var object = new Parse.Object('Object'); + object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); + + object.save().then(() => { + equal(object.isNew(), false); + + return require('request-promise').get({ + url: Parse.serverURL + "/classes/Object", + json: { + where: { + strings: { + $all: [ {$unknown: '\^\\Qlazy\\E'} ] + } + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Javascript-Key': Parse.javaScriptKey + } + }); + }) + .then(function (results) { + equal(results.results.length, 0); + done(); + }, function () { + }); + }); + var BoxedNumber = Parse.Object.extend({ className: "BoxedNumber" }); From efc43edb5cc7af16db8c484c9fd3fe1473860193 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Fri, 16 Feb 2018 19:03:03 +0100 Subject: [PATCH 25/29] feat: Add more tests --- spec/MongoTransform.spec.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 582ca1bd43..28abd5ed5d 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -339,10 +339,33 @@ describe('parseObjectToMongoObjectForCreate', () => { var outputValue = { arrayField: {'$all': [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/]}, }; + var output = transform.transformWhere(null, input); jequal(outputValue.arrayField, output.arrayField); done(); }); + + it('$regex in $all list must be { $regex: "string" }', (done) => { + var input = { + arrayField: {'$all': [{$regex: 1}]}, + }; + + expect(() => { + transform.transformWhere(null, input) + }).toThrow(); + done(); + }); + + it('all values in $all must be $regex (start with string) or non $regex (start with string)', (done) => { + var input = { + arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$unknown: '^\\Qtwo\\E'}]}, + }; + + expect(() => { + transform.transformWhere(null, input) + }).toThrow(); + done(); + }); }); describe('transformUpdate', () => { From cc0ed3fe62d57051e3b551756adba172aa98aa5b Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 5 May 2018 13:36:51 +0200 Subject: [PATCH 26/29] fix: Unify MongoDB and PostgreSQL functionality --- spec/ParseQuery.spec.js | 62 +------------------- src/Adapters/Storage/Mongo/MongoTransform.js | 8 ++- 2 files changed, 9 insertions(+), 61 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 13b8e66b0a..a68475f491 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -682,36 +682,7 @@ describe('Parse.Query testing', () => { }); }); - it_exclude_dbs(['postgres'])('containsAllStartingWith single empty value must throw an exception', (done) => { - - var object = new Parse.Object('Object'); - object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - - object.save().then(() => { - equal(object.isNew(), false); - - return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", - json: { - where: { - strings: { - $all: [ {} ] - } - } - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }) - .then(function () { - }, function () { - done(); - }); - }); - - it_only_db('postgres')('containsAllStartingWith single empty value must throw an exception', (done) => { + it('containsAllStartingWith single empty value returns empty results', (done) => { var object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); @@ -777,36 +748,7 @@ describe('Parse.Query testing', () => { }); }); - it_exclude_dbs(['postgres'])('containsAllStartingWith single invalid regex should throw an exception', (done) => { - - var object = new Parse.Object('Object'); - object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - - object.save().then(() => { - equal(object.isNew(), false); - - return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", - json: { - where: { - strings: { - $all: [ {$unknown: '\^\\Qlazy\\E'} ] - } - } - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }) - .then(function () { - }, function () { - done(); - }); - }); - - it_only_db('postgres')('containsAllStartingWith single invalid regex should throw an exception', (done) => { + it('containsAllStartingWith single invalid regex returns empty results', (done) => { var object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 038881d87d..58a9b71ea9 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -151,6 +151,12 @@ const isAllValuesRegexOrNone = values => { return true; } +const isAnyValueRegexStartsWith = values => { + return values.some(function (value) { + return isStartsWithRegex(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"); @@ -771,7 +777,7 @@ function transformConstraint(constraint, field) { } answer[key] = arr.map(transformInteriorAtom); - if (!isAllValuesRegexOrNone(answer[key])) { + if (isAnyValueRegexStartsWith(answer[key]) && !isAllValuesRegexOrNone(answer[key])) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'All $all values must be of regex type or none: ' + answer[key]); } From 79f1f329ce051644d4fc271356ff9f1c13526ec1 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 5 May 2018 13:56:31 +0200 Subject: [PATCH 27/29] fix: Lint checks --- spec/MongoTransform.spec.js | 10 +++++----- spec/ParseQuery.spec.js | 32 ++++++++++++++++---------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 4ebbcd95c4..ee06084f59 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -355,20 +355,20 @@ describe('parseObjectToMongoObjectForCreate', () => { }); it('$regex in $all list', (done) => { - var input = { + const input = { arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$regex: '^\\Qtwo\\E'}, {$regex: '^\\Qthree\\E'}]}, }; - var outputValue = { + const outputValue = { arrayField: {'$all': [/^\Qone\E/, /^\Qtwo\E/, /^\Qthree\E/]}, }; - var output = transform.transformWhere(null, input); + const output = transform.transformWhere(null, input); jequal(outputValue.arrayField, output.arrayField); done(); }); it('$regex in $all list must be { $regex: "string" }', (done) => { - var input = { + const input = { arrayField: {'$all': [{$regex: 1}]}, }; @@ -379,7 +379,7 @@ describe('parseObjectToMongoObjectForCreate', () => { }); it('all values in $all must be $regex (start with string) or non $regex (start with string)', (done) => { - var input = { + const input = { arrayField: {'$all': [{$regex: '^\\Qone\\E'}, {$unknown: '^\\Qtwo\\E'}]}, }; diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index a3ab7a40ea..5e06323f20 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -499,14 +499,14 @@ describe('Parse.Query testing', () => { it('containsAllStartingWith should match all strings that starts with string', (done) => { - var object = new Parse.Object('Object'); + const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - var object2 = new Parse.Object('Object'); + const object2 = new Parse.Object('Object'); object2.set('strings', ['the', 'brown', 'fox', 'jumps']); - var object3 = new Parse.Object('Object'); + const object3 = new Parse.Object('Object'); object3.set('strings', ['over', 'the', 'lazy', 'dog']); - var objectList = [object, object2, object3]; + const objectList = [object, object2, object3]; Parse.Object.saveAll(objectList).then((results) => { equal(objectList.length, results.length); @@ -584,7 +584,7 @@ describe('Parse.Query testing', () => { it('containsAllStartingWith values must be all of type starting with regex', (done) => { - var object = new Parse.Object('Object'); + const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); object.save().then(() => { @@ -618,7 +618,7 @@ describe('Parse.Query testing', () => { it_exclude_dbs(['postgres'])('containsAllStartingWith empty array values should return empty results', (done) => { - var object = new Parse.Object('Object'); + const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); object.save().then(() => { @@ -648,14 +648,14 @@ describe('Parse.Query testing', () => { it_only_db('postgres')('containsAllStartingWith empty array values should return empty results', (done) => { - var object = new Parse.Object('Object'); + const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - var object2 = new Parse.Object('Object'); + const object2 = new Parse.Object('Object'); object2.set('strings', ['the', 'brown', 'fox', 'jumps']); - var object3 = new Parse.Object('Object'); + const object3 = new Parse.Object('Object'); object3.set('strings', ['over', 'the', 'lazy', 'dog']); - var objectList = [object, object2, object3]; + const objectList = [object, object2, object3]; Parse.Object.saveAll(objectList).then((results) => { equal(objectList.length, results.length); @@ -684,7 +684,7 @@ describe('Parse.Query testing', () => { it('containsAllStartingWith single empty value returns empty results', (done) => { - var object = new Parse.Object('Object'); + const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); object.save().then(() => { @@ -714,14 +714,14 @@ describe('Parse.Query testing', () => { it('containsAllStartingWith single regex value should return corresponding matching results', (done) => { - var object = new Parse.Object('Object'); + const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - var object2 = new Parse.Object('Object'); + const object2 = new Parse.Object('Object'); object2.set('strings', ['the', 'brown', 'fox', 'jumps']); - var object3 = new Parse.Object('Object'); + const object3 = new Parse.Object('Object'); object3.set('strings', ['over', 'the', 'lazy', 'dog']); - var objectList = [object, object2, object3]; + const objectList = [object, object2, object3]; Parse.Object.saveAll(objectList).then((results) => { equal(objectList.length, results.length); @@ -750,7 +750,7 @@ describe('Parse.Query testing', () => { it('containsAllStartingWith single invalid regex returns empty results', (done) => { - var object = new Parse.Object('Object'); + const object = new Parse.Object('Object'); object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); object.save().then(() => { From da76c18b6f953ce01f453f9523304b2e5c914257 Mon Sep 17 00:00:00 2001 From: Eduard Bosch Bertran Date: Sat, 5 May 2018 14:33:27 +0200 Subject: [PATCH 28/29] fix: Test broken $regex in $all list must be { $regex: "string" } --- src/Adapters/Storage/Mongo/MongoTransform.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 0ac6541e46..9eeef66b89 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -123,8 +123,12 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc return {key, value}; } +const isRegex = value => { + return value && (value instanceof RegExp) +} + const isStartsWithRegex = value => { - if (!value || !(value instanceof RegExp)) { + if (!isRegex(value)) { return false; } @@ -151,9 +155,9 @@ const isAllValuesRegexOrNone = values => { return true; } -const isAnyValueRegexStartsWith = values => { +const isAnyValueRegex = values => { return values.some(function (value) { - return isStartsWithRegex(value); + return isRegex(value); }); } @@ -777,9 +781,10 @@ function transformConstraint(constraint, field) { } answer[key] = arr.map(transformInteriorAtom); - if (isAnyValueRegexStartsWith(answer[key]) && !isAllValuesRegexOrNone(answer[key])) { + 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: ' - + answer[key]); + + values); } break; From 07a4d691da7e50a52488946f923e39a61048888b Mon Sep 17 00:00:00 2001 From: Diamond Lewis Date: Tue, 15 May 2018 20:21:03 -0500 Subject: [PATCH 29/29] test for empty $all --- spec/ParseQuery.spec.js | 38 +------------------ .../Postgres/sql/array/contains-all-regex.sql | 5 ++- .../Postgres/sql/array/contains-all.sql | 5 ++- 3 files changed, 9 insertions(+), 39 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 1a24adb5e3..23d8ad4a8a 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -628,7 +628,7 @@ describe('Parse.Query testing', () => { }); }); - it_exclude_dbs(['postgres'])('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']); @@ -658,42 +658,6 @@ describe('Parse.Query testing', () => { }); }); - it_only_db('postgres')('containsAllStartingWith empty array values should return empty results', (done) => { - - const object = new Parse.Object('Object'); - object.set('strings', ['the', 'brown', 'lazy', 'fox', 'jumps']); - const object2 = new Parse.Object('Object'); - object2.set('strings', ['the', 'brown', 'fox', 'jumps']); - const object3 = new Parse.Object('Object'); - object3.set('strings', ['over', 'the', 'lazy', 'dog']); - - const objectList = [object, object2, object3]; - - Parse.Object.saveAll(objectList).then((results) => { - equal(objectList.length, results.length); - - return require('request-promise').get({ - url: Parse.serverURL + "/classes/Object", - json: { - where: { - strings: { - $all: [] - } - } - }, - headers: { - 'X-Parse-Application-Id': Parse.applicationId, - 'X-Parse-Javascript-Key': Parse.javaScriptKey - } - }); - }) - .then(function (results) { - equal(results.results.length, objectList.length); - done(); - }, function () { - }); - }); - it('containsAllStartingWith single empty value returns empty results', (done) => { const object = new Parse.Object('Object'); diff --git a/src/Adapters/Storage/Postgres/sql/array/contains-all-regex.sql b/src/Adapters/Storage/Postgres/sql/array/contains-all-regex.sql index 1b94d33499..7ca5853a9f 100644 --- a/src/Adapters/Storage/Postgres/sql/array/contains-all-regex.sql +++ b/src/Adapters/Storage/Postgres/sql/array/contains-all-regex.sql @@ -7,5 +7,8 @@ CREATE OR REPLACE FUNCTION array_contains_all_regex( IMMUTABLE STRICT AS $function$ - SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements_text("array") as elt WHERE elt LIKE ANY (SELECT jsonb_array_elements_text("values"))) as RES; + SELECT CASE + WHEN 0 = jsonb_array_length("values") THEN true = false + ELSE (SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements_text("array") as elt WHERE elt LIKE ANY (SELECT jsonb_array_elements_text("values"))) as RES) + END; $function$; \ No newline at end of file diff --git a/src/Adapters/Storage/Postgres/sql/array/contains-all.sql b/src/Adapters/Storage/Postgres/sql/array/contains-all.sql index 24355bc732..8db1ca0e7b 100644 --- a/src/Adapters/Storage/Postgres/sql/array/contains-all.sql +++ b/src/Adapters/Storage/Postgres/sql/array/contains-all.sql @@ -7,5 +7,8 @@ CREATE OR REPLACE FUNCTION array_contains_all( IMMUTABLE STRICT AS $function$ - SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES; + SELECT CASE + WHEN 0 = jsonb_array_length("values") THEN true = false + ELSE (SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements_text("array") as elt WHERE elt IN (SELECT jsonb_array_elements_text("values"))) as RES) + END; $function$;