diff --git a/CHANGELOG.md b/CHANGELOG.md index 58ca7100..4c3d95a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ X.Y.Z Release notes ### Bug fixes * Fixed the type definition for `Realm.Permissions.User`. Thanks to @apperside! ([#2012](https://github.com/realm/realm-js/issues/2012), since v2.3.0-beta.2) +* Previously, adding a schema definition (using `config.schema = [Dog, Person]` for example) will prevent the permission schema from being added for query based Realm. ([#2017](https://github.com/realm/realm-js/issues/2017), since v2.3.0). +* As part of including the permission schema implicitly when using query based Realm, the schema `Realm.Permissions.Realm` was missing, which may break any query including it. ([#2016](https://github.com/realm/realm-js/issues/2016), since v2.3.0) * Fixed the type definition for `Realm.getPrivileges()`, `Realm.getPrivileges(className)` and `Realm.getPrivileges(object)`. ([#2030](https://github.com/realm/realm-js/pull/2030), since v2.2.14) ### Compatibility diff --git a/lib/extensions.js b/lib/extensions.js index 2950b915..65ce47a1 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -74,15 +74,6 @@ module.exports = function(realmConstructor) { return promise; } - // For synced Realms we open the Realm without specifying the schema and then wait until - // the Realm has finished its initial sync with the server. We then reopen it with the correct - // schema. This avoids writing the schema to a potentially read-only Realm file, which would - // result in sync rejecting the writes. `_waitForDownload` ensures that the session is kept - // alive until our callback has returned, which prevents it from being torn down and recreated - // when we close the schemaless Realm and open it with the correct schema. - if (config.sync.fullSynchronization === false && config.schema === undefined) { - throw new Error('Query-based sync requires a schema.'); - } let syncSession; let promise = new Promise((resolve, reject) => { let realm = new realmConstructor(waitForDownloadConfig(config)); diff --git a/lib/user-methods.js b/lib/user-methods.js index 90f1f8f3..7134627c 100644 --- a/lib/user-methods.js +++ b/lib/user-methods.js @@ -584,7 +584,6 @@ const instanceMethods = { user: this, url: realmUrl, }, - schema: [], }; // Set query-based as the default setting if the user doesn't specified any other behaviour. @@ -592,16 +591,6 @@ const instanceMethods = { defaultConfig.sync.fullSynchronization = false; } - // Automatically add Permission classes to the schema if Query-based sync is enabled - if (defaultConfig.sync.fullSynchronization === false || (config && config.sync && config.sync.partial === true)) { - defaultConfig.schema = [ - Realm.Permissions.Class, - Realm.Permissions.Permission, - Realm.Permissions.Role, - Realm.Permissions.User, - ]; - } - // Merge default configuration with user provided config. User defined properties should aways win. // Doing the naive merge in JS break objects that are backed by native objects, so these needs to // be merged manually. This is currently only `sync.user`. diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 6e22ded6..15edb82f 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -501,6 +501,89 @@ void RealmClass::constructor(ContextType ctx, ObjectType this_object, size_t schema_updated = true; } +#if REALM_ENABLE_SYNC + // Include permission schema for query-based sync + if (config.sync_config && config.sync_config->is_partial) { + std::vector objectsSchema; + + if (!config.schema) { + config.schema.emplace(realm::Schema(objectsSchema)); + } + + auto it = config.schema->find("__Class"); + if (it == config.schema->end()) { + realm::ObjectSchema clazz = {"__Class", { + {"name", realm::PropertyType::String, Property::IsPrimary{true}}, + {"permissions", realm::PropertyType::Object|realm::PropertyType::Array, "__Permission"} + }}; + objectsSchema.emplace_back(std::move(clazz)); + schema_updated = true; + } + + it = config.schema->find("__Permission"); + if (it == config.schema->end()) { + realm::ObjectSchema permission = {"__Permission", { + {"role", realm::PropertyType::Object|realm::PropertyType::Nullable, "__Role"}, + {"canRead", realm::PropertyType::Bool}, + {"canUpdate", realm::PropertyType::Bool}, + {"canDelete", realm::PropertyType::Bool}, + {"canSetPermissions", realm::PropertyType::Bool}, + {"canQuery", realm::PropertyType::Bool}, + {"canCreate", realm::PropertyType::Bool}, + {"canModifySchema", realm::PropertyType::Bool} + }}; + objectsSchema.emplace_back(std::move(permission)); + // adding default values + std::map> object_defaults; + object_defaults.emplace("canRead", Protected(ctx, Value::from_boolean(ctx, false))); + object_defaults.emplace("canUpdate", Protected(ctx, Value::from_boolean(ctx, false))); + object_defaults.emplace("canDelete", Protected(ctx, Value::from_boolean(ctx, false))); + object_defaults.emplace("canSetPermissions", Protected(ctx, Value::from_boolean(ctx, false))); + object_defaults.emplace("canQuery", Protected(ctx, Value::from_boolean(ctx, false))); + object_defaults.emplace("canCreate", Protected(ctx, Value::from_boolean(ctx, false))); + object_defaults.emplace("canModifySchema", Protected(ctx, Value::from_boolean(ctx, false))); + + defaults.emplace("__Permission", std::move(object_defaults)); + schema_updated = true; + } + + it = config.schema->find("__Realm"); + if (it == config.schema->end()) { + realm::ObjectSchema realm = {"__Realm", { + {"id", realm::PropertyType::Int, realm::Property::IsPrimary{true}}, + {"permissions", realm::PropertyType::Object|realm::PropertyType::Array, "__Permission"} + }}; + objectsSchema.emplace_back(std::move(realm)); + schema_updated = true; + } + + it = config.schema->find("__Role"); + if (it == config.schema->end()) { + realm::ObjectSchema role = {"__Role", { + {"name", realm::PropertyType::String, realm::Property::IsPrimary{true}}, + {"members", realm::PropertyType::Object|realm::PropertyType::Array, "__User"} + }}; + objectsSchema.emplace_back(std::move(role)); + schema_updated = true; + } + + it = config.schema->find("__User"); + if (it == config.schema->end()) { + realm::ObjectSchema user = {"__User", { + {"id", realm::PropertyType::String, realm::Property::IsPrimary{true}}, + {"role", realm::PropertyType::Object|realm::PropertyType::Nullable, "__Role"} + }}; + objectsSchema.emplace_back(std::move(user)); + schema_updated = true; + } + + objectsSchema.insert(objectsSchema.end(), std::make_move_iterator(config.schema->begin()), + std::make_move_iterator(config.schema->end())); + + config.schema.emplace(realm::Schema(objectsSchema)); + } +#endif + static const String schema_version_string = "schemaVersion"; ValueType version_value = Object::get_property(ctx, object, schema_version_string); if (!Value::is_undefined(ctx, version_value)) { diff --git a/tests/js/permission-tests.js b/tests/js/permission-tests.js index 8e71c20e..26e3babb 100644 --- a/tests/js/permission-tests.js +++ b/tests/js/permission-tests.js @@ -231,5 +231,109 @@ module.exports = { {read: true, update: false, delete: false, setPermissions: false}); realm.close(); }); + }, + + testAddPermissionSchemaForQueryBasedRealmOnly() { + return new Promise((resolve, reject) => { + Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then((user) => { + let config = { + sync: { + user: user, + url: `realm://NO_SERVER/foo`, + fullSynchronization: false, + } + }; + + let realm = new Realm(config); + TestCase.assertTrue(realm.empty); + + TestCase.assertEqual(realm.schema.length, 5); + TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Class').length, 1); + TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Permission').length, 1); + TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Realm').length, 1); + TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__Role').length, 1); + TestCase.assertEqual(realm.schema.filter(schema => schema.name === '__User').length, 1); + + realm.close(); + Realm.deleteFile(config); + + // Full sync shouldn't include the permission schema + config = { + sync: { + user: user, + url: `realm://NO_SERVER/foo`, + fullSynchronization: true + } + }; + realm = new Realm(config); + TestCase.assertTrue(realm.empty); + TestCase.assertEqual(realm.schema.length, 0); + + realm.close(); + Realm.deleteFile(config); + + resolve(); + }).catch(error => reject(error)); + }); + }, + + testUsingAddedPermissionSchemas() { + return new Promise((resolve, reject) => { + Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then((user) => { + const config = user.createConfiguration(); + const PrivateChatRoomSchema = { + name: 'PrivateChatRoom', + primaryKey: 'name', + properties: { + 'name': { type: 'string', optional: false }, + 'permissions': { type: 'list', objectType: '__Permission' } + } + }; + config.schema = [PrivateChatRoomSchema]; + const realm = new Realm(config); + + let rooms = realm.objects(PrivateChatRoomSchema.name); + let subscription = rooms.subscribe(); + subscription.addListener((sub, state) => { + if (state === Realm.Sync.SubscriptionState.Complete) { + let roles = realm.objects(Realm.Permissions.Role.schema.name).filtered(`name = '__User:${user.identity}'`); + TestCase.assertEqual(roles.length, 1); + + realm.write(() => { + const permission = realm.create(Realm.Permissions.Permission.schema.name, + { canUpdate: true, canRead: true, canQuery: true, role: roles[0] }); + + let room = realm.create(PrivateChatRoomSchema.name, { name: `#sales_${uuid()}` }); + room.permissions.push(permission); + }); + + waitForUpload(realm).then(() => { + realm.close(); + Realm.deleteFile(config); + // connecting with an empty schema should be possible, permission is added implicitly + Realm.open(user.createConfiguration()).then((realm) => { + let permissions = realm.objects(Realm.Permissions.Permission.schema.name).filtered(`role.name = '__User:${user.identity}'`); + let subscription = permissions.subscribe(); + subscription.addListener((sub, state) => { + if (state === Realm.Sync.SubscriptionState.Complete) { + TestCase.assertEqual(permissions.length, 1); + TestCase.assertTrue(permissions[0].canRead); + TestCase.assertTrue(permissions[0].canQuery); + TestCase.assertTrue(permissions[0].canUpdate); + TestCase.assertFalse(permissions[0].canDelete); + TestCase.assertFalse(permissions[0].canSetPermissions); + TestCase.assertFalse(permissions[0].canCreate); + TestCase.assertFalse(permissions[0].canModifySchema); + + realm.close(); + resolve(); + } + }); + }); + }); + } + }); + }).catch(error => reject(error)); + }); } } diff --git a/tests/js/session-tests.js b/tests/js/session-tests.js index e5a8a327..5839faec 100644 --- a/tests/js/session-tests.js +++ b/tests/js/session-tests.js @@ -824,13 +824,6 @@ module.exports = { TestCase.assertThrows(() => Realm.automaticSyncConfiguration('foo', 'bar')); // too many arguments } - function schemalessNotAllowed() { - let config = Realm.Sync.User.current.createConfiguration(); - config.schema = undefined; // no schema in the configuration - Realm.deleteFile(config); - TestCase.assertThrows(() => { let realm = new Realm(config); } ); - } - const credentials = Realm.Sync.Credentials.nickname(username); return runOutOfProcess(__dirname + '/partial-sync-api-helper.js', username, REALM_MODULE_PATH) .then(() => { @@ -841,7 +834,6 @@ module.exports = { __partialIsNotAllowed(); shouldFail(); defaultRealmInvalidArguments(); - schemalessNotAllowed(); return new Promise((resolve, reject) => { let config = Realm.Sync.User.current.createConfiguration(); @@ -1161,18 +1153,4 @@ module.exports = { }) }) }, - - testOfflinePermissionSchemas() { - if (!isNodeProccess) { - return; - } - - return Realm.Sync.User.login('http://localhost:9080', Realm.Sync.Credentials.anonymous()).then((u) => { - return new Promise((resolve, reject) => { - let realm = new Realm(u.createConfiguration()); - TestCase.assertEqual(5, realm.objects(Realm.Permissions.Class.schema.name).length); - resolve('Done'); - }); - }); - } }