From 848a5b1c0969ad7b3e2a6e1ccafebbe0b7584bdc Mon Sep 17 00:00:00 2001 From: Mark Rowe Date: Wed, 8 Nov 2017 02:22:59 -0800 Subject: [PATCH] Expose an object's internal object ID, and allow fetching an object by its object ID (#1460) * Expose an object's internal object ID, and allow fetching an object by its object ID * Throw an exception if methods related to object IDs are used on non-synced Realms. * Use `std::stoull` to ensure we can return the entire range of possible values. * Add tests for _objectId() / _objectForObjectId(). * Adding change log * Skip ObjectIdTests.testSynced for non-Node. --- CHANGELOG.md | 15 +++++++ lib/browser/index.js | 1 + lib/browser/objects.js | 4 +- src/js_realm.hpp | 59 ++++++++++++++++++++++++++ src/js_realm_object.hpp | 49 ++++++++++++++++++++++ tests/js/index.js | 1 + tests/js/object-id-tests.js | 84 +++++++++++++++++++++++++++++++++++++ 7 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 tests/js/object-id-tests.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ea78c1..fac7510d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +X.Y.Z Release notes +============================================================= +### Breaking changes +* None. + +### Enchancements +* None. + +### Bug fixes +* None. + +### Internal +* Added support for object IDs. + + 2.0.4 Release notes (2017-11-7) ============================================================= ### Breaking changes diff --git a/lib/browser/index.js b/lib/browser/index.js index 61dd013c..607af4af 100644 --- a/lib/browser/index.js +++ b/lib/browser/index.js @@ -129,6 +129,7 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [ 'removeAllListeners', 'close', '_waitForDownload', + '_objectForObjectId', ]); // Mutating methods: diff --git a/lib/browser/objects.js b/lib/browser/objects.js index 9c9ad87e..aeea2b45 100644 --- a/lib/browser/objects.js +++ b/lib/browser/objects.js @@ -31,7 +31,9 @@ export default class RealmObject { createMethods(RealmObject.prototype, objectTypes.OBJECT, [ 'isValid', 'objectSchema', - 'linkingObjects' + 'linkingObjects', + '_objectId', + '_isSameObject', ]); export function clearRegisteredConstructors() { diff --git a/src/js_realm.hpp b/src/js_realm.hpp index d6b164d8..9c226dbc 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -18,6 +18,7 @@ #pragma once +#include #include #include @@ -182,6 +183,7 @@ public: static void close(ContextType, ObjectType, Arguments, ReturnValue &); static void compact(ContextType, ObjectType, Arguments, ReturnValue &); static void delete_model(ContextType, ObjectType, Arguments, ReturnValue &); + static void object_for_object_id(ContextType, ObjectType, Arguments, ReturnValue&); #if REALM_ENABLE_SYNC static void subscribe_to_objects(ContextType, ObjectType, Arguments, ReturnValue &); #endif @@ -241,6 +243,7 @@ public: {"compact", wrap}, {"deleteModel", wrap}, {"_waitForDownload", wrap}, + {"_objectForObjectId", wrap}, #if REALM_ENABLE_SYNC {"_subscribeToObjects", wrap}, #endif @@ -986,6 +989,62 @@ void RealmClass::compact(ContextType ctx, ObjectType this_object, Arguments a return_value.set(realm->compact()); } +#if REALM_ENABLE_SYNC +namespace { + +// FIXME: Sync should provide this: https://github.com/realm/realm-sync/issues/1796 +inline sync::ObjectID object_id_from_string(std::string const& string) +{ + if (string.front() != '{' || string.back() != '}') + throw std::invalid_argument("Invalid object ID."); + + size_t dash_index = string.find('-'); + if (dash_index == std::string::npos) + throw std::invalid_argument("Invalid object ID."); + + std::string high_string = string.substr(1, dash_index - 1); + std::string low_string = string.substr(dash_index + 1, string.size() - dash_index - 2); + + if (high_string.size() == 0 || high_string.size() > 16 || low_string.size() == 0 || low_string.size() > 16) + throw std::invalid_argument("Invalid object ID."); + + auto isxdigit = static_cast(std::isxdigit); + if (!std::all_of(high_string.begin(), high_string.end(), isxdigit) || + !std::all_of(low_string.begin(), low_string.end(), isxdigit)) { + throw std::invalid_argument("Invalid object ID."); + } + + return sync::ObjectID(std::stoull(high_string, nullptr, 16), std::stoull(low_string, nullptr, 16)); +} + +} // unnamed namespace +#endif // REALM_ENABLE_SYNC + +template +void RealmClass::object_for_object_id(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue& return_value) { + args.validate_count(2); + +#if REALM_ENABLE_SYNC + SharedRealm realm = *get_internal>(this_object); + if (!sync::has_object_ids(realm->read_group())) + throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); + + std::string object_type = Value::validated_to_string(ctx, args[0]); + validated_object_schema_for_value(ctx, realm, args[0], object_type); + + std::string object_id_string = Value::validated_to_string(ctx, args[1]); + auto object_id = object_id_from_string(object_id_string); + + const Group& group = realm->read_group(); + size_t ndx = sync::row_for_object_id(group, *ObjectStore::table_for_object_type(group, object_type), object_id); + if (ndx != realm::npos) { + return_value.set(RealmObjectClass::create_instance(ctx, realm::Object(realm, object_type, ndx))); + } +#else + throw std::logic_error("Realm._objectForObjectId() can only be used with synced Realms."); +#endif // REALM_ENABLE_SYNC +} + #if REALM_ENABLE_SYNC template void RealmClass::subscribe_to_objects(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { diff --git a/src/js_realm_object.hpp b/src/js_realm_object.hpp index 23ed86de..f18698fb 100644 --- a/src/js_realm_object.hpp +++ b/src/js_realm_object.hpp @@ -43,6 +43,7 @@ struct RealmObjectClass : ClassDefinition { using Object = js::Object; using Function = js::Function; using ReturnValue = js::ReturnValue; + using Arguments = js::Arguments; static ObjectType create_instance(ContextType, realm::Object); @@ -53,6 +54,8 @@ struct RealmObjectClass : ClassDefinition { static void is_valid(ContextType, FunctionType, ObjectType, size_t, const ValueType [], ReturnValue &); static void get_object_schema(ContextType, FunctionType, ObjectType, size_t, const ValueType [], ReturnValue &); static void linking_objects(ContextType, FunctionType, ObjectType, size_t, const ValueType [], ReturnValue &); + static void get_object_id(ContextType, ObjectType, Arguments, ReturnValue &); + static void is_same_object(ContextType, ObjectType, Arguments, ReturnValue &); const std::string name = "RealmObject"; @@ -66,6 +69,8 @@ struct RealmObjectClass : ClassDefinition { {"isValid", wrap}, {"objectSchema", wrap}, {"linkingObjects", wrap}, + {"_objectId", wrap}, + {"_isSameObject", wrap}, }; }; @@ -152,6 +157,50 @@ std::vector> RealmObjectClass::get_property_names(ContextType ctx, return names; } +template +void RealmObjectClass::get_object_id(ContextType ctx, ObjectType object, Arguments args, ReturnValue& return_value) { + args.validate_maximum(0); + +#if REALM_ENABLE_SYNC + auto realm_object = get_internal>(object); + const Group& group = realm_object->realm()->read_group(); + if (!sync::has_object_ids(group)) + throw std::logic_error("_objectId() can only be used with objects from synced Realms."); + + const Row& row = realm_object->row(); + auto object_id = sync::object_id_for_row(group, *row.get_table(), row.get_index()); + return_value.set(object_id.to_string()); +#else + throw std::logic_error("_objectId() can only be used with objects from synced Realms."); +#endif +} + +template +void RealmObjectClass::is_same_object(ContextType ctx, ObjectType object, Arguments args, ReturnValue& return_value) { + args.validate_count(1); + + ObjectType otherObject = Value::validated_to_object(ctx, args[0]); + if (!Object::template is_instance>(ctx, otherObject)) { + return_value.set(false); + return; + } + + auto self = get_internal>(object); + auto other = get_internal>(otherObject); + + if (!self->realm() || self->realm() != other->realm()) { + return_value.set(false); + return; + } + + if (!self->is_valid() || !other->is_valid()) { + return_value.set(false); + return; + } + + return_value.set(self->row().get_table() == other->row().get_table() + && self->row().get_index() == other->row().get_index()); +} } // js } // realm diff --git a/tests/js/index.js b/tests/js/index.js index 37524737..9b9514da 100644 --- a/tests/js/index.js +++ b/tests/js/index.js @@ -42,6 +42,7 @@ var TESTS = { QueryTests: require('./query-tests'), MigrationTests: require('./migration-tests'), EncryptionTests: require('./encryption-tests'), + ObjectIDTests: require('./object-id-tests'), // GarbageCollectionTests: require('./garbage-collection'), }; diff --git a/tests/js/object-id-tests.js b/tests/js/object-id-tests.js new file mode 100644 index 00000000..80063732 --- /dev/null +++ b/tests/js/object-id-tests.js @@ -0,0 +1,84 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2017 Realm Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////// + +/* eslint-env es6, node */ + +'use strict'; + +const Realm = require('realm'); +const TestCase = require('./asserts'); + +const isNodeProccess = (typeof process === 'object' && process + '' === '[object process]'); + +function uuid() { + return '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); + }); +} + +module.exports = { + testNonSynced: function() { + let realm = new Realm({schema: [{ name: 'Dog', properties: { name: 'string' } }]}); + var dog; + realm.write(() => { + dog = realm.create('Dog', ['Fido']); + }); + TestCase.assertThrowsContaining(() => dog._objectId(), "_objectId() can only be used with objects from synced Realms"); + TestCase.assertThrowsContaining(() => realm._objectForObjectId('Dog', 'foo'), "Realm._objectForObjectId() can only be used with synced Realms"); + }, + + testSynced: function() { + if (!global.enableSyncTests || !isNodeProccess) + return; + + return Realm.Sync.User.register('http://localhost:9080', uuid(), 'password').then(user => { + const config = { sync: { user, url: 'realm://localhost:9080/~/myrealm' }, + schema: [{ name: 'IntegerPrimaryKey', properties: { int: 'int?' }, primaryKey: 'int' }, + { name: 'StringPrimaryKey', properties: { string: 'string?' }, primaryKey: 'string' }, + { name: 'NoPrimaryKey', properties: { string: 'string' }}, + ], + }; + return Realm.open(config).then(realm => { + var integer, nullInteger; + var string, nullString; + var none; + realm.write(() => { + integer = realm.create('IntegerPrimaryKey', [12345]); + nullInteger = realm.create('IntegerPrimaryKey', [null]); + string = realm.create('StringPrimaryKey', ["hello, world"]); + nullString = realm.create('StringPrimaryKey', [null]); + none = realm.create('NoPrimaryKey', ["hello, world"]); + }); + + let integerId = integer._objectId(); + let nullIntegerId = nullInteger._objectId(); + let stringId = string._objectId(); + let nullStringId = nullString._objectId(); + let noneId = none._objectId(); + + TestCase.assertTrue(integer._isSameObject(realm._objectForObjectId('IntegerPrimaryKey', integerId))); + TestCase.assertTrue(nullInteger._isSameObject(realm._objectForObjectId('IntegerPrimaryKey', nullIntegerId))); + TestCase.assertTrue(string._isSameObject(realm._objectForObjectId('StringPrimaryKey', stringId))); + TestCase.assertTrue(nullString._isSameObject(realm._objectForObjectId('StringPrimaryKey', nullStringId))); + TestCase.assertTrue(none._isSameObject(realm._objectForObjectId('NoPrimaryKey', noneId))); + }); + }); + + } +};