diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b6bd148..5a7b9e68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,15 @@ -2.0.0 Release notes (2017-9-29) +X.Y.Z-rc Release notes +============================================================= +### Breaking changes +* None + +### Enhancements +* Support migration from Realms sync 1.0 to sync 2.0 versions + +### Bug fixes +* None + +2.0.0-rc16 Release notes (2017-9-29) ============================================================= ### Breaking changes * Upgtading to Realm Core 4.0.1 (bug fixes) @@ -10,7 +21,7 @@ * Upgrading to Realm Core 4.0.0 and Realm Sync 2.0.0-rc25. ### Enhancements -* None. +* None ### Bug fixes * Configuration of sync file system is not done on module import but later when actually needed by sync (#1351) diff --git a/dependencies.list b/dependencies.list index ccf0679e..a693d003 100644 --- a/dependencies.list +++ b/dependencies.list @@ -2,4 +2,4 @@ PACKAGE_NAME=realm-js VERSION=2.0.0-rc16 REALM_CORE_VERSION=4.0.1 REALM_SYNC_VERSION=2.0.0-rc26 -REALM_OBJECT_SERVER_VERSION=2.0.0-alpha.42 +REALM_OBJECT_SERVER_VERSION=2.0.0-alpha.44 diff --git a/docs/realm.js b/docs/realm.js index 8a3aa209..279f9ee0 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -88,6 +88,7 @@ class Realm { * migrated to use the new schema. * @param {Realm~Configuration} [config] - **Required** when first creating the Realm. * @throws {Error} If anything in the provided `config` is invalid. + * @throws {IncompatibleSyncedRealmError} when an incompatible synced Realm is opened */ constructor(config) {} @@ -105,7 +106,8 @@ class Realm { * @param {Realm~Configuration} config * @param {callback(error, realm)} - will be called when the Realm is ready. * @param {callback(transferred, transferable)} [progressCallback] - an optional callback for download progress notifications - * @throws {Error} If anything in the provided `config` is invalid. + * @throws {Error} If anything in the provided `config` is invalid + * @throws {IncompatibleSyncedRealmError} when an incompatible synced Realm is opened */ static openAsync(config, callback, progressCallback) {} diff --git a/docs/sync.js b/docs/sync.js index 09ffc302..d684fb36 100644 --- a/docs/sync.js +++ b/docs/sync.js @@ -17,6 +17,13 @@ //////////////////////////////////////////////////////////////////////////// /** + * When opening a Realm created with Realm Mobile Platform v1.x, it is automatically + * migration to format of Realm Mobile Plarform v2.x. In the case where this migration + * is not possible, an exception is thrown. The exception´s `message` property will be equal + * to `IncompatibleSyncedRealmException`. The Realm is backed up, and the property `configuration` + * is a {Realm~Configuration} which refers to it. You can open it as a local, read-only Realm, and + * copy objects to a new synced Realm. + * * @memberof Realm */ class Sync { @@ -122,6 +129,23 @@ class AuthError extends Error { get type() {} } +/** + * Describes an error when an incompatible synced Realm is opened. The old version of the Realm can be accessed in readonly mode using the configuration() member + * @memberof Realm.Sync + */ +class IncompatibleSyncedRealmError { + /** + * The name of the error is 'IncompatibleSyncedRealmError' + */ + get name() {} + + /** + * The {Realm~Configuration} of the backed up Realm. + * @type {Realm~Configuration} + */ + get configuration() {} +} + /** * Class for logging in and managing Sync users. * @memberof Realm.Sync diff --git a/lib/browser/rpc.js b/lib/browser/rpc.js index f71eeac5..37c219f1 100644 --- a/lib/browser/rpc.js +++ b/lib/browser/rpc.js @@ -212,6 +212,23 @@ function makeRequest(url, data) { return JSON.parse(responseText); } +//returns an object from rpc serialized json value +function deserialize_json_value(value) { + let result = {}; + for (let index = 0; index < value.keys.length; index++) { + var propName = value.keys[index]; + var propValue = value.values[index]; + if (propValue.type && propValue.type == 'dict') { + result[propName] = deserialize_json_value(propValue); + } + else { + result[propName] = propValue.value; + } + } + + return result; +} + function sendRequest(command, data, host = sessionHost) { if (!host) { throw new Error('Must first create RPC session with a valid host'); @@ -226,9 +243,21 @@ function sendRequest(command, data, host = sessionHost) { let error = response && response.error; // Remove the type prefix from the error message (e.g. "Error: "). - if (error) { + if (error && error.replace) { error = error.replace(/^[a-z]+: /i, ''); } + else if (error.type && error.type === 'dict') { + const responseError = deserialize_json_value(error); + let responeMessage; + if (response.message && response.message !== '') { + // Remove the type prefix from the error message (e.g. "Error: "). + responeMessage = response.message.replace(/^[a-z]+: /i, ''); + } + + const exceptionToReport = new Error(responeMessage); + Object.assign(exceptionToReport, responseError); + throw exceptionToReport; + } throw new Error(error || `Invalid response for "${command}"`); } diff --git a/lib/errors.js b/lib/errors.js index f2c55fe2..197d5ba4 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -20,7 +20,7 @@ function AuthError(problem) { const error = Error.call(this, problem.title); - + this.name = 'AuthError'; this.message = error.message; this.stack = error.stack; diff --git a/lib/extensions.js b/lib/extensions.js index 61cf1021..e6033fc9 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -17,7 +17,7 @@ //////////////////////////////////////////////////////////////////////////// 'use strict'; - + let getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function(obj) { return Object.getOwnPropertyNames(obj).reduce(function (descriptors, name) { descriptors[name] = Object.getOwnPropertyDescriptor(obj, name); diff --git a/src/js_realm.hpp b/src/js_realm.hpp index ad8775e5..f8454d0b 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -252,6 +252,26 @@ public: }; private: + static void handleRealmFileException(ContextType ctx, realm::Realm::Config config, const RealmFileException& ex) { + switch (ex.kind()) { + case RealmFileException::Kind::IncompatibleSyncedRealm: { + ObjectType configuration = Object::create_empty(ctx); + Object::set_property(ctx, configuration, "path", Value::from_string(ctx, ex.path())); + Object::set_property(ctx, configuration, "readOnly", Value::from_boolean(ctx, true)); + if (!config.encryption_key.empty()) { + Object::set_property(ctx, configuration, "encryption_key", Value::from_binary(ctx, BinaryData(&config.encryption_key[0], 64))); + } + + ObjectType object = Object::create_empty(ctx); + Object::set_property(ctx, object, "name", Value::from_string(ctx, "IncompatibleSyncedRealmError")); + Object::set_property(ctx, object, "configuration", configuration); + throw Exception(ctx, object); + } + default: + throw; + } + } + static std::string validated_notification_name(ContextType ctx, const ValueType &value) { std::string name = Value::validated_to_string(ctx, value, "notification name"); if (name != "change") { @@ -482,7 +502,16 @@ SharedRealm RealmClass::create_shared_realm(ContextType ctx, realm::Realm::Co ObjectDefaultsMap && defaults, ConstructorMap && constructors) { config.execution_context = Context::get_execution_context_id(ctx); - SharedRealm realm = realm::Realm::get_shared_realm(config); + SharedRealm realm; + try { + realm = realm::Realm::get_shared_realm(config); + } + catch (const RealmFileException& ex) { + handleRealmFileException(ctx, config, ex); + } + catch (...) { + throw; + } GlobalContextType global_context = Context::get_global_context(ctx); if (!realm->m_binding_context) { @@ -691,7 +720,17 @@ void RealmClass::wait_for_download_completion(ContextType ctx, ObjectType thi std::function progressFunc; - auto realm = realm::Realm::get_shared_realm(config); + SharedRealm realm; + try { + realm = realm::Realm::get_shared_realm(config); + } + catch (const RealmFileException& ex) { + handleRealmFileException(ctx, config, ex); + } + catch (...) { + throw; + } + if (auto sync_config = config.sync_config) { static const String progressFuncName = "_onDownloadProgress"; diff --git a/src/rpc.cpp b/src/rpc.cpp index cb160526..5f6b9b2b 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -360,8 +360,24 @@ json RPCServer::perform_request(std::string name, const json &args) { assert(action); m_worker.add_task([=] { - return action(args); + try { + return action(args); + } + catch (jsc::Exception ex) { + json exceptionAsJson = nullptr; + try { + exceptionAsJson = serialize_json_value(ex); + } + catch (...) { + exceptionAsJson = {{"error", "An exception occured while processing the request. Could not serialize the exception as JSON"}}; + } + return (json){{"error", exceptionAsJson}, {"message", ex.what()}}; + } + catch (std::exception &exception) { + return (json){{"error", exception.what()}}; + } }); + } try { diff --git a/tests/data/sync-v1.realm b/tests/data/sync-v1.realm new file mode 100644 index 00000000..833b3248 Binary files /dev/null and b/tests/data/sync-v1.realm differ diff --git a/tests/js/index.js b/tests/js/index.js index 40b9af42..1225425e 100644 --- a/tests/js/index.js +++ b/tests/js/index.js @@ -46,7 +46,7 @@ if (global.enableSyncTests) { // FIXME: Permission tests currently fail in chrome debugging mode. if (typeof navigator === 'undefined' || !/Chrome/.test(navigator.userAgent)) { // eslint-disable-line no-undef - //TESTS.PermissionTests = require('./permission-tests'); + TESTS.PermissionTests = require('./permission-tests'); } } diff --git a/tests/js/permission-tests.js b/tests/js/permission-tests.js index 5f33e95d..e8a15571 100644 --- a/tests/js/permission-tests.js +++ b/tests/js/permission-tests.js @@ -67,20 +67,20 @@ function repeatUntil(fn, predicate) { } module.exports = { - testApplyAndGetGrantedPermissions() { - return createUsersWithTestRealms(1) - .then(([user]) => { - return user.applyPermissions({ userId: '*' }, `/${user.identity}/test`, 'read') - .then(repeatUntil(() => user.getGrantedPermissions('any'), - permissions => permissions.length > 1)) - .then(permissions => { - TestCase.assertEqual(permissions[1].path, `/${user.identity}/test`); - TestCase.assertEqual(permissions[1].mayRead, true); - TestCase.assertEqual(permissions[1].mayWrite, false); - TestCase.assertEqual(permissions[1].mayManage, false); - }); - }); - }, + // testApplyAndGetGrantedPermissions() { + // return createUsersWithTestRealms(1) + // .then(([user]) => { + // return user.applyPermissions({ userId: '*' }, `/${user.identity}/test`, 'read') + // .then(repeatUntil(() => user.getGrantedPermissions('any'), + // permissions => permissions.length > 1)) + // .then(permissions => { + // TestCase.assertEqual(permissions[1].path, `/${user.identity}/test`); + // TestCase.assertEqual(permissions[1].mayRead, true); + // TestCase.assertEqual(permissions[1].mayWrite, false); + // TestCase.assertEqual(permissions[1].mayManage, false); + // }); + // }); + // }, testOfferPermissions() { return createUsersWithTestRealms(2) diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index 36b52840..1e13a6b4 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -34,12 +34,14 @@ function node_require(module) { let tmp; let fs; let execFile; +let path; if (isNodeProccess) { tmp = node_require('tmp'); fs = node_require('fs'); execFile = node_require('child_process').execFile; tmp.setGracefulCleanup(); + path = node_require("path"); } @@ -76,6 +78,14 @@ function promisifiedLogin(server, username, password) { }); } +function copyFileToTempDir(filename) { + let tmpDir = tmp.dirSync(); + let content = fs.readFileSync(filename); + let tmpFile = tmp.fileSync({ dir: tmpDir.name }); + fs.appendFileSync(tmpFile.fd, content); + return tmpFile.name; +} + function runOutOfProcess(nodeJsFilePath) { var nodeArgs = Array.prototype.slice.call(arguments); let tmpDir = tmp.dirSync(); @@ -461,6 +471,130 @@ module.exports = { }); }, + testIncompatibleSyncedRealmOpen() { + let realm = "sync-v1.realm"; + if (isNodeProccess) { + realm = copyFileToTempDir(path.join(process.cwd(), "data", realm)); + } + else { + //copy the bundled RN realm files for the test + Realm.copyBundledRealmFiles(); + } + + return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then(user => { + return new Promise((resolve, _reject) => { + const config = { + path: realm, + sync: { + user, + error : err => cosole.log(err), + url: 'realm://localhost:9080/~/sync-v1' + } + }; + + Realm.open(config) + .then(realm => + _reject("Should fail with IncompatibleSyncedRealmError")) + .catch(e => { + if (e.name == "IncompatibleSyncedRealmError") { + const backupRealm = new Realm(e.configuration); + TestCase.assertEqual(backupRealm.objects('Dog').length, 3); + resolve(); + return; + } + + function printObject(o) { + var out = ''; + for (var p in o) { + out += p + ': ' + o[p] + '\n'; + } + return out; + } + + _reject("Failed with unexpected error " + printObject(e)); + }); + }); + }); + }, + + testIncompatibleSyncedRealmOpenAsync() { + let realm = "sync-v1.realm"; + if (isNodeProccess) { + realm = copyFileToTempDir(path.join(process.cwd(), "data", realm)); + } + else { + //copy the bundled RN realm files for the test + Realm.copyBundledRealmFiles(); + } + + return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then(user => { + return new Promise((resolve, _reject) => { + const config = { + path: realm, + sync: { + user, + error : err => cosole.log(err), + url: 'realm://localhost:9080/~/sync-v1' + } + }; + + Realm.openAsync(config, (error, realm) => { + if (!error) { + _reject("Should fail with IncompatibleSyncedRealmError"); + return; + } + + if (error.name == "IncompatibleSyncedRealmError") { + const backupRealm = new Realm(error.configuration); + TestCase.assertEqual(backupRealm.objects('Dog').length, 3); + resolve(); + return; + } + + _reject("Failed with unexpected error" + JSON.stringify(error)); + }); + }); + }); + }, + + testIncompatibleSyncedRealmConsructor() { + let realm = "sync-v1.realm"; + if (isNodeProccess) { + realm = copyFileToTempDir(path.join(process.cwd(), "data", realm)); + } + else { + //copy the bundled RN realm files for the test + Realm.copyBundledRealmFiles(); + } + + return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then(user => { + return new Promise((resolve, _reject) => { + const config = { + path: realm, + sync: { + user, + error : err => cosole.log(err), + url: 'realm://localhost:9080/~/sync-v1' + } + }; + + try { + const realm = new Realm(config); + _reject("Should fail with IncompatibleSyncedRealmError"); + } + catch (e) { + if (e.name == "IncompatibleSyncedRealmError") { + const backupRealm = new Realm(e.configuration); + TestCase.assertEqual(backupRealm.objects('Dog').length, 3); + resolve(); + return; + } + + _reject("Failed with unexpected error" + JSON.stringify(e)); + } + }); + }); + }, testProgressNotificationsForRealmConstructor() { if (!isNodeProccess) { diff --git a/tests/react-test-app/ios/ReactTests.xcodeproj/project.pbxproj b/tests/react-test-app/ios/ReactTests.xcodeproj/project.pbxproj index a92495e7..774f755b 100644 --- a/tests/react-test-app/ios/ReactTests.xcodeproj/project.pbxproj +++ b/tests/react-test-app/ios/ReactTests.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 855301CF1E20069D00FF108E /* dates-v3.realm in Resources */ = {isa = PBXBuildFile; fileRef = 855301CD1E20069D00FF108E /* dates-v3.realm */; }; 855301D01E20069D00FF108E /* dates-v5.realm in Resources */ = {isa = PBXBuildFile; fileRef = 855301CE1E20069D00FF108E /* dates-v5.realm */; }; 855301D31E2006F700FF108E /* RealmReactTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 855301D11E2006F400FF108E /* RealmReactTests.m */; }; + A4CEF4BB1F7F862D00BA3B26 /* sync-v1.realm in Resources */ = {isa = PBXBuildFile; fileRef = A4CEF4BA1F7F862D00BA3B26 /* sync-v1.realm */; }; E2050A7A5BE14CEA9A9E0722 /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B37A7097A134D5CBB4C462A /* libc++.tbd */; }; /* End PBXBuildFile section */ @@ -253,6 +254,7 @@ 855301CE1E20069D00FF108E /* dates-v5.realm */ = {isa = PBXFileReference; lastKnownFileType = file; path = "dates-v5.realm"; sourceTree = ""; }; 855301D11E2006F400FF108E /* RealmReactTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RealmReactTests.m; path = ReactTests/RealmReactTests.m; sourceTree = ""; }; 8B37A7097A134D5CBB4C462A /* libc++.tbd */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; }; + A4CEF4BA1F7F862D00BA3B26 /* sync-v1.realm */ = {isa = PBXFileReference; lastKnownFileType = file; path = "sync-v1.realm"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -476,6 +478,7 @@ 855301CC1E20069D00FF108E /* data */ = { isa = PBXGroup; children = ( + A4CEF4BA1F7F862D00BA3B26 /* sync-v1.realm */, 855301CD1E20069D00FF108E /* dates-v3.realm */, 855301CE1E20069D00FF108E /* dates-v5.realm */, ); @@ -805,6 +808,7 @@ files = ( 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + A4CEF4BB1F7F862D00BA3B26 /* sync-v1.realm in Resources */, 855301D01E20069D00FF108E /* dates-v5.realm in Resources */, 855301CF1E20069D00FF108E /* dates-v3.realm in Resources */, );