Restore support for opening query-based sync Realms with a dynamic schema (#2065)

* Restore support for opening query-based sync Realms with a dynamic schema

* Adjust how the schema is extended for query-based Realms

* Register constructors for permissions types even no schema is supplied

* Remove some cruft from tests

* Fix a use-after-free in dynamic schema mode

* Fix a test
This commit is contained in:
Thomas Goyne 2018-10-16 00:49:16 -07:00 committed by Nikola Irinchev
parent 10f72c5444
commit bfb06ac0df
3 changed files with 252 additions and 291 deletions

View File

@ -33,25 +33,6 @@ function setConstructorOnPrototype(klass) {
}
}
// Return a configuration usable by `Realm.open` when waiting for a download.
// It must have caching disabled, and no schema or schema version specified.
function waitForDownloadConfig(config) {
if (!config) {
return {_cache: false};
}
if (typeof config == 'string') {
return {path: config, _cache: false};
}
if (typeof config == 'object') {
return Object.assign({}, config, {schema: undefined, schemaVersion: undefined, _cache: false});
}
// Unknown type. Pass the config through.
return config;
}
/**
* Finds the permissions associated with a given Role or create them as needed.
*
@ -118,21 +99,12 @@ module.exports = function(realmConstructor) {
let syncSession;
let promise = new Promise((resolve, reject) => {
let realm = new realmConstructor(waitForDownloadConfig(config));
realm._waitForDownload(
(session) => { syncSession = session; },
(error) => {
realm.close();
syncSession = realmConstructor._asyncOpen(config, (realm, error) => {
if (error) {
setTimeout(() => { reject(error); }, 1);
}
else {
try {
let syncedRealm = new realmConstructor(config);
setTimeout(() => { resolve(syncedRealm); }, 1);
} catch (e) {
reject(e);
}
setTimeout(() => { resolve(realm); }, 1);
}
});
});
@ -453,41 +425,18 @@ module.exports = function(realmConstructor) {
}
}
}));
}
// Realm instance methods that are always available
Object.defineProperties(realmConstructor.prototype, getOwnPropertyDescriptors({
/**
* Extra internal constructor callback called by the C++ side.
* Used to work around the fact that we cannot override the original constructor,
* but still need to modify any input config.
*/
_constructor(config) {
// Even though this runs code only available for Sync it requires some serious misconfiguration
// for this to happen
if (config && config.sync) {
if (!Realm.Sync) {
throw new Error("Realm is not compiled with Sync, but the configuration contains sync features.");
}
// Only inject schemas on query-based Realms
if (config.sync.partial === true || config.sync.fullSynchronization === false) {
if (!config.schema) {
config['schema'] = [];
}
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Class);
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Permission);
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Realm);
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.Role);
addSchemaIfNeeded(config.schema, realmConstructor.Permissions.User);
addSchemaIfNeeded(config.schema, realmConstructor.Subscription.ResultSets);
}
}
return config;
Object.defineProperties(realmConstructor, getOwnPropertyDescriptors({
_extendQueryBasedSchema(schema) {
addSchemaIfNeeded(schema, realmConstructor.Permissions.Class);
addSchemaIfNeeded(schema, realmConstructor.Permissions.Permission);
addSchemaIfNeeded(schema, realmConstructor.Permissions.Realm);
addSchemaIfNeeded(schema, realmConstructor.Permissions.Role);
addSchemaIfNeeded(schema, realmConstructor.Permissions.User);
addSchemaIfNeeded(schema, realmConstructor.Subscription.ResultSets);
},
}));
}
// TODO: Remove this now useless object.
var types = Object.freeze({

View File

@ -228,7 +228,7 @@ public:
static void commit_transaction(ContextType, ObjectType, Arguments, ReturnValue&);
static void cancel_transaction(ContextType, ObjectType, Arguments, ReturnValue&);
static void add_listener(ContextType, ObjectType, Arguments, ReturnValue &);
static void wait_for_download_completion(ContextType, ObjectType, Arguments, ReturnValue &);
static void async_open_realm(ContextType, ObjectType, Arguments, ReturnValue &);
static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &);
static void remove_all_listeners(ContextType, ObjectType, Arguments, ReturnValue &);
static void close(ContextType, ObjectType, Arguments, ReturnValue &);
@ -255,6 +255,8 @@ public:
// static methods
static void constructor(ContextType, ObjectType, size_t, const ValueType[]);
static SharedRealm create_shared_realm(ContextType, realm::Realm::Config, bool, ObjectDefaultsMap &&, ConstructorMap &&);
static bool get_realm_config(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[],
Realm::Config& config, ObjectDefaultsMap& defaults, ConstructorMap& constructors);
static void schema_version(ContextType, ObjectType, Arguments, ReturnValue &);
static void clear_test_state(ContextType, ObjectType, Arguments, ReturnValue &);
@ -272,6 +274,9 @@ public:
{"clearTestState", wrap<clear_test_state>},
{"copyBundledRealmFiles", wrap<copy_bundled_realm_files>},
{"deleteFile", wrap<delete_file>},
#if REALM_ENABLE_SYNC
{"_asyncOpen", wrap<async_open_realm>},
#endif
};
PropertyMap<T> const static_properties = {
@ -298,9 +303,6 @@ public:
{"privileges", wrap<privileges>},
{"_objectForObjectId", wrap<object_for_object_id>},
{"_schemaName", wrap<get_schema_name_from_object>},
#if REALM_ENABLE_SYNC
{"_waitForDownload", wrap<wait_for_download_completion>},
#endif
};
PropertyMap<T> const properties = {
@ -374,6 +376,11 @@ public:
}
}
// Beginning a read transaction may reread the schema, invalidating the
// pointer that we're returning. Avoid this by ensuring that we're in a
// read transaction before we search the schema.
realm->read_group();
auto &schema = realm->schema();
auto object_schema = schema.find(object_type);
@ -440,27 +447,18 @@ static inline void convert_outdated_datetime_columns(const SharedRealm &realm) {
}
template<typename T>
void RealmClass<T>::constructor(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[]) {
bool RealmClass<T>::get_realm_config(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[],
Realm::Config& config, ObjectDefaultsMap& defaults, ConstructorMap& constructors) {
if (argc > 1) {
throw std::runtime_error("Invalid arguments when constructing 'Realm'");
}
// Callback to custom constructor
// This is used to work around the fact that we cannot reliably wrap the the Realm constructor
// without risking breaking existing code, so instead we make an extra roundtrip to this method.
ValueType modifiedConfig = Object::call_method(ctx, this_object, "_constructor", argc, arguments);
// Continue with C++ construction
realm::Realm::Config config;
ObjectDefaultsMap defaults;
ConstructorMap constructors;
bool schema_updated = false;
if (Value::is_undefined(ctx, modifiedConfig)) {
if (argc == 0) {
config.path = default_path();
}
else if (argc == 1) {
ValueType value = modifiedConfig;
ValueType value = arguments[0];
if (Value::is_string(ctx, value)) {
config.path = Value::validated_to_string(ctx, value, "path");
}
@ -513,6 +511,14 @@ void RealmClass<T>::constructor(ContextType ctx, ObjectType this_object, size_t
ValueType schema_value = Object::get_property(ctx, object, schema_string);
if (!Value::is_undefined(ctx, schema_value)) {
ObjectType schema_object = Value::validated_to_array(ctx, schema_value, "schema");
#if REALM_ENABLE_SYNC
// Ensure that the permissions and ResultSets object definitions
// are present in the schema for query-based sync
if (config.sync_config && config.sync_config->is_partial) {
auto realm_constructor = Value::validated_to_object(ctx, Object::get_global(ctx, "Realm"));
Object::call_method(ctx, realm_constructor, "_extendQueryBasedSchema", 1, &schema_value);
}
#endif
config.schema.emplace(Schema<T>::parse_schema(ctx, schema_object, defaults, constructors));
schema_updated = true;
}
@ -600,7 +606,15 @@ void RealmClass<T>::constructor(ContextType ctx, ObjectType this_object, size_t
config.path = normalize_realm_path(config.path);
ensure_directory_exists_for_file(config.path);
return schema_updated;
}
template<typename T>
void RealmClass<T>::constructor(ContextType ctx, ObjectType this_object, size_t argc, const ValueType arguments[]) {
realm::Realm::Config config;
ObjectDefaultsMap defaults;
ConstructorMap constructors;
bool schema_updated = get_realm_config(ctx, this_object, argc, arguments, config, defaults, constructors);
auto realm = create_shared_realm(ctx, config, schema_updated, std::move(defaults), std::move(constructors));
// Fix for datetime -> timestamp conversion
@ -622,13 +636,6 @@ SharedRealm RealmClass<T>::create_shared_realm(ContextType ctx, realm::Realm::Co
handleRealmFileException(ctx, config, ex);
}
#if REALM_ENABLE_SYNC
auto schema = realm->schema();
if (realm->is_partial() && schema.empty() && config.cache) {
throw std::invalid_argument("Query-based sync requires a schema.");
}
#endif
GlobalContextType global_context = Context<T>::get_global_context(ctx);
if (!realm->m_binding_context) {
realm->m_binding_context.reset(new RealmDelegate<T>(realm, global_context));
@ -643,6 +650,18 @@ SharedRealm RealmClass<T>::create_shared_realm(ContextType ctx, realm::Realm::Co
js_binding_context->m_defaults = std::move(defaults);
js_binding_context->m_constructors = std::move(constructors);
}
#if REALM_ENABLE_SYNC
// For query-based Realms we need to register the constructors for the
// permissions types even if a schema isn't specified
else if (config.sync_config && config.sync_config->is_partial && js_binding_context->m_constructors.empty()) {
ValueType schema_value = Object::create_array(ctx);
auto realm_constructor = Value::validated_to_object(ctx, Object::get_global(ctx, "Realm"));
Object::call_method(ctx, realm_constructor, "_extendQueryBasedSchema", 1, &schema_value);
Schema<T>::parse_schema(ctx, Value::to_object(ctx, schema_value), defaults, constructors);
js_binding_context->m_defaults = std::move(defaults);
js_binding_context->m_constructors = std::move(constructors);
}
#endif
return realm;
}
@ -801,62 +820,24 @@ void RealmClass<T>::get_sync_session(ContextType ctx, ObjectType object, ReturnV
#if REALM_ENABLE_SYNC
template<typename T>
void RealmClass<T>::wait_for_download_completion(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
void RealmClass<T>::async_open_realm(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {
args.validate_maximum(2);
auto callback_function = Value::validated_to_function(ctx, args[0 + (args.count == 2)]);
Realm::Config config;
ObjectDefaultsMap defaults;
ConstructorMap constructors;
bool schema_updated = get_realm_config(ctx, this_object, args.count - 1, args.value, config, defaults, constructors);
ValueType session_callback = Value::from_null(ctx);
if (args.count == 2) {
session_callback = Value::validated_to_function(ctx, args[0]);
}
auto realm = *get_internal<T, RealmClass<T>>(this_object);
auto* sync_config = realm->config().sync_config.get();
if (!sync_config) {
throw std::logic_error("_waitForDownload can only be used on a synchronized Realm.");
if (!config.sync_config) {
throw std::logic_error("_asyncOpen can only be used on a synchronized Realm.");
}
Protected<FunctionType> protected_callback(ctx, callback_function);
Protected<ObjectType> protected_this(ctx, this_object);
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
std::shared_ptr<SyncUser> user = sync_config->user;
if (user && user->state() != SyncUser::State::Error) {
if (auto session = user->session_for_on_disk_path(realm->config().path)) {
if (!Value::is_null(ctx, session_callback)) {
FunctionType session_callback_func = Value::to_function(ctx, session_callback);
auto syncSession = create_object<T, SessionClass<T>>(ctx, new WeakSession(session));
ValueType callback_arguments[1];
callback_arguments[0] = syncSession;
Function<T>::callback(protected_ctx, session_callback_func, typename T::Object(), 1, callback_arguments);
}
EventLoopDispatcher<WaitHandler> wait_handler([=](std::error_code error_code) {
HANDLESCOPE
if (!error_code) {
//success
Function<T>::callback(protected_ctx, protected_callback, typename T::Object(), 0, nullptr);
}
else {
//fail
ObjectType object = Object::create_empty(protected_ctx);
Object::set_property(protected_ctx, object, "message", Value::from_string(protected_ctx, error_code.message()));
Object::set_property(protected_ctx, object, "errorCode", Value::from_number(protected_ctx, error_code.value()));
ValueType callback_arguments[1];
callback_arguments[0] = object;
Function<T>::callback(protected_ctx, protected_callback, typename T::Object(), 1, callback_arguments);
}
// Ensure that the session remains alive until the callback has had an opportunity to reopen the Realm
// with the appropriate schema.
(void)session;
});
session->wait_for_download_completion(std::move(wait_handler));
return;
}
}
auto& user = config.sync_config->user;
if (user && user->state() == SyncUser::State::Error) {
ObjectType object = Object::create_empty(protected_ctx);
Object::set_property(protected_ctx, object, "message",
Value::from_string(protected_ctx, "Cannot asynchronously open synced Realm because the associated session previously experienced a fatal error"));
@ -866,6 +847,63 @@ void RealmClass<T>::wait_for_download_completion(ContextType ctx, ObjectType thi
callback_arguments[0] = object;
Function<T>::callback(protected_ctx, protected_callback, protected_this, 1, callback_arguments);
}
// First download the Realm with no schema set to avoid spurious writes that
// the server may either reject (for read-only Realms) or just waste time
// merging.
std::shared_ptr<Realm> realm;
try {
auto download_config = config;
download_config.schema = util::none;
download_config.cache = false;
realm = realm::Realm::get_shared_realm(std::move(download_config));
}
catch (const RealmFileException& ex) {
handleRealmFileException(ctx, config, ex);
}
auto session = user->session_for_on_disk_path(realm->config().path);
EventLoopDispatcher<WaitHandler> wait_handler([=, config=std::move(config),
defaults=std::move(defaults),
constructors=std::move(constructors)](std::error_code error_code) mutable {
HANDLESCOPE
if (error_code) {
ObjectType object = Object::create_empty(protected_ctx);
Object::set_property(protected_ctx, object, "message", Value::from_string(protected_ctx, error_code.message()));
Object::set_property(protected_ctx, object, "errorCode", Value::from_number(protected_ctx, error_code.value()));
ValueType callback_arguments[2];
callback_arguments[0] = Value::from_null(protected_ctx);
callback_arguments[1] = object;
Function<T>::callback(protected_ctx, protected_callback, typename T::Object(), 2, callback_arguments);
return;
}
// Ensure that all of our metadata tables are properly initialized if
// this is a query-based sync Realm
if (config.sync_config->is_partial) {
realm->update_schema({}, 0);
}
realm->close();
// Reopen it with the real configuration and pass that Realm back to the callback
auto final_realm = create_shared_realm(ctx, std::move(config),
schema_updated, std::move(defaults),
std::move(constructors));
ObjectType object = create_object<T, RealmClass<T>>(protected_ctx, new SharedRealm(final_realm));
ValueType callback_arguments[2];
callback_arguments[0] = object;
callback_arguments[1] = Value::from_null(protected_ctx);
Function<T>::callback(protected_ctx, protected_callback, typename T::Object(), 2, callback_arguments);
// Ensure the sync session is kept alive while we close and reopen the Realm
static_cast<void>(session);
});
session->wait_for_download_completion(std::move(wait_handler));
return_value.set(create_object<T, SessionClass<T>>(ctx, new WeakSession(session)));
}
#endif
template<typename T>

View File

@ -118,11 +118,9 @@ const getPartialRealm = () => {
}
});
return Realm.open(config); // Creates the Realm on the server
}).then(realm => {
return waitForUpload(realm);
}).then(realm => {
return waitForDownload(realm);
});
})
.then(waitForUpload)
.then(waitForDownload)
};
const assertFullAccess = function(permission) {
@ -276,9 +274,9 @@ module.exports = {
},
testAddPermissionSchemaForQueryBasedRealmOnly() {
return new Promise((resolve, reject) => {
Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then((user) => {
return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then((user) => {
let config = {
schema: [],
sync: {
user: user,
url: `realm://NO_SERVER/foo`,
@ -301,6 +299,7 @@ module.exports = {
// Full sync shouldn't include the permission schema
config = {
schema: [],
sync: {
user: user,
url: `realm://NO_SERVER/foo`,
@ -313,9 +312,6 @@ module.exports = {
realm.close();
Realm.deleteFile(config);
resolve();
}).catch(error => reject(error));
});
},
@ -353,11 +349,11 @@ module.exports = {
realm.close();
Realm.deleteFile(config);
// connecting with an empty schema should be possible, permission is added implicitly
Realm.open(user.createConfiguration()).then((realm) => {
return waitForUpload(realm);
}).then((realm) => {
return waitForDownload(realm);
}).then((realm) => {
return Realm.open(user.createConfiguration());
})
.then(waitForUpload)
.then(waitForDownload)
.then((realm) => {
let permissions = realm.objects(Realm.Permissions.Permission).filtered(`role.name = '__User:${user.identity}'`);
TestCase.assertEqual(permissions.length, 1);
TestCase.assertTrue(permissions[0].canRead);
@ -370,7 +366,6 @@ module.exports = {
realm.close();
resolve();
});
});
}
});
}).catch(error => reject(error));
@ -379,7 +374,6 @@ module.exports = {
testFindOrCreate_realmPermissions() {
return getPartialRealm().then(realm => {
return new Promise((resolve, reject) => {
let realmPermissions = realm.permissions();
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>" ]
realm.write(() => {
@ -395,14 +389,11 @@ module.exports = {
TestCase.assertFalse(permissions.canSetPermissions);
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:<xxx>", "foo" ]
});
resolve();
});
});
},
testFindOrCreate_existingRole() {
return getPartialRealm().then(realm => {
return new Promise((resolve, reject) => {
realm.write(() => {
realm.create('__Role', {'name':'foo'});
});
@ -421,14 +412,11 @@ module.exports = {
TestCase.assertFalse(permissions.canSetPermissions);
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
});
resolve();
});
});
},
testFindOrCreate_classPermissions() {
return getPartialRealm().then(realm => {
return new Promise((resolve, reject) => {
let classPermissions = realm.permissions('__Class');
TestCase.assertEqual(2, realm.objects('__Role').length); // [ "everyone", "__User:xxx" ]
realm.write(() => {
@ -444,39 +432,30 @@ module.exports = {
TestCase.assertFalse(permissions.canSetPermissions);
TestCase.assertEqual(3, realm.objects('__Role').length); // [ "everyone", "__User:xxx", "foo" ]
});
resolve();
});
});
},
testFindOrCreate_throwsOutsideWrite() {
return getPartialRealm().then(realm => {
return new Promise((resolve, reject) => {
let realmPermissions = realm.permissions();
TestCase.assertThrows(() => realmPermissions.findOrCreate("foo"));
let classPermissions = realm.permissions('__Class');
TestCase.assertThrows(() => classPermissions.findOrCreate("foo"));
resolve();
});
});
},
testPermissions_Realm: function() {
return getPartialRealm().then(realm => {
return new Promise((resolve, reject) => {
let permissions = realm.permissions();
TestCase.assertEqual(1, permissions.permissions.length);
let perm = permissions.permissions[0];
TestCase.assertEqual("everyone", perm.role.name);
assertFullAccess(perm);
resolve();
});
});
},
testPermissions_Class: function() {
return getPartialRealm().then(realm => {
return new Promise((resolve, reject) => {
let permissions = realm.permissions('__Class');
TestCase.assertEqual('__Class', permissions.name)
TestCase.assertEqual(1, permissions.permissions.length);
@ -489,17 +468,12 @@ module.exports = {
TestCase.assertTrue(perm.canQuery);
TestCase.assertFalse(perm.canModifySchema);
TestCase.assertTrue(perm.canSetPermissions);
resolve();
});
});
},
testPermissions_Class_InvalidClassArgument: function() {
return getPartialRealm().then(realm => {
return new Promise((resolve, reject) => {
TestCase.assertThrows(() => realm.permissions('foo'));
resolve();
});
});
},