Add support for accessing linking objects / backlinks (#1101)

* Add support for linkingObjects

* Test linkingObjects

* Borrow names helper from list tests

* include computed properties when serializing the schema for the RN debugger

* add API docs

* review comments

* Expose admin users to JS (#1100)

The JS binding used to conflate `SyncUser::is_admin()` with the user being created by calling `Realm.Sync.User.adminToken()`, but now that we expose a user’s role on the server under `is_admin()` this supposition is no longer correct.

#1097 attempted to fix one such case, but fixing it only uncovered another: in `UserClass<T>::all_users()`.  I’ve gone through all the callsites of `SyncUser::is_admin()` to make sure they don’t assume an admin token user.

* [1.8.3] Bump version

* add linkingObjects method to Realm.Object

* changelog
This commit is contained in:
Marius Rackwitz 2017-06-29 11:59:10 +02:00 committed by Yavor Georgiev
parent f1695f33db
commit 9e0a9a3bd3
12 changed files with 331 additions and 34 deletions

View File

@ -1,3 +1,14 @@
vNext Release notes (TBD)
=============================================================
### Breaking changes
* None
### Enhancements
* Add support for Linking Objects (AKA Backlinks).
### Bug fixes
* Node
1.8.3 Release notes (2017-6-27)
=============================================================
### Breaking changes

View File

@ -36,4 +36,14 @@ class Object {
* @since 1.8.1
*/
objectSchema() {}
/**
* Returns all the objects that link to this object in the specified relationship.
* @param {string} objectType - The type of the objects that link to this object's type.
* @param {string} property - The name of the property that references objects of this object's type.
* @throws {Error} If the relationship is not valid.
* @returns {Realm.Results} the objects that link to this object.
* @since 1.9.0
*/
linkingObjects(objectType, property) {}
}

View File

@ -241,8 +241,10 @@ Realm.defaultPath;
* @typedef Realm~ObjectSchemaProperty
* @type {Object}
* @property {Realm~PropertyType} type - The type of this property.
* @property {string} [objectType] - **Required** when `type` is `"list"`, and must match the
* type of an object in the same schema.
* @property {string} [objectType] - **Required** when `type` is `"list"` or `"linkingObjects"`,
* and must match the type of an object in the same schema.
* @property {string} [property] - **Required** when `type` is `"linkingObjects"`, and must match
* the name of a property on the type specified in `objectType` that links to the type this property belongs to.
* @property {any} [default] - The default value for this property on creation when not
* otherwise specified.
* @property {boolean} [optional] - Signals if this property may be assigned `null` or `undefined`.
@ -262,7 +264,7 @@ Realm.defaultPath;
* A property type may be specified as one of the standard builtin types, or as an object type
* inside the same schema.
* @typedef Realm~PropertyType
* @type {("bool"|"int"|"float"|"double"|"string"|"date"|"data"|"list"|"<ObjectType>")}
* @type {("bool"|"int"|"float"|"double"|"string"|"date"|"data"|"list"|"linkingObjects"|"<ObjectType>")}
* @property {boolean} "bool" - Property value may either be `true` or `false`.
* @property {number} "int" - Property may be assigned any number, but will be stored as a
* round integer, meaning anything after the decimal will be truncated.
@ -278,6 +280,9 @@ Realm.defaultPath;
* @property {Realm.List} "list" - Property may be assigned any ordered collection
* (e.g. `Array`, {@link Realm.List}, {@link Realm.Results}) of objects all matching the
* `objectType` specified in the {@link Realm~ObjectSchemaProperty ObjectSchemaProperty}.
* @property {Realm.Results} "linkingObjects" - Property is read-only and always returns a {@link Realm.Results}
* of all the objects matching the `objectType` that are linking to the current object
* through the `property` relationship specified in {@link Realm~ObjectSchemaProperty ObjectSchemaProperty}.
* @property {Realm.Object} "<ObjectType>" - A string that matches the `name` of an object in the
* same schema (see {@link Realm~ObjectSchema ObjectSchema}) this property may be assigned
* any object of this type from inside the same Realm, and will always be _optional_

View File

@ -30,7 +30,8 @@ export default class RealmObject {
// Non-mutating methods:
createMethods(RealmObject.prototype, objectTypes.OBJECT, [
'isValid',
'objectSchema'
'objectSchema',
'linkingObjects'
]);
export function clearRegisteredConstructors() {

8
lib/index.d.ts vendored
View File

@ -24,7 +24,7 @@ declare namespace Realm {
* PropertyType
* @see { @link https://realm.io/docs/javascript/latest/api/Realm.html#~PropertyType }
*/
type PropertyType = string | 'bool' | 'int' | 'float' | 'double' | 'string' | 'data' | 'date' | 'list';
type PropertyType = string | 'bool' | 'int' | 'float' | 'double' | 'string' | 'data' | 'date' | 'list' | 'linkingObjects';
/**
* ObjectSchemaProperty
@ -33,6 +33,7 @@ declare namespace Realm {
interface ObjectSchemaProperty {
type: PropertyType;
objectType?: string;
property?: string;
default?: any;
optional?: boolean;
indexed?: boolean;
@ -102,6 +103,11 @@ declare namespace Realm {
* @returns ObjectSchema
*/
objectSchema(): ObjectSchema;
/**
* @returns Results<T>
*/
linkingObjects<T>(objectType: string, property: string): Results<T>;
}
const Object: {

View File

@ -52,6 +52,7 @@ 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 &);
const std::string name = "RealmObject";
@ -64,6 +65,7 @@ struct RealmObjectClass : ClassDefinition<T, realm::Object> {
MethodMap<T> const methods = {
{"isValid", wrap<is_valid>},
{"objectSchema", wrap<get_object_schema>},
{"linkingObjects", wrap<linking_objects>},
};
};
@ -152,3 +154,37 @@ std::vector<String<T>> RealmObjectClass<T>::get_property_names(ContextType ctx,
} // js
} // realm
// move this all the way here because it needs to include "js_results.hpp" which in turn includes this file
#include "js_results.hpp"
template<typename T>
void realm::js::RealmObjectClass<T>::linking_objects(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) {
validate_argument_count(argc, 2);
std::string object_type = Value::validated_to_string(ctx, arguments[0], "objectType");
std::string property_name = Value::validated_to_string(ctx, arguments[1], "property");
auto object = get_internal<T, RealmObjectClass<T>>(this_object);
auto target_object_schema = object->realm()->schema().find(object_type);
if (target_object_schema == object->realm()->schema().end()) {
throw std::logic_error(util::format("Could not find schema for type '%1'", object_type));
}
auto link_property = target_object_schema->property_for_name(property_name);
if (!link_property) {
throw std::logic_error(util::format("Type '%1' does not contain property '%2'", object_type, property_name));
}
if (link_property->object_type != object->get_object_schema().name) {
throw std::logic_error(util::format("'%1.%2' is not a relationship to '%3'", object_type, property_name, object->get_object_schema().name));
}
realm::TableRef table = ObjectStore::table_for_object_type(object->realm()->read_group(), target_object_schema->name);
auto row = object->row();
auto tv = row.get_table()->get_backlink_view(row.get_index(), table.get(), link_property->table_column);
return_value.set(ResultsClass<T>::create_instance(ctx, realm::Results(object->realm(), std::move(tv))));
}

View File

@ -75,6 +75,7 @@ Property Schema<T>::parse_property(ContextType ctx, ValueType attributes, std::s
static const String type_string = "type";
static const String object_type_string = "objectType";
static const String optional_string = "optional";
static const String property_string = "property";
Property prop;
prop.name = property_name;
@ -123,21 +124,30 @@ Property Schema<T>::parse_property(ContextType ctx, ValueType attributes, std::s
prop.type = realm::PropertyType::Array;
prop.object_type = Object::validated_get_string(ctx, property_object, object_type_string);
}
else {
else if (type == "linkingObjects") {
prop.type = realm::PropertyType::LinkingObjects;
if (!Value::is_valid(property_object)) {
throw std::runtime_error("Object property must specify 'objectType'");
}
prop.object_type = Object::validated_get_string(ctx, property_object, object_type_string);
prop.link_origin_property_name = Object::validated_get_string(ctx, property_object, property_string);
}
else if (type == "object") {
prop.type = realm::PropertyType::Object;
prop.is_nullable = true;
// The type could either be 'object' or the name of another object type in the same schema.
if (type == "object") {
if (!Value::is_valid(property_object)) {
throw std::runtime_error("Object property must specify 'objectType'");
}
prop.object_type = Object::validated_get_string(ctx, property_object, object_type_string);
}
else {
// The type could be the name of another object type in the same schema.
prop.type = realm::PropertyType::Object;
prop.is_nullable = true;
prop.object_type = type;
}
}
if (Value::is_valid(property_object)) {
ValueType default_value = Object::get_property(ctx, property_object, default_string);
@ -177,14 +187,27 @@ ObjectSchema Schema<T>::parse_object_schema(ContextType ctx, ObjectType object_s
for (uint32_t i = 0; i < length; i++) {
ObjectType property_object = Object::validated_get_object(ctx, properties_object, i);
std::string property_name = Object::validated_get_string(ctx, property_object, name_string);
object_schema.persisted_properties.emplace_back(parse_property(ctx, property_object, property_name, object_defaults));
Property property = parse_property(ctx, property_object, property_name, object_defaults);
if (property.type == realm::PropertyType::LinkingObjects) {
object_schema.computed_properties.emplace_back(std::move(property));
}
else {
object_schema.persisted_properties.emplace_back(std::move(property));
}
}
}
else {
auto property_names = Object::get_property_names(ctx, properties_object);
for (auto &property_name : property_names) {
ValueType property_value = Object::get_property(ctx, properties_object, property_name);
object_schema.persisted_properties.emplace_back(parse_property(ctx, property_value, property_name, object_defaults));
Property property = parse_property(ctx, property_value, property_name, object_defaults);
if (property.type == realm::PropertyType::LinkingObjects) {
object_schema.computed_properties.emplace_back(std::move(property));
}
else {
object_schema.persisted_properties.emplace_back(std::move(property));
}
}
}
@ -243,6 +266,9 @@ typename T::Object Schema<T>::object_for_object_schema(ContextType ctx, const Ob
for (auto& property : object_schema.persisted_properties) {
Object::set_property(ctx, properties, property.name, object_for_property(ctx, property));
}
for (auto& property : object_schema.computed_properties) {
Object::set_property(ctx, properties, property.name, object_for_property(ctx, property));
}
static const String properties_string = "properties";
Object::set_property(ctx, object, properties_string, properties);
@ -271,6 +297,11 @@ typename T::Object Schema<T>::object_for_property(ContextType ctx, const Propert
Object::set_property(ctx, object, object_type_string, Value::from_string(ctx, property.object_type));
}
static const String property_string = "property";
if (property.type == realm::PropertyType::LinkingObjects) {
Object::set_property(ctx, object, property_string, Value::from_string(ctx, property.link_origin_property_name));
}
static const String indexed_string = "indexed";
if (property.is_indexed) {
Object::set_property(ctx, object, indexed_string, Value::from_boolean(ctx, true));

View File

@ -463,6 +463,10 @@ json RPCServer::serialize_object_schema(const realm::ObjectSchema &object_schema
properties.push_back(prop.name);
}
for (auto &prop : object_schema.computed_properties) {
properties.push_back(prop.name);
}
return {
{"name", object_schema.name},
{"properties", properties},

View File

@ -22,6 +22,7 @@ var Realm = require('realm');
var TESTS = {
ListTests: require('./list-tests'),
LinkingObjectsTests: require('./linkingobjects-tests'),
ObjectTests: require('./object-tests'),
RealmTests: require('./realm-tests'),
ResultsTests: require('./results-tests'),

View File

@ -0,0 +1,127 @@
////////////////////////////////////////////////////////////////////////////
//
// 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.
//
////////////////////////////////////////////////////////////////////////////
'use strict';
var Realm = require('realm');
var TestCase = require('./asserts');
var schemas = require('./schemas');
function names(results) {
return results.map(function(object) {
return object.name;
});
}
module.exports = {
testBasics: function() {
var realm = new Realm({schema: [schemas.PersonObject]});
var olivier, oliviersParents;
realm.write(function() {
olivier = realm.create('PersonObject', {name: 'Olivier', age: 0});
realm.create('PersonObject', {name: 'Christine', age: 25, children: [olivier]});
oliviersParents = olivier.parents;
TestCase.assertArraysEqual(names(oliviersParents), ['Christine']);
});
TestCase.assertArraysEqual(names(oliviersParents), ['Christine']);
var jp;
realm.write(function() {
jp = realm.create('PersonObject', {name: 'JP', age: 28, children: [olivier]});
TestCase.assertArraysEqual(names(oliviersParents), ['Christine', 'JP']);
});
realm.write(function() {
realm.delete(olivier);
TestCase.assertEqual(oliviersParents.length, 0);
});
},
testFilteredLinkingObjects: function() {
var realm = new Realm({schema: [schemas.PersonObject]});
var christine, olivier, oliviersParents;
realm.write(function() {
olivier = realm.create('PersonObject', {name: 'Olivier', age: 0});
christine = realm.create('PersonObject', {name: 'Christine', age: 25, children: [olivier]});
realm.create('PersonObject', {name: 'JP', age: 28, children: [olivier]});
oliviersParents = olivier.parents;
});
// Three separate queries so that accessing a property on one doesn't invalidate testing of other properties.
var resultsA = oliviersParents.filtered('age > 25');
var resultsB = oliviersParents.filtered('age > 25');
var resultsC = oliviersParents.filtered('age > 25');
realm.write(function() {
var removed = christine.children.splice(0);
TestCase.assertEqual(removed.length, 1);
});
TestCase.assertEqual(resultsA.length, 1);
TestCase.assertEqual(resultsB.filtered("name = 'Christine'").length, 0);
TestCase.assertArraysEqual(names(resultsC), ['JP']);
},
testMethod: function() {
var realm = new Realm({schema: [schemas.PersonObject]});
var person;
realm.write(function () {
person = realm.create('PersonObject', { name: 'Person 1', age: 50 });
});
TestCase.assertThrows(() => person.linkingObjects('NoSuchSchema', 'noSuchProperty'),
"Could not find schema for type 'NoSuchSchema'");
TestCase.assertThrows(() => person.linkingObjects('PersonObject', 'noSuchProperty'),
"Type 'PersonObject' does not contain property 'noSuchProperty'");
TestCase.assertThrows(() => person.linkingObjects('PersonObject', 'name'),
"'PersonObject.name' is not a relationship to 'PersonObject'");
var olivier, oliviersParents;
realm.write(function() {
olivier = realm.create('PersonObject', {name: 'Olivier', age: 0});
realm.create('PersonObject', {name: 'Christine', age: 25, children: [olivier]});
oliviersParents = olivier.linkingObjects('PersonObject', 'children');
TestCase.assertArraysEqual(names(oliviersParents), ['Christine']);
});
TestCase.assertArraysEqual(names(oliviersParents), ['Christine']);
var jp;
realm.write(function() {
jp = realm.create('PersonObject', {name: 'JP', age: 28, children: [olivier]});
TestCase.assertArraysEqual(names(oliviersParents), ['Christine', 'JP']);
});
realm.write(function() {
realm.delete(olivier);
TestCase.assertEqual(oliviersParents.length, 0);
});
},
};

View File

@ -123,6 +123,43 @@ module.exports = {
TestCase.assertThrows(function() {
new Realm({schema: [{properties: {intCol: 'int'}}]});
}, 'The schema should be an array of ObjectSchema objects');
// linkingObjects property where the source property is missing
TestCase.assertThrows(function() {
new Realm({schema: [{
name: 'InvalidObject',
properties: {
linkingObjects: {type:'linkingObjects', objectType: 'InvalidObject', property: 'nosuchproperty'}
}
}]});
}, "Property 'InvalidObject.nosuchproperty' declared as origin of linking objects property 'InvalidObject.linkingObjects' does not exist");
// linkingObjects property where the source property is not a link
TestCase.assertThrows(function() {
new Realm({schema: [{
name: 'InvalidObject',
properties: {
integer: 'int',
linkingObjects: {type:'linkingObjects', objectType: 'InvalidObject', property: 'integer'}
}
}]});
}, "Property 'InvalidObject.integer' declared as origin of linking objects property 'InvalidObject.linkingObjects' is not a link")
// linkingObjects property where the source property links elsewhere
TestCase.assertThrows(function() {
new Realm({schema: [{
name: 'InvalidObject',
properties: {
link: 'IntObject',
linkingObjects: {type:'linkingObjects', objectType: 'InvalidObject', property: 'link'}
}
}, {
name: 'IntObject',
properties: {
integer: 'int'
}
}]});
}, "Property 'InvalidObject.link' declared as origin of linking objects property 'InvalidObject.linkingObjects' links to type 'IntObject'")
},
testRealmConstructorReadOnly: function() {
@ -178,7 +215,7 @@ module.exports = {
},
testRealmWrite: function() {
var realm = new Realm({schema: [schemas.IntPrimary, schemas.AllTypes, schemas.TestObject]});
var realm = new Realm({schema: [schemas.IntPrimary, schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]});
// exceptions should be propogated
TestCase.assertThrows(function() {
@ -271,7 +308,7 @@ module.exports = {
},
testRealmCreateUpsert: function() {
var realm = new Realm({schema: [schemas.IntPrimary, schemas.StringPrimary, schemas.AllTypes, schemas.TestObject]});
var realm = new Realm({schema: [schemas.IntPrimary, schemas.StringPrimary, schemas.AllTypes, schemas.TestObject, schemas.LinkToAllTypes]});
realm.write(function() {
var values = {
primaryCol: '0',
@ -789,7 +826,7 @@ module.exports = {
testSchema: function() {
var originalSchema = [schemas.TestObject, schemas.BasicTypes, schemas.NullableBasicTypes, schemas.IndexedTypes, schemas.IntPrimary,
schemas.PersonObject, schemas.LinkTypes];
schemas.PersonObject, schemas.LinkTypes, schemas.LinkingObjectsObject];
var schemaMap = {};
originalSchema.forEach(function(objectSchema) {
@ -827,6 +864,11 @@ module.exports = {
TestCase.assertEqual(prop1.objectType, prop2.objectType);
TestCase.assertEqual(prop1.optional, undefined);
}
else if (prop1.type == 'linking objects') {
TestCase.assertEqual(prop1.objectType, prop2.objectType);
TestCase.assertEqual(prop1.property, prop2.property);
TestCase.assertEqual(prop1.optional, undefined);
}
else {
TestCase.assertEqual(prop1.type, isString(prop2) ? prop2 : prop2.type);
TestCase.assertEqual(prop1.optional, prop2.optional || undefined);

View File

@ -18,6 +18,8 @@
'use strict';
const Realm = require('realm');
exports.TestObject = {
name: 'TestObject',
properties: {
@ -32,6 +34,8 @@ PersonObject.schema = {
name: 'string',
age: 'double',
married: {type: 'bool', default: false},
children: {type: 'list', objectType: 'PersonObject'},
parents: {type: 'linkingObjects', objectType: 'PersonObject', property: 'children'},
}
};
PersonObject.prototype.description = function() {
@ -40,6 +44,8 @@ PersonObject.prototype.description = function() {
PersonObject.prototype.toString = function() {
return this.name;
};
Object.setPrototypeOf(PersonObject, Realm.Object);
Object.setPrototypeOf(PersonObject.prototype, Realm.Object.prototype);
exports.PersonObject = PersonObject;
exports.PersonList = {
@ -127,9 +133,17 @@ exports.AllTypes = {
dataCol: 'data',
objectCol: 'TestObject',
arrayCol: {type: 'list', objectType: 'TestObject'},
linkingObjectsCol: {type: 'linkingObjects', objectType: 'LinkToAllTypesObject', property: 'allTypesCol'},
}
};
exports.LinkToAllTypes = {
name: 'LinkToAllTypesObject',
properties: {
allTypesCol: 'AllTypesObject',
}
}
exports.DefaultValues = {
name: 'DefaultValuesObject',
properties: {
@ -185,3 +199,12 @@ exports.DateObject = {
nullDate: { type: 'date', optional: true }
}
};
exports.LinkingObjectsObject = {
name: 'LinkingObjectsObject',
properties: {
value: 'int',
links: {type: 'list', objectType: 'LinkingObjectsObject'},
linkingObjects: {type: 'linkingObjects', objectType: 'LinkingObjectsObject', property: 'links'}
}
}