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.
This commit is contained in:
Mark Rowe 2017-11-08 02:22:59 -08:00 committed by Kenneth Geisshirt
parent 837e8d90a3
commit 848a5b1c09
7 changed files with 212 additions and 1 deletions

View File

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

View File

@ -129,6 +129,7 @@ util.createMethods(Realm.prototype, objectTypes.REALM, [
'removeAllListeners',
'close',
'_waitForDownload',
'_objectForObjectId',
]);
// Mutating methods:

View File

@ -31,7 +31,9 @@ export default class RealmObject {
createMethods(RealmObject.prototype, objectTypes.OBJECT, [
'isValid',
'objectSchema',
'linkingObjects'
'linkingObjects',
'_objectId',
'_isSameObject',
]);
export function clearRegisteredConstructors() {

View File

@ -18,6 +18,7 @@
#pragma once
#include <cctype>
#include <list>
#include <map>
@ -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<compact>},
{"deleteModel", wrap<delete_model>},
{"_waitForDownload", wrap<wait_for_download_completion>},
{"_objectForObjectId", wrap<object_for_object_id>},
#if REALM_ENABLE_SYNC
{"_subscribeToObjects", wrap<subscribe_to_objects>},
#endif
@ -986,6 +989,62 @@ void RealmClass<T>::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<int(*)(int)>(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<typename T>
void RealmClass<T>::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<T, RealmClass<T>>(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<T>::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<typename T>
void RealmClass<T>::subscribe_to_objects(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) {

View File

@ -43,6 +43,7 @@ struct RealmObjectClass : ClassDefinition<T, realm::Object> {
using Object = js::Object<T>;
using Function = js::Function<T>;
using ReturnValue = js::ReturnValue<T>;
using Arguments = js::Arguments<T>;
static ObjectType create_instance(ContextType, realm::Object);
@ -53,6 +54,8 @@ struct RealmObjectClass : ClassDefinition<T, realm::Object> {
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<T, realm::Object> {
{"isValid", wrap<is_valid>},
{"objectSchema", wrap<get_object_schema>},
{"linkingObjects", wrap<linking_objects>},
{"_objectId", wrap<get_object_id>},
{"_isSameObject", wrap<is_same_object>},
};
};
@ -152,6 +157,50 @@ std::vector<String<T>> RealmObjectClass<T>::get_property_names(ContextType ctx,
return names;
}
template<typename T>
void RealmObjectClass<T>::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<T, RealmObjectClass<T>>(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<typename T>
void RealmObjectClass<T>::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<RealmObjectClass<T>>(ctx, otherObject)) {
return_value.set(false);
return;
}
auto self = get_internal<T, RealmObjectClass<T>>(object);
auto other = get_internal<T, RealmObjectClass<T>>(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

View File

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

View File

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