diff --git a/.vscode/launch.json b/.vscode/launch.json index 09612ab2..e9eab097 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -54,6 +54,7 @@ "type": "node", "request": "attach", "name": "Attach to Port", + "protocol": "legacy", "address": "localhost", "port": 5858 } diff --git a/CHANGELOG.md b/CHANGELOG.md index 68c0a8e3..e195fab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,13 @@ ### Enhancements * Improve performance of the RPC worker for chrome debugging. +* Added Progress API `realm.syncSession.addProgressNotification` and `realm.syncSession.removeProgressNotification` +* Added additional parameter for `Realm.open` and `Realm.openAsync` for download progress notifications * Added `Realm.deleteFile` for deleting a Realm (#363). +* Added `Realm.deleteModel` for deleting a Realm model in a migration (#573). ### Bug fixes -* Adding missing TypeScript declation (#1283). +* Adding missing TypeScript definitions; Permissions (#1283), `setFeatureToken()`, and instructions (#1298). 1.11.1 Release notes (2017-9-1) ============================================================= diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b18df5e..0aa3c8cd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,4 +87,5 @@ To finish adding your new function, you will have to add your function a few pla * In `lib/index.d.ts` you add the TypeScript declaration * Documentation is added in `docs/realm.js` -* Add your function to `lib/browser/index.js` in order to enable it in the Chrome Debugger \ No newline at end of file +* Add your function to `lib/browser/index.js` in order to enable it in the Chrome Debugger +* Add an entry to `CHANGELOG.md` if applicable (Breaking changes/Enhancements/Bug fixes) diff --git a/docs/realm.js b/docs/realm.js index 05d5ead5..2bd4c314 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -132,6 +132,12 @@ class Realm { */ delete(object) {} + /** + * Deletes a Realm model, including all of its objects. + * @param {string} name - the model name + */ + deleteModel(name) {} + /** * **WARNING:** This will delete **all** objects in the Realm! */ diff --git a/docs/sync.js b/docs/sync.js index fa45aae6..1636d137 100644 --- a/docs/sync.js +++ b/docs/sync.js @@ -334,6 +334,28 @@ class Session { * @type {string} */ get state() {} + + /** + * Register a progress notification callback on a session object + * @param {string} direction - The progress direction to register for. + * Can be either: + * - `download` - report download progress + * - `upload` - report upload progress + * @param {string} mode - The progress notification mode to use for the registration. + * Can be either: + * - `reportIndefinitely` - the registration will stay active until the callback is unregistered + * - `forCurrentlyOutstandingWork` - the registration will be active until only the currently transferable bytes are synced + * @param {callback(transferred, transferable)} callback - called with the following arguments: + * - `transferred` - the current number of bytes already transferred + * - `transferable` - the total number of transferable bytes (the number of bytes already transferred plus the number of bytes pending transfer) + */ + addProgressNotification(direction, mode, progressCallback) {} + + /** Unregister a progress notification callback that was previously registered with addProgressNotification. + * Calling the function multiple times with the same callback is ignored. + * @param {callback(transferred, transferable)} callback - a previously registered progress callback + */ + removeProgressNotification(progressCallback) {} } diff --git a/lib/browser/index.js b/lib/browser/index.js index 8caff788..cb285f8e 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -131,6 +131,7 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [ // Mutating methods: util.createMethods(Realm.prototype, objectTypes.REALM, [ 'delete', + 'deleteModel', 'deleteAll', 'write', 'compact', @@ -187,7 +188,8 @@ Object.defineProperties(Realm, { }, }, _waitForDownload: { - value: function(_config, callback) { + value: function(_config, sessionCallback, callback) { + sessionCallback(); callback(); } }, diff --git a/lib/browser/session.js b/lib/browser/session.js index 34b51261..2308d8b1 100644 --- a/lib/browser/session.js +++ b/lib/browser/session.js @@ -22,7 +22,9 @@ import { keys, objectTypes } from './constants'; import { getterForProperty, createMethods } from './util'; import { deserialize } from './rpc'; -export default class Session { } +export default class Session { + +} Object.defineProperties(Session.prototype, { url: { get: getterForProperty('url') }, @@ -31,7 +33,9 @@ Object.defineProperties(Session.prototype, { createMethods(Session.prototype, objectTypes.SESSION, [ '_refreshAccessToken', - '_simulateError' + '_simulateError', + 'addProgressNotification', + 'removeProgressNotification' ]); export function createSession(realmId, info) { diff --git a/lib/browser/util.js b/lib/browser/util.js index 6f41bfc2..3c757416 100644 --- a/lib/browser/util.js +++ b/lib/browser/util.js @@ -40,7 +40,7 @@ export function createMethod(type, name, mutates) { let id = this[keys.id]; if (!realmId || !id) { - throw new TypeError(name + ' method was not called a Realm object!'); + throw new TypeError(name + ' method was not called on a Realm object!'); } if (this[keys.type] !== type) { throw new TypeError(name + ' method was called on an object of the wrong type!'); diff --git a/lib/extensions.js b/lib/extensions.js index 7b086554..3ec8f6cf 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -43,28 +43,55 @@ module.exports = function(realmConstructor) { //Add async open API Object.defineProperties(realmConstructor, getOwnPropertyDescriptors({ open(config) { - return new Promise((resolve, reject) => { - realmConstructor._waitForDownload(config, (error) => { - if (error) { - reject(error); - } - else { - try { - let syncedRealm = new this(config); - //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented - setTimeout(() => { resolve(syncedRealm); }, 1); - } catch (e) { - reject(e); + let syncSession; + let promise = new Promise((resolve, reject) => { + realmConstructor._waitForDownload(config, + (session) => { + syncSession = session; + }, + (error) => { + if (error) { + setTimeout(() => { reject(error); }, 1); } - } - }); + else { + try { + let syncedRealm = new this(config); + //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented + setTimeout(() => { resolve(syncedRealm); }, 1); + } catch (e) { + reject(e); + } + } + }); }); + + promise.progress = (callback) => { + if (syncSession) { + syncSession.addProgressNotification('download', 'forCurrentlyOutstandingWork', callback); + } + + return promise; + }; + + return promise; }, - openAsync(config, callback) { - realmConstructor._waitForDownload(config, (error) => { + openAsync(config, progressCallback, callback) { + + if (!callback) { + callback = progressCallback; + progressCallback = null; + } + + realmConstructor._waitForDownload(config, + (syncSession) => { + if (progressCallback) { + syncSession.addProgressNotification('download', 'forCurrentlyOutstandingWork', progressCallback);; + } + }, + (error) => { if (error) { - callback(error); + setTimeout(() => { callback(error); }, 1); } else { try { diff --git a/lib/index.d.ts b/lib/index.d.ts index f1f2039a..22e65558 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -332,6 +332,10 @@ declare namespace Realm.Sync { ssl_trust_certificate_path?: string; } + type ProgressNotificationCallback = (transferred: number, transferable: number) => void; + type ProgressDirection = 'download' | 'upload'; + type ProgressMode = 'reportIndefinitely' | 'forCurrentlyOutstandingWork'; + /** * Session * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Sync.Session.html } @@ -341,6 +345,9 @@ declare namespace Realm.Sync { readonly state: 'invalid' | 'active' | 'inactive'; readonly url: string; readonly user: User; + + addProgressNotification(direction: ProgressDirection, mode: ProgressMode, progressCallback: ProgressNotificationCallback): void; + removeProgressNotification(progressCallback: ProgressNotificationCallback): void; } /** @@ -367,10 +374,14 @@ declare namespace Realm.Sync { function removeAllListeners(name?: string): void; function removeListener(regex: string, name: string, changeCallback: (changeEvent: ChangeEvent) => void): void; function setLogLevel(logLevel: 'all' | 'trace' | 'debug' | 'detail' | 'info' | 'warn' | 'error' | 'fatal' | 'off'): void; + function setFeatureToken(token: string): void; + /** + * @deprecated, to be removed in 2.0 + */ function setAccessToken(accessToken: string): void; type Instruction = { - type: 'INSERT' | 'SET' | 'DELETE' | 'CLEAR' | 'LIST_SET' | 'LIST_INSERT' | 'LIST_ERASE' | 'LIST_CLEAR' | 'ADD_TYPE' | 'ADD_PROPERTIES' + type: 'INSERT' | 'SET' | 'DELETE' | 'CLEAR' | 'LIST_SET' | 'LIST_INSERT' | 'LIST_ERASE' | 'LIST_CLEAR' | 'ADD_TYPE' | 'ADD_PROPERTIES' | 'CHANGE_IDENTITY' | 'SWAP_IDENTITY' object_type: string, identity: string, values: any | undefined @@ -402,6 +413,11 @@ declare namespace Realm.Sync { } } + +interface ProgressPromise extends Promise { + progress(callback: Realm.Sync.ProgressNotificationCallback) : Promise +} + declare class Realm { static defaultPath: string; @@ -422,17 +438,20 @@ declare class Realm { */ static schemaVersion(path: string, encryptionKey?: ArrayBuffer | ArrayBufferView): number; + + /** * Open a realm asynchronously with a promise. If the realm is synced, it will be fully synchronized before it is available. * @param {Configuration} config */ - static open(config: Realm.Configuration): Promise + static open(config: Realm.Configuration): ProgressPromise; /** * Open a realm asynchronously with a callback. If the realm is synced, it will be fully synchronized before it is available. * @param {Configuration} config + * @param {ProgressNotificationCallback} progressCallback? a progress notification callback for 'download' direction and 'forCurrentlyOutstandingWork' mode * @param {Function} callback will be called when the realm is ready. */ - static openAsync(config: Realm.Configuration, callback: (error: any, realm: Realm) => void): void + static openAsync(config: Realm.Configuration, progressCallback?: Realm.Sync.ProgressNotificationCallback, callback: (error: any, realm: Realm) => void): void /** * Delete the Realm file for the given configuration. @@ -469,6 +488,11 @@ declare class Realm { */ delete(object: Realm.Object | Realm.Object[] | Realm.List | Realm.Results | any): void; + /** + * @returns void + */ + deleteModel(name: string): void; + /** * @returns void */ diff --git a/lib/permission-api.js b/lib/permission-api.js index 4ff5b843..2df3491a 100644 --- a/lib/permission-api.js +++ b/lib/permission-api.js @@ -86,7 +86,7 @@ function getSpecialPurposeRealm(user, realmName, schema) { setTimeout(() => {}, 1); if (error) { - reject(error); + setTimeout(() => reject(error), 1); } else { try { @@ -95,7 +95,7 @@ function getSpecialPurposeRealm(user, realmName, schema) { //FIXME: RN hangs here. Remove when node's makeCallback alternative is implemented (#1255) setTimeout(() => resolve(syncedRealm), 1); } catch (e) { - reject(e); + setTimeout(() => reject(e), 1); } } }); diff --git a/lib/user-methods.js b/lib/user-methods.js index 16d6c348..1b7720ab 100644 --- a/lib/user-methods.js +++ b/lib/user-methods.js @@ -28,7 +28,7 @@ function node_require(module) { function checkTypes(args, types) { args = Array.prototype.slice.call(args); for (var i = 0; i < types.length; ++i) { - if (typeof args[i] !== types[i]) { + if (args.length > i && typeof args[i] !== types[i]) { throw new TypeError('param ' + i + ' must be of type ' + types[i]); } } @@ -162,7 +162,11 @@ const staticMethods = { adminUser(token, server) { checkTypes(arguments, ['string', 'string']); - return this._adminUser(server, token); + const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + return this.createUser(server || '', uuid, token, true); }, register(server, username, password, callback) { diff --git a/scripts/test.sh b/scripts/test.sh index 75e10535..7c03fb84 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -48,7 +48,11 @@ download_server() { } start_server() { - ./object-server-for-testing/start-object-server.command & + #disabled ROS logging + sh ./object-server-for-testing/start-object-server.command &> /dev/null & + + #enabled ROS logging + #sh ./object-server-for-testing/start-object-server.command & SERVER_PID=$! } diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 2ef56650..16e71fc8 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -180,7 +180,7 @@ public: static void remove_all_listeners(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void close(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void compact(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - + static void delete_model(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); // properties static void get_empty(ContextType, ObjectType, ReturnValue &); @@ -235,6 +235,7 @@ public: {"removeAllListeners", wrap}, {"close", wrap}, {"compact", wrap}, + {"deleteModel", wrap}, }; PropertyMap const properties = { @@ -559,6 +560,17 @@ void RealmClass::delete_file(ContextType ctx, FunctionType, ObjectType this_o } +template +void RealmClass::delete_model(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 1); + ValueType value = arguments[0]; + + SharedRealm& realm = *get_internal>(this_object); + + std::string model_name = Value::validated_to_string(ctx, value, "deleteModel"); + ObjectStore::delete_data_for_object(realm->read_group(), model_name); +} + template void RealmClass::get_default_path(ContextType ctx, ObjectType object, ReturnValue &return_value) { return_value.set(realm::js::default_path()); @@ -619,11 +631,16 @@ void RealmClass::get_sync_session(ContextType ctx, ObjectType object, ReturnV template void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - validate_argument_count(argc, 2); - auto callback_function = Value::validated_to_function(ctx, arguments[1]); + validate_argument_count(argc, 2, 3); + auto config_object = Value::validated_to_object(ctx, arguments[0]); + auto callback_function = Value::validated_to_function(ctx, arguments[argc - 1]); + + ValueType session_callback = Value::from_null(ctx); + if (argc == 3) { + session_callback = Value::validated_to_function(ctx, arguments[1]); + } #if REALM_ENABLE_SYNC - auto config_object = Value::validated_to_object(ctx, arguments[0]); ValueType sync_config_value = Object::get_property(ctx, config_object, "sync"); if (!Value::is_undefined(ctx, sync_config_value)) { realm::Realm::Config config; @@ -694,6 +711,14 @@ void RealmClass::wait_for_download_completion(ContextType ctx, FunctionType, std::shared_ptr user = sync_config->user; if (user && user->state() != SyncUser::State::Error) { if (auto session = user->session_for_on_disk_path(config.path)) { + if (!Value::is_null(ctx, session_callback)) { + FunctionType session_callback_func = Value::to_function(ctx, session_callback); + auto syncSession = create_object>(ctx, new WeakSession(session)); + ValueType callback_arguments[1]; + callback_arguments[0] = syncSession; + Function::callback(protected_ctx, session_callback_func, protected_this, 1, callback_arguments); + } + if (progressFuncDefined) { session->register_progress_notifier(std::move(progressFunc), SyncSession::NotifierType::download, false); } diff --git a/src/js_sync.hpp b/src/js_sync.hpp index bd081b7e..0ff96000 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -175,6 +175,7 @@ class SessionClass : public ClassDefinition { public: std::string const name = "Session"; + using ProgressHandler = void(uint64_t transferred_bytes, uint64_t transferrable_bytes); static FunctionType create_constructor(ContextType); @@ -185,6 +186,8 @@ public: static void simulate_error(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void refresh_access_token(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + static void add_progress_notification(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &); + static void remove_progress_notification(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &); PropertyMap const properties = { {"config", {wrap, nullptr}}, @@ -195,7 +198,9 @@ public: MethodMap const methods = { {"_simulateError", wrap}, - {"_refreshAccessToken", wrap} + {"_refreshAccessToken", wrap}, + {"addProgressNotification", wrap}, + {"removeProgressNotification", wrap}, }; }; @@ -323,7 +328,83 @@ void SessionClass::refresh_access_token(ContextType ctx, FunctionType, Object } template -class SyncClass : public ClassDefinition { +void SessionClass::add_progress_notification(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 3); + + if (auto session = get_internal>(this_object)->lock()) { + + std::string direction = Value::validated_to_string(ctx, arguments[0], "direction"); + std::string mode = Value::validated_to_string(ctx, arguments[1], "mode"); + SyncSession::NotifierType notifierType; + if (direction == "download") { + notifierType = SyncSession::NotifierType::download; + } + else if (direction == "upload") { + notifierType = SyncSession::NotifierType::upload; + } + else { + throw std::invalid_argument("Invalid argument 'direction'. Only 'download' and 'upload' progress notification directions are supported"); + } + + bool is_streaming = false; + if (mode == "reportIndefinitely") { + is_streaming = true; + } + else if (mode == "forCurrentlyOutstandingWork") { + is_streaming = false; + } + else { + throw std::invalid_argument("Invalid argument 'mode'. Only 'reportIndefinitely' and 'forCurrentlyOutstandingWork' progress notification modes are supported"); + } + + auto callback_function = Value::validated_to_function(ctx, arguments[2], "callback"); + + Protected protected_callback(ctx, callback_function); + Protected protected_this(ctx, this_object); + Protected protected_ctx(Context::get_global_context(ctx)); + std::function progressFunc; + + EventLoopDispatcher progress_handler([=](uint64_t transferred_bytes, uint64_t transferrable_bytes) { + HANDLESCOPE + ValueType callback_arguments[2]; + callback_arguments[0] = Value::from_number(protected_ctx, transferred_bytes); + callback_arguments[1] = Value::from_number(protected_ctx, transferrable_bytes); + + Function::callback(protected_ctx, protected_callback, typename T::Object(), 2, callback_arguments); + }); + + progressFunc = std::move(progress_handler); + + + auto registrationToken = session->register_progress_notifier(std::move(progressFunc), notifierType, false); + + auto syncSession = create_object>(ctx, new WeakSession(session)); + PropertyAttributes attributes = ReadOnly | DontEnum | DontDelete; + Object::set_property(ctx, callback_function, "_syncSession", syncSession, attributes); + Object::set_property(ctx, callback_function, "_registrationToken", Value::from_number(protected_ctx, registrationToken), attributes); + } +} + +template +void SessionClass::remove_progress_notification(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 1); + auto callback_function = Value::validated_to_function(ctx, arguments[0], "callback"); + auto syncSessionProp = Object::get_property(ctx, callback_function, "_syncSession"); + if (Value::is_undefined(ctx, syncSessionProp) || Value::is_null(ctx, syncSessionProp)) { + return; + } + + auto syncSession = Value::validated_to_object(ctx, syncSessionProp); + auto registrationToken = Object::get_property(ctx, callback_function, "_registrationToken"); + + if (auto session = get_internal>(syncSession)->lock()) { + auto reg = Value::validated_to_number(ctx, registrationToken); + session->unregister_progress_notifier(reg); + } +} + +template +class SyncClass : public ClassDefinition { using GlobalContextType = typename T::GlobalContext; using ContextType = typename T::Context; using FunctionType = typename T::Function; @@ -343,6 +424,7 @@ public: static void set_sync_log_level(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); // private + static std::function session_bind_callback(ContextType ctx, ObjectType sync_constructor); static void populate_sync_config(ContextType, ObjectType realm_constructor, ObjectType config_object, Realm::Config&); // static properties @@ -382,6 +464,24 @@ void SyncClass::set_sync_log_level(ContextType ctx, FunctionType, ObjectType realm::SyncManager::shared().set_log_level(log_level_2); } +template +std::function SyncClass::session_bind_callback(ContextType ctx, ObjectType sync_constructor) +{ + Protected protected_ctx(Context::get_global_context(ctx)); + Protected protected_sync_constructor(ctx, sync_constructor); + return EventLoopDispatcher([protected_ctx, protected_sync_constructor](const std::string& path, const realm::SyncConfig& config, std::shared_ptr) { + HANDLESCOPE + ObjectType user_constructor = Object::validated_get_object(protected_ctx, protected_sync_constructor, "User"); + FunctionType refreshAccessToken = Object::validated_get_function(protected_ctx, user_constructor, "_refreshAccessToken"); + + ValueType arguments[3]; + arguments[0] = create_object>(protected_ctx, new SharedUser(config.user)); + arguments[1] = Value::from_string(protected_ctx, path); + arguments[2] = Value::from_string(protected_ctx, config.realm_url); + Function::call(protected_ctx, refreshAccessToken, 3, arguments); + }); +} + template void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constructor, ObjectType config_object, Realm::Config& config) { @@ -391,21 +491,8 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr } else if (!Value::is_undefined(ctx, sync_config_value)) { auto sync_config_object = Value::validated_to_object(ctx, sync_config_value); - ObjectType sync_constructor = Object::validated_get_object(ctx, realm_constructor, std::string("Sync")); - Protected protected_sync(ctx, sync_constructor); - Protected protected_ctx(Context::get_global_context(ctx)); - - EventLoopDispatcher bind([protected_ctx, protected_sync](const std::string& path, const realm::SyncConfig& config, std::shared_ptr) { - HANDLESCOPE - ObjectType user_constructor = Object::validated_get_object(protected_ctx, protected_sync, std::string("User")); - FunctionType refreshAccessToken = Object::validated_get_function(protected_ctx, user_constructor, std::string("_refreshAccessToken")); - - ValueType arguments[3]; - arguments[0] = create_object>(protected_ctx, new SharedUser(config.user)); - arguments[1] = Value::from_string(protected_ctx, path.c_str()); - arguments[2] = Value::from_string(protected_ctx, config.realm_url.c_str()); - Function::call(protected_ctx, refreshAccessToken, 3, arguments); - }); + ObjectType sync_constructor = Object::validated_get_object(ctx, realm_constructor, "Sync"); + auto bind = session_bind_callback(ctx, sync_constructor); std::function error_handler; ValueType error_func = Object::get_property(ctx, sync_config_object, "error"); @@ -455,6 +542,5 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr } } } - } // js } // realm diff --git a/tests/js/download-api-helper.js b/tests/js/download-api-helper.js index 67a697d6..5b6ac165 100644 --- a/tests/js/download-api-helper.js +++ b/tests/js/download-api-helper.js @@ -2,33 +2,49 @@ This script creates 3 new objects into a new realm. These are objects are validated to exists by the download api tests. */ 'use strict'; - +console.log("download-api-helper started"); const username = process.argv[2]; const realmName = process.argv[3]; const realmModule = process.argv[4]; var Realm = require(realmModule); -Realm.Sync.User.register('http://localhost:9080', username, 'password', (error, user) => { + +function createObjects(user) { + const config = { + sync: { user, + url: `realm://localhost:9080/~/${realmName}`, + error: err => console.log(err) + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }] + }; + + var realm = new Realm(config); + + realm.write(() => { + for (let i = 1; i <= 3; i++) { + realm.create('Dog', { name: `Lassy ${i}` }); + } + }); + + console.log("Dogs count " + realm.objects('Dog').length); + setTimeout(() => process.exit(0), 3000); +} + +Realm.Sync.User.register('http://localhost:9080', username, 'password', (error, registeredUser) => { if (error) { - console.log(error); - process.exit(-2); - } else { - const config = { - sync: { user, url: `realm://localhost:9080/~/${realmName}`, error: err => console.log(err) }, - schema: [{ name: 'Dog', properties: { name: 'string' } }] - }; - - var realm = new Realm(config); - - realm.write(() => { - for (let i = 1; i <= 3; i++) { - realm.create('Dog', { name: `Lassy ${i}` }); + const registrationError = JSON.stringify(error); + Realm.Sync.User.login('http://localhost:9080', username, 'password', (err, loggedUser) => { + if (err) { + const loginError = JSON.stringify(err); + console.error("download-api-helper failed:\n User.register() error:\n" + registrationError + "\n User.login() error:\n" + loginError); + process.exit(-2); + } + else { + createObjects(loggedUser); } }); - - console.log("Dogs count " + realm.objects('Dog').length); - setTimeout(() => process.exit(0), 3000); + } + else { + createObjects(registeredUser); } -}); - - +}); \ No newline at end of file diff --git a/tests/js/migration-tests.js b/tests/js/migration-tests.js index 017ec45e..de5cd271 100644 --- a/tests/js/migration-tests.js +++ b/tests/js/migration-tests.js @@ -158,4 +158,170 @@ module.exports = { } }); }, + + testDeleteModelMigration: function() { + const schema = [{ + name: 'TestObject', + properties: { + prop0: 'string', + prop1: 'int', + } + }]; + + var realm = new Realm({schema: schema}); + + realm.write(function() { + realm.create('TestObject', ['stringValue', 1]); + }); + + realm.close(); + + realm = new Realm({schema: [], schemaVersion: 1}); + TestCase.assertEqual(realm.schema.length, 0); // no models + realm.close(); // this won't delete the model + + realm = new Realm({schema: schema, schemaVersion: 2}); + TestCase.assertEqual(realm.objects('TestObject').length, 1); // the model objects are still there + realm.close(); + + // now delete the model explicitly, which should delete the objects too + realm = new Realm({schema: [], schemaVersion: 3, migration: function(oldRealm, newRealm) { + newRealm.deleteModel('TestObject'); + }}); + + TestCase.assertEqual(realm.schema.length, 0); // no models + + realm.close(); + + realm = new Realm({schema: schema, schemaVersion: 4}); + + TestCase.assertEqual(realm.objects('TestObject').length, 0); + + realm.close(); + }, + + testDeleteModelInSchema: function() { + const schema = [{ + name: 'TestObject', + properties: { + prop0: 'string', + prop1: 'int', + } + }]; + + var realm = new Realm({schema: schema}); + + realm.write(function() { + realm.create('TestObject', ['stringValue', 1]); + }); + + realm.close(); + + + // now delete the model explicitly, but it should remain as it's still in the schema + // only the rows should get deleted + realm = new Realm({schema: schema, schemaVersion: 1, migration: function(oldRealm, newRealm) { + newRealm.deleteModel('TestObject'); + }}); + + TestCase.assertEqual(realm.schema.length, 1); // model should remain + TestCase.assertEqual(realm.objects('TestObject').length, 0); // objects should be gone + + realm.close(); + + realm = new Realm({schema: schema, schemaVersion: 2}); + TestCase.assertEqual(realm.objects('TestObject').length, 0); + + realm.close(); + }, + + testDeleteModelIgnoreNotExisting: function() { + const schema = [{ + name: 'TestObject', + properties: { + prop0: 'string', + prop1: 'int', + } + }]; + + var realm = new Realm({schema: schema}); + + realm.write(function() { + realm.create('TestObject', ['stringValue', 1]); + }); + + realm.close(); + + // non-existing models should be ignore on delete + realm = new Realm({schema: schema, schemaVersion: 1, migration: function(oldRealm, newRealm) { + newRealm.deleteModel('NonExistingModel'); + }}); + + realm.close(); + + realm = new Realm({schema: schema, schemaVersion: 2}); + TestCase.assertEqual(realm.objects('TestObject').length, 1); + + realm.close(); + }, + + testDeleteModelWithRelationship: function() { + const ShipSchema = { + name: 'Ship', + properties: { + ship_name: 'string', + captain: 'Captain' + } + }; + + const CaptainSchema = { + name: 'Captain', + properties: { + captain_name: 'string', + ships: { type: 'linkingObjects', objectType: 'Ship', property: 'captain' } + } + }; + + var realm = new Realm({schema: [ShipSchema, CaptainSchema]}); + + realm.write(function() { + realm.create('Ship', { + ship_name: 'My Ship', + captain: { + captain_name: 'John Doe' + } + }); + }); + + TestCase.assertEqual(realm.objects('Captain').length, 1); + TestCase.assertEqual(realm.objects('Ship').length, 1); + TestCase.assertEqual(realm.objects('Ship')[0].captain.captain_name, "John Doe"); + TestCase.assertEqual(realm.objects('Captain')[0].ships[0].ship_name, "My Ship"); + + realm.close(); + + realm = new Realm({schema: [ShipSchema, CaptainSchema], schemaVersion: 1, migration: function(oldRealm, newRealm) { + TestCase.assertThrows(function(e) { + // deleting a model which is target of linkingObjects results in an exception + newRealm.deleteModel('Captain'); + console.log(e); + }, "Table is target of cross-table link columns"); + }}); + + TestCase.assertEqual(realm.objects('Captain').length, 1); + TestCase.assertEqual(realm.objects('Ship').length, 1); + + realm.close(); + + realm = new Realm({schema: [ShipSchema, CaptainSchema], schemaVersion: 2, migration: function(oldRealm, newRealm) { + // deleting a model which isn't target of linkingObjects works fine + newRealm.deleteModel('Ship'); + }}); + + TestCase.assertEqual(realm.objects('Captain').length, 1); + TestCase.assertEqual(realm.objects('Ship').length, 0); + TestCase.assertEqual(realm.objects('Captain')[0].ships.length, 0); + + realm.close(); + }, }; diff --git a/tests/js/permission-tests.js b/tests/js/permission-tests.js index 02a9d10c..c1bf831e 100644 --- a/tests/js/permission-tests.js +++ b/tests/js/permission-tests.js @@ -59,6 +59,7 @@ module.exports = { return createUsersWithTestRealms(1) .then(([user]) => { return user.applyPermissions({ userId: '*' }, `/${user.identity}/test`, 'read') + .then(wait(100)) .then(() => user.getGrantedPermissions('any')) .then(permissions => { TestCase.assertEqual(permissions[1].path, `/${user.identity}/test`); diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index ed0cb82a..153bd6e9 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -26,7 +26,7 @@ const Realm = require('realm'); const TestCase = require('./asserts'); const isNodeProccess = (typeof process === 'object' && process + '' === '[object process]'); -console.log("isnode " + isNodeProccess + " typeof " + (typeof(process) === 'object')); + function node_require(module) { return require(module); } @@ -84,13 +84,23 @@ function runOutOfProcess(nodeJsFilePath) { fs.appendFileSync(tmpFile.fd, content, { encoding: 'utf8' }); nodeArgs[0] = tmpFile.name; return new Promise((resolve, reject) => { - const child = execFile('node', nodeArgs, { cwd: tmpDir.name }, (error, stdout, stderr) => { - if (error) { - reject(new Error(`Error executing ${nodeJsFilePath} Error: ${error}`)); + try { + console.log('runOutOfProcess command\n node ' + nodeArgs.join(" ")); + const child = execFile('node', nodeArgs, { cwd: tmpDir.name }, (error, stdout, stderr) => { + if (error) { + console.error("runOutOfProcess failed\n" + error); + reject(new Error(`Running ${nodeJsFilePath} failed. error: ${error}`)); + return; + } + + console.log('runOutOfProcess success\n' + stdout); + resolve(); + }); } - resolve(); - }); - }) + catch (e) { + reject(e); + }; + }); } module.exports = { @@ -150,7 +160,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { const accessTokenRefreshed = this; @@ -186,7 +196,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { return new Promise((resolve, reject) => { @@ -239,7 +249,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { const accessTokenRefreshed = this; @@ -275,7 +285,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { return new Promise((resolve, reject) => { @@ -322,7 +332,7 @@ module.exports = { const realmName = uuid(); const expectedObjectsCount = 3; - runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) .then(() => { return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { return new Promise((resolve, reject) => { @@ -449,5 +459,191 @@ module.exports = { session._simulateError(123, 'simulated error'); }); }); - } + }, + + + testProgressNotificationsForRealmConstructor() { + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const realmName = uuid(); + + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + let realm = new Realm(config); + const progressCallback = (transferred, total) => { + resolve(); + }; + + realm.syncSession.addProgressNotification('download', 'reportIndefinitely', progressCallback); + + setTimeout(function() { + reject("Progress Notifications API failed to call progress callback for Realm constructor"); + }, 5000); + }); + }); + }); + }, + + testProgressNotificationsUnregisterForRealmConstructor() { + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const realmName = uuid(); + + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + let realm = new Realm(config); + let unregisterFunc; + + let writeDataFunc = () => { + realm.write(() => { + for (let i = 1; i <= 3; i++) { + realm.create('Dog', { name: `Lassy ${i}` }); + } + }); + } + + let syncFinished = false; + let failOnCall = false; + const progressCallback = (transferred, total) => { + if (failOnCall) { + reject(new Error("Progress callback should not be called after removeProgressNotification")); + } + + syncFinished = transferred === total; + + //unregister and write some new data. + if (syncFinished) { + failOnCall = true; + unregisterFunc(); + + //use second callback to wait for sync finished + realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', (x, y) => { + if (x === y) { + resolve(); + } + }); + writeDataFunc(); + } + }; + + realm.syncSession.addProgressNotification('upload', 'reportIndefinitely', progressCallback); + + unregisterFunc = () => { + realm.syncSession.removeProgressNotification(progressCallback); + }; + + writeDataFunc(); + }); + }); + }); + }, + + testProgressNotificationsForRealmOpen() { + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const realmName = uuid(); + + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + let progressCalled = false; + Realm.open(config) + .progress((transferred, total) => { + progressCalled = true; + }) + .then(() => { + TestCase.assertTrue(progressCalled); + resolve(); + }) + .catch((e) => reject(e)); + + setTimeout(function() { + reject("Progress Notifications API failed to call progress callback for Realm constructor"); + }, 5000); + }); + }); + }); + }, + + testProgressNotificationsForRealmOpenAsync() { + if (!isNodeProccess) { + return Promise.resolve(); + } + + const username = uuid(); + const realmName = uuid(); + + return runOutOfProcess(__dirname + '/download-api-helper.js', username, realmName, REALM_MODULE_PATH) + .then(() => { + return promisifiedLogin('http://localhost:9080', username, 'password').then(user => { + return new Promise((resolve, reject) => { + let config = { + sync: { + user, + url: `realm://localhost:9080/~/${realmName}` + }, + schema: [{ name: 'Dog', properties: { name: 'string' } }], + }; + + let progressCalled = false; + + Realm.openAsync(config, + (transferred, total) => { + progressCalled = true; + }, + (error, realm) => { + if (error) { + reject(error); + return; + } + + TestCase.assertTrue(progressCalled); + resolve(); + }); + + setTimeout(function() { + reject("Progress Notifications API failed to call progress callback for Realm constructor"); + }, 5000); + }); + }); + }); + }, } diff --git a/tests/spec/unit_tests.js b/tests/spec/unit_tests.js index 00a37752..78fc7927 100644 --- a/tests/spec/unit_tests.js +++ b/tests/spec/unit_tests.js @@ -20,6 +20,7 @@ /* eslint-disable no-console */ 'use strict'; +const isNodeProccess = (typeof process === 'object' && process + '' === '[object process]'); const fs = require('fs'); const path = require('path'); @@ -28,7 +29,11 @@ const Realm = require('realm'); const RealmTests = require('../js'); jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000; -const isDebuggerAttached = typeof v8debug === 'object'; +let isDebuggerAttached = typeof v8debug === 'object'; +if (!isDebuggerAttached && isNodeProccess) { + isDebuggerAttached = /--debug|--inspect/.test(process.execArgv.join(' ')); +} + if (isDebuggerAttached) { jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000000; }