Merge pull request #1321 from realm/kneth/v1_v2-upgrade

v1 -> v2 upgrade path. (Support IncompatibleRealmSynced)
This commit is contained in:
blagoev 2017-10-02 18:18:53 +03:00 committed by GitHub
commit 0763d70989
14 changed files with 284 additions and 25 deletions

View File

@ -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)

View File

@ -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

View File

@ -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) {}

View File

@ -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

View File

@ -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}"`);
}

View File

@ -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;

View File

@ -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);

View File

@ -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<T>(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<T>::create_shared_realm(ContextType ctx, realm::Realm::Co
ObjectDefaultsMap && defaults, ConstructorMap && constructors) {
config.execution_context = Context<T>::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<T>::get_global_context(ctx);
if (!realm->m_binding_context) {
@ -691,7 +720,17 @@ void RealmClass<T>::wait_for_download_completion(ContextType ctx, ObjectType thi
std::function<ProgressHandler> 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";

View File

@ -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 {

BIN
tests/data/sync-v1.realm Normal file

Binary file not shown.

View File

@ -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');
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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 = "<group>"; };
855301D11E2006F400FF108E /* RealmReactTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RealmReactTests.m; path = ReactTests/RealmReactTests.m; sourceTree = "<group>"; };
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 = "<group>"; };
/* 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 */,
);