From 9e0a9a3bd35cd4482f64f9b55dced3f4b18337a2 Mon Sep 17 00:00:00 2001 From: Marius Rackwitz Date: Thu, 29 Jun 2017 11:59:10 +0200 Subject: [PATCH] Add support for accessing linking objects / backlinks (#1101) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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::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 --- CHANGELOG.md | 11 +++ docs/object.js | 10 +++ docs/realm.js | 11 ++- lib/browser/objects.js | 3 +- lib/index.d.ts | 8 +- src/js_realm_object.hpp | 36 +++++++++ src/js_schema.hpp | 57 ++++++++++---- src/rpc.cpp | 4 + tests/js/index.js | 1 + tests/js/linkingobjects-tests.js | 127 +++++++++++++++++++++++++++++++ tests/js/realm-tests.js | 48 +++++++++++- tests/js/schemas.js | 49 ++++++++---- 12 files changed, 331 insertions(+), 34 deletions(-) create mode 100644 tests/js/linkingobjects-tests.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 855c215e..4c908af7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/object.js b/docs/object.js index 587d93e5..83cc33a5 100644 --- a/docs/object.js +++ b/docs/object.js @@ -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) {} } diff --git a/docs/realm.js b/docs/realm.js index 7419db8d..20fc99e3 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -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"|"")} + * @type {("bool"|"int"|"float"|"double"|"string"|"date"|"data"|"list"|"linkingObjects"|"")} * @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} "" - 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_ diff --git a/lib/browser/objects.js b/lib/browser/objects.js index e0385f7b..f0f5caf7 100644 --- a/lib/browser/objects.js +++ b/lib/browser/objects.js @@ -30,7 +30,8 @@ export default class RealmObject { // Non-mutating methods: createMethods(RealmObject.prototype, objectTypes.OBJECT, [ 'isValid', - 'objectSchema' + 'objectSchema', + 'linkingObjects' ]); export function clearRegisteredConstructors() { diff --git a/lib/index.d.ts b/lib/index.d.ts index 9eb6817c..382db510 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -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 + */ + linkingObjects(objectType: string, property: string): Results; } const Object: { diff --git a/src/js_realm_object.hpp b/src/js_realm_object.hpp index 8f787329..1ca090ce 100644 --- a/src/js_realm_object.hpp +++ b/src/js_realm_object.hpp @@ -52,6 +52,7 @@ 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 &); const std::string name = "RealmObject"; @@ -64,6 +65,7 @@ struct RealmObjectClass : ClassDefinition { MethodMap const methods = { {"isValid", wrap}, {"objectSchema", wrap}, + {"linkingObjects", wrap}, }; }; @@ -152,3 +154,37 @@ std::vector> RealmObjectClass::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 +void realm::js::RealmObjectClass::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>(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::create_instance(ctx, realm::Results(object->realm(), std::move(tv)))); +} diff --git a/src/js_schema.hpp b/src/js_schema.hpp index 1ce3a7dd..44d1a629 100644 --- a/src/js_schema.hpp +++ b/src/js_schema.hpp @@ -75,6 +75,7 @@ Property Schema::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,20 +124,29 @@ Property Schema::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 { - prop.object_type = type; + + 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)) { @@ -177,14 +187,27 @@ ObjectSchema Schema::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::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::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)); diff --git a/src/rpc.cpp b/src/rpc.cpp index 369c7b41..32e693ed 100644 --- a/src/rpc.cpp +++ b/src/rpc.cpp @@ -462,6 +462,10 @@ json RPCServer::serialize_object_schema(const realm::ObjectSchema &object_schema for (auto &prop : object_schema.persisted_properties) { properties.push_back(prop.name); } + + for (auto &prop : object_schema.computed_properties) { + properties.push_back(prop.name); + } return { {"name", object_schema.name}, diff --git a/tests/js/index.js b/tests/js/index.js index 4c954735..7de4bfe1 100644 --- a/tests/js/index.js +++ b/tests/js/index.js @@ -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'), diff --git a/tests/js/linkingobjects-tests.js b/tests/js/linkingobjects-tests.js new file mode 100644 index 00000000..fb07a0ff --- /dev/null +++ b/tests/js/linkingobjects-tests.js @@ -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); + }); + }, +}; diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 57698620..b96315a1 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -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); diff --git a/tests/js/schemas.js b/tests/js/schemas.js index beb452f9..2abedc36 100644 --- a/tests/js/schemas.js +++ b/tests/js/schemas.js @@ -18,6 +18,8 @@ 'use strict'; +const Realm = require('realm'); + exports.TestObject = { name: 'TestObject', properties: { @@ -29,9 +31,11 @@ function PersonObject() {} PersonObject.schema = { name: 'PersonObject', properties: { - name: 'string', - age: 'double', - married: {type: 'bool', default: false}, + 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 = { @@ -117,19 +123,27 @@ exports.AllTypes = { name: 'AllTypesObject', primaryKey: 'primaryCol', properties: { - primaryCol: 'string', - boolCol: 'bool', - intCol: 'int', - floatCol: 'float', - doubleCol: 'double', - stringCol: 'string', - dateCol: 'date', - dataCol: 'data', - objectCol: 'TestObject', - arrayCol: {type: 'list', objectType: 'TestObject'}, + primaryCol: 'string', + boolCol: 'bool', + intCol: 'int', + floatCol: 'float', + doubleCol: 'double', + stringCol: 'string', + dateCol: 'date', + 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'} + } +}