diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e5a6490ff..99e1681d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ NOTE: This version bumps the Realm file format to version 11. It is not possible ### Enhancements * Added RemoteMongoClient functionality to `Realm.User` +* Added Push functionality to `Realm.User` ### Fixed * Added missing `SyncConfiguration.error` field in the typescript definitions. diff --git a/docs/sync.js b/docs/sync.js index 622be23b88..a248f5745b 100644 --- a/docs/sync.js +++ b/docs/sync.js @@ -569,6 +569,33 @@ class User { * @returns {Realm~RemoteMongoDB} */ remoteMongoClient(serviceName) { } + + /** + * @class Realm.User~Push Access to the operations of the push service. + */ + + /** + * Registers the provided token with this User's device. + * + * @function Realm.User~Push#register + * @param {string} token + * @returns {Promise} completed when the user is registered, or the operation fails. + */ + + /** + * Deregisters this User's device. + * + * @function Realm.User~Push#deregister + * @returns {Promise} completed when the user is deregistered, or the operation fails. + */ + + /** + * Access the operations of the push service. + * + * @param {string} serviceName + * @returns {Realm.User~Push} + */ + push(serviceName) { } } /** diff --git a/lib/app.js b/lib/app.js index 0f5880b8de..9540c2e713 100644 --- a/lib/app.js +++ b/lib/app.js @@ -18,29 +18,15 @@ "use strict"; +const {promisify} = require("./utils.js"); + const instanceMethods = { logIn(credentials) { - return new Promise((resolve, reject) => { - this._login(credentials, (user, error) => { - if (error) { - reject(error); - } else { - resolve(user); - } - }); - }); + return promisify(cb => this._login(credentials, cb)); }, removeUser() { - return new Promise((resolve, reject) => { - this._removeUser((error) => { - if (error) { - reject(error); - } else { - resolve(); - } - }); - }); + return promisify(cb => this._removeUser(cb)); }, get auth() { diff --git a/lib/browser/user.js b/lib/browser/user.js index 1095600cf3..619778b6e6 100644 --- a/lib/browser/user.js +++ b/lib/browser/user.js @@ -31,6 +31,8 @@ createMethods(User.prototype, objectTypes.USER, [ "_deleteUser", "_linkCredentials", "_callFunction", + "_pushRegister", + "_pushDeregister", ]); Object.defineProperties(User.prototype, { diff --git a/lib/email_password_provider_client_methods.js b/lib/email_password_provider_client_methods.js index cd53b253d4..2592fd7075 100644 --- a/lib/email_password_provider_client_methods.js +++ b/lib/email_password_provider_client_methods.js @@ -18,65 +18,27 @@ 'use strict'; +const {promisify} = require("./utils.js"); + const instanceMethods = { registerEmail(email, password) { - return new Promise((resolve, reject) => { - this._registerEmail(email, password, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return promisify(cb => this._registerEmail(email, password, cb)); }, confirmUser(token, token_id) { - return new Promise((resolve, reject) => { - this._confirmUser(token, token_id, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return promisify(cb => this._confirmUser(token, token_id, cb)); }, resendConfirmationEmail(email) { - return new Promise((resolve, reject) => { - this._resendConfirmationEmail(email, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return promisify(cb => this._resendConfirmationEmail(email, cb)); }, sendResetPasswordEmail(email) { - return new Promise((resolve, reject) => { - this._sendResetPasswordEmail(email, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return promisify(cb => this._sendResetPasswordEmail(email, cb)); }, resetPassword(password, token, token_id) { - return new Promise((resolve, reject) => { - this._sendResetPasswordEmail(password, token, token_id, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return promisify(cb => this._sendResetPasswordEmail(password, token_id, token_id, cb)); } }; diff --git a/lib/user.js b/lib/user.js index 3e1e02fb70..82a6f41477 100644 --- a/lib/user.js +++ b/lib/user.js @@ -19,18 +19,11 @@ "use strict"; const {RemoteMongoDBCollection} = require("./mongo_client.js"); +const {promisify} = require("./utils.js"); const instanceMethods = { linkCredentials(credentials) { - return new Promise((resolve, reject) => { - this._linkCredentials(credentials, (user, error) => { - if (error) { - reject(error); - } else { - resolve(user); - } - }); - }); + return promisify(cb => this._linkCredentials(credentials, cb)); }, callFunction(name, args, service = undefined) { @@ -48,27 +41,12 @@ const instanceMethods = { } args = cleanArgs(args); - return new Promise((resolve, reject) => { - this._callFunction(name, args, service, (result, error) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); + return promisify(cb => this._callFunction(name, args, service, cb)); }, - refreshCustomData() { - return new Promise((resolve, reject) => { - this._refreshCustomData((error) => { - if (error) { - reject(error); - } else { - resolve(this.customData); - } - }); - }); + async refreshCustomData() { + await promisify(cb => this._refreshCustomData(cb)); + return this.customData; }, remoteMongoClient(serviceName) { @@ -87,6 +65,18 @@ const instanceMethods = { }; }, + push(serviceName) { + const user = this; + return { + register(token) { + return promisify(cb => user._pushRegister(serviceName, token, cb)); + }, + deregister() { + return promisify(cb => user._pushDeregister(serviceName, cb)); + }, + }; + }, + _functionsOnService(service) { const user = this; return new Proxy({}, { diff --git a/lib/user_apikey_provider_client.js b/lib/user_apikey_provider_client.js index a50ef92bfe..2ec63b4ef7 100644 --- a/lib/user_apikey_provider_client.js +++ b/lib/user_apikey_provider_client.js @@ -18,77 +18,31 @@ "use strict"; +const {promisify} = require("./utils.js"); + const instanceMethods = { createAPIKey(name) { - return new Promise((resolve, reject) => { - this._createAPIKey(name, (apiKey, err) => { - if (err) { - reject(err); - } else { - resolve(apiKey); - } - }); - }); + return promisify(cb => this._createAPIKey(name, cb)); }, fetchAPIKey(id) { - return new Promise((resolve, reject) => { - this._fetchAPIKey(id, (apiKey, err) => { - if (err) { - reject(err); - } else { - resolve(apiKey); - } - }); - }); + return promisify(cb => this._fetchAPIKey(id, cb)); }, fetchAPIKeys() { - return new Promise((resolve, reject) => { - this._fetchAPIKeys((apiKeys, err)=> { - if (err) { - reject(err); - } else { - resolve(apiKeys); - } - }); - }); + return promisify(cb => this._fetchAPIKeys(cb)); }, deleteAPIKey(apiKeyId) { - return new Promise((resolve, reject) => { - this._deleteAPIKey(apiKeyId, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return promisify(cb => this._deleteAPIKey(apiKeyId, cb)); }, enableAPIKey(apiKeyId) { - return new Promise((resolve, reject) => { - this._enableAPIKey(apiKeyId, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return promisify(cb => this._enableAPIKey(apiKeyId, cb)); }, disableAPIKey(apiKeyId) { - return new Promise((resolve, reject) => { - this._disableAPIKey(apiKeyId, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); + return promisify(cb => this._disableAPIKey(apiKeyId, cb)); }, }; @@ -99,4 +53,4 @@ const staticMethods = { module.exports = { static: staticMethods, instance: instanceMethods, -}; \ No newline at end of file +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000000..760808539d --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,47 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2020 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +"use strict"; + + +module.exports = { + /** + * Helper to wrap callback-taking C++ function into a Promise-returning JS function + * @example + * // floop() is a wrapper method on a type with a _floop C++ method. + * function floop(how, why) { + * return promisify(cb => this._floop(how, why, cb)); + * } + */ + promisify(func) { + return new Promise((resolve, reject) => { + func((...cbargs) => { + if (cbargs.length < 1 || cbargs.length > 2) + throw Error(`invalid cbargs length ${cbargs.length}`) + let error = cbargs[cbargs.length-1]; + if (error) { + reject(error); + } else if (cbargs.length == 2) { + resolve(cbargs[0]); + } else { + resolve(); + } + }); + }); + }, +} diff --git a/realm.gypi b/realm.gypi index 5f66dc418e..d5450a2cda 100644 --- a/realm.gypi +++ b/realm.gypi @@ -144,6 +144,7 @@ "src/object-store/src/sync/remote_mongo_collection.cpp", "src/object-store/src/sync/remote_mongo_database.cpp", "src/object-store/src/sync/generic_network_transport.cpp", + "src/object-store/src/sync/push_client.cpp", "src/object-store/src/util/bson/bson.cpp", "src/object-store/src/util/bson/regular_expression.cpp", ], diff --git a/src/js_app_credentials.hpp b/src/js_app_credentials.hpp index 398cfbec21..94bcf547dc 100644 --- a/src/js_app_credentials.hpp +++ b/src/js_app_credentials.hpp @@ -140,8 +140,11 @@ template void CredentialsClass::function(ContextType ctx, ObjectType this_object, Arguments& arguments, ReturnValue& return_value) { arguments.validate_count(1); const std::string payload_json = Value::validated_to_string(ctx, arguments[0], "payload"); + const auto payload_bson = bson::parse(payload_json); + if (payload_bson.type() != bson::Bson::Type::Document) + throw std::invalid_argument("payload must be a json object"); - auto credentials = realm::app::AppCredentials::function(payload_json); + auto credentials = realm::app::AppCredentials::function(payload_bson.operator const bson::BsonDocument&()); return_value.set(create_object>(ctx, new app::AppCredentials(credentials))); } diff --git a/src/js_network_transport.hpp b/src/js_network_transport.hpp index 1944c56179..264766e787 100644 --- a/src/js_network_transport.hpp +++ b/src/js_network_transport.hpp @@ -216,7 +216,7 @@ struct JavaScriptNetworkTransport : public JavaScriptNetworkTransportWrapper case app::HttpMethod::get: return "GET"; case app::HttpMethod::put: return "PUT"; case app::HttpMethod::post: return "POST"; - case app::HttpMethod::del: return "DEL"; + case app::HttpMethod::del: return "DELETE"; case app::HttpMethod::patch: return "PATCH"; default: throw std::runtime_error("Unknown HttpMethod argument"); } diff --git a/src/js_user.hpp b/src/js_user.hpp index 5b5ddce3cc..0aa07541f2 100644 --- a/src/js_user.hpp +++ b/src/js_user.hpp @@ -96,6 +96,8 @@ class UserClass : public ClassDefinition> { static void link_credentials(ContextType, ObjectType, Arguments&, ReturnValue&); static void call_function(ContextType, ObjectType, Arguments&, ReturnValue&); static void refresh_custom_data(ContextType, ObjectType, Arguments&, ReturnValue&); + static void push_register(ContextType, ObjectType, Arguments&, ReturnValue&); + static void push_deregister(ContextType, ObjectType, Arguments&, ReturnValue&); MethodMap const methods = { @@ -104,6 +106,8 @@ class UserClass : public ClassDefinition> { {"_linkCredentials", wrap}, {"_callFunction", wrap}, {"_refreshCustomData", wrap}, + {"_pushRegister", wrap}, + {"_pushDeregister", wrap}, }; }; @@ -303,5 +307,52 @@ void UserClass::refresh_custom_data(ContextType ctx, ObjectType this_object, })); } +template +void UserClass::push_register(ContextType ctx, ObjectType this_object, Arguments& args, ReturnValue &return_value) { + args.validate_count(3); + auto user = get_internal>(ctx, this_object); + auto service = Value::validated_to_string(ctx, args[0], "service"); + auto token = Value::validated_to_string(ctx, args[1], "token"); + auto callback = Value::validated_to_function(ctx, args[2], "callback"); + + user->m_app->push_notification_client(service).register_device( + token, + *user, + realm::util::EventLoopDispatcher([ctx = Protected(Context::get_global_context(ctx)), + callback = Protected(ctx, callback), + this_object = Protected(ctx, this_object)] + (util::Optional error) { + HANDLESCOPE(ctx); + Function::callback(ctx, callback, this_object, { + !error ? Value::from_undefined(ctx) : Object::create_obj(ctx, { + {"message", Value::from_string(ctx, error->message)}, + {"code", Value::from_number(ctx, error->error_code.value())}, + }), + }); + })); +} + +template +void UserClass::push_deregister(ContextType ctx, ObjectType this_object, Arguments& args, ReturnValue &return_value) { + args.validate_count(2); + auto user = get_internal>(ctx, this_object); + auto service = Value::validated_to_string(ctx, args[0], "service"); + auto callback = Value::validated_to_function(ctx, args[1], "callback"); + + user->m_app->push_notification_client(service).deregister_device( + *user, + realm::util::EventLoopDispatcher([ctx = Protected(Context::get_global_context(ctx)), + callback = Protected(ctx, callback), + this_object = Protected(ctx, this_object)] + (util::Optional error) { + HANDLESCOPE(ctx); + Function::callback(ctx, callback, this_object, { + !error ? Value::from_undefined(ctx) : Object::create_obj(ctx, { + {"message", Value::from_string(ctx, error->message)}, + {"code", Value::from_number(ctx, error->error_code.value())}, + }), + }); + })); +} } } diff --git a/src/object-store b/src/object-store index c02707bc28..e1570f8d3d 160000 --- a/src/object-store +++ b/src/object-store @@ -1 +1 @@ -Subproject commit c02707bc28e1886970c5da29ef481dc0cb6c3dd8 +Subproject commit e1570f8d3d7cf4d77f049933e6a241a501301383 diff --git a/tests/js/user-tests.js b/tests/js/user-tests.js index 00db5ac5e9..3baabe03eb 100644 --- a/tests/js/user-tests.js +++ b/tests/js/user-tests.js @@ -237,6 +237,23 @@ module.exports = { TestCase.assertEqual(await collection.count({hello: "pineapple"}), 0); }, + async testPush() { + let app = new Realm.App(appConfig); + let credentials = Realm.Credentials.anonymous(); + let user = await app.logIn(credentials); + + let push = user.push('gcm'); + + await push.deregister(); // deregister never registered not an error + await push.register("hello"); + await push.register("hello"); // double register not an error + await push.deregister(); + await push.deregister(); // double deregister not an error + + const err = await TestCase.assertThrowsAsync(async() => await user.push('nonesuch').register('hello')) + TestCase.assertEqual(err.message, "service not found: 'nonesuch'"); + }, + async testAllWithAnonymous() { let app = new Realm.App(appConfig); await logOutExistingUsers(app); diff --git a/types/index.d.ts b/types/index.d.ts index 870294d059..e178e9b762 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -371,6 +371,10 @@ declare namespace Realm { linkCredentials(credentials: Credentials): Promise; callFunction(name: string, args: any[]): Promise; refreshCustomData(): Promise; + push(serviceName: string): { + register(token: string): Promise, + deregister(): Promise, + }; readonly apiKeys: Realm.Auth.APIKeys; }