diff --git a/CHANGELOG.md b/CHANGELOG.md index e7a8cc12..0f300600 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +X.Y.Z Release notes +============================================================= +### Breaking changes +* None + +### Enhancements +* Added `update` method to `Realm.Results` to support bulk updates (#808). + +### Bug fixes +* None + 2.0.0-rc19 Release notes (2017-10-7) ============================================================= ### Breaking changes diff --git a/docs/results.js b/docs/results.js index 2a406a33..245cda4e 100644 --- a/docs/results.js +++ b/docs/results.js @@ -28,4 +28,12 @@ * @memberof Realm */ class Results extends Collection { + /** + * Bulk update objects in the collection. + * @param {string} property - The name of the property. + * @param {string} value - The updated property value. + * @throws {Error} If no property with the name exists. + * @since 2.0.0-rc20 + */ + update(property, value) {} } diff --git a/lib/browser/results.js b/lib/browser/results.js index efb0306e..a35f9fda 100644 --- a/lib/browser/results.js +++ b/lib/browser/results.js @@ -25,6 +25,7 @@ import { createMethods } from './util'; export default class Results extends Collection { } +// Non-mutating methods: createMethods(Results.prototype, objectTypes.RESULTS, [ 'filtered', 'sorted', @@ -40,6 +41,11 @@ createMethods(Results.prototype, objectTypes.RESULTS, [ 'removeAllListeners', ]); +// Mutating methods: +createMethods(Results.prototype, objectTypes.RESULTS, [ + 'update', +], true); + export function createResults(realmId, info) { return createCollection(Results.prototype, realmId, info); } diff --git a/lib/index.d.ts b/lib/index.d.ts index a624aa70..40177b48 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -250,7 +250,13 @@ declare namespace Realm { * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Results.html } */ interface Results extends Collection { - + /** + * Bulk update objects in the collection. + * @param {string} property + * @param {any} value + * @returns void + */ + update(property: string, value: any): void; } const Results: { diff --git a/src/js_results.hpp b/src/js_results.hpp index 45e68a18..876512e1 100644 --- a/src/js_results.hpp +++ b/src/js_results.hpp @@ -87,6 +87,8 @@ struct ResultsClass : ClassDefinition, CollectionClass< template static void index_of(ContextType, Fn&, Arguments, ReturnValue &); + static void update(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); + // aggregate functions static void min(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); static void max(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); @@ -118,6 +120,7 @@ struct ResultsClass : ClassDefinition, CollectionClass< {"removeListener", wrap}, {"removeAllListeners", wrap}, {"indexOf", wrap}, + {"update", wrap}, }; PropertyMap const properties = { @@ -298,6 +301,32 @@ void ResultsClass::index_of(ContextType ctx, Fn& fn, Arguments args, ReturnVa } } +template +void ResultsClass::update(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { + validate_argument_count(argc, 2); + + std::string property = Value::validated_to_string(ctx, arguments[0], "property"); + auto results = get_internal>(this_object); + + auto schema = results->get_object_schema(); + if (!schema.property_for_name(StringData(property))) { + throw std::invalid_argument(util::format("No such property: %1", property)); + } + + auto realm = results->get_realm(); + if (!realm->is_in_transaction()) { + throw std::runtime_error("Can only 'update' objects within a transaction."); + } + + // TODO: This approach just moves the for-loop from JS to C++ + // Ideally, we'd implement this in OS or Core in an optimized fashion + for (auto i = results->size(); i > 0; i--) { + auto realm_object = realm::Object(realm, schema, results->get(i - 1)); + auto obj = RealmObjectClass::create_instance(ctx, realm_object); + RealmObjectClass::set_property(ctx, obj, property, arguments[1]); + } +} + template void ResultsClass::index_of(ContextType ctx, ObjectType this_object, Arguments args, ReturnValue &return_value) { diff --git a/tests/js/results-tests.js b/tests/js/results-tests.js index 76c3810a..111af616 100644 --- a/tests/js/results-tests.js +++ b/tests/js/results-tests.js @@ -626,4 +626,231 @@ module.exports = { TestCase.assertEqual(results.length, 0); TestCase.assertEqual(calls, 2); }, + + testResultsUpdate: function() { + const N = 5; + + var realm = new Realm({schema: [schemas.NullableBasicTypes]}); + realm.write(() => { + for(var i = 0; i < N; i++) { + realm.create('NullableBasicTypesObject', { intCol: 10 }); + } + }); + + // update should work on a basic result set + var results = realm.objects('NullableBasicTypesObject'); + TestCase.assertEqual(results.length, 5); + realm.write(() => { + results.update('intCol', 20); + }); + TestCase.assertEqual(results.length, 5); + TestCase.assertEqual(realm.objects('NullableBasicTypesObject').filtered('intCol = 20').length, 5); + + // update should work on a filtered result set + results = realm.objects('NullableBasicTypesObject').filtered('intCol = 20'); + realm.write(() => { + results.update('intCol', 10); + }); + TestCase.assertEqual(results.length, 0); + TestCase.assertEqual(realm.objects('NullableBasicTypesObject').filtered('intCol = 10').length, 5); + + // update should work on a sorted result set + results = realm.objects('NullableBasicTypesObject').filtered('intCol == 10').sorted('intCol'); + realm.write(() => { + results.update('intCol', 20); + }); + TestCase.assertEqual(results.length, 0); + TestCase.assertEqual(realm.objects('NullableBasicTypesObject').filtered('intCol = 20').length, 5); + + // update should work on a result snapshot + results = realm.objects('NullableBasicTypesObject').filtered('intCol == 20').snapshot(); + realm.write(() => { + results.update('intCol', 10); + }); + TestCase.assertEqual(results.length, 5); // snapshot length should not change + TestCase.assertEqual(realm.objects('NullableBasicTypesObject').filtered('intCol = 10').length, 5); + + realm.close(); + }, + + testResultsUpdateDataTypes: function() { + const N = 5; + + var realm = new Realm({schema: [schemas.NullableBasicTypes]}); + realm.write(() => { + for(var i = 0; i < N; i++) { + realm.create('NullableBasicTypesObject', { + boolCol: false, + stringCol: 'hello', + intCol: 10, + floatCol: 10.0, + doubleCol: 10.0, + dateCol: new Date(10) + }); + } + }); + + const testCases = [ + // col name, initial filter, initial filter pre-update count, initial filter post-update count, updated value, updated filter, updated filter post-update count + [ 'boolCol', 'boolCol = false', N, 0, true, 'boolCol = true', N ], + [ 'stringCol', 'stringCol = "hello"', N, 0, 'world', 'stringCol = "world"', N ], + [ 'intCol', 'intCol = 10', N, 0, 20, 'intCol = 20', N ], + [ 'floatCol', 'floatCol = 10.0', N, 0, 20.0, 'floatCol = 20.0', N ], + [ 'doubleCol', 'doubleCol = 10.0', N, 0, 20.0, 'doubleCol = 20.0', N ], + ]; + + testCases.forEach(function(testCase) { + var results = realm.objects('NullableBasicTypesObject').filtered(testCase[1]); + TestCase.assertEqual(results.length, testCase[2]); + + realm.write(() => { + results.update(testCase[0], testCase[4]); + }); + + TestCase.assertEqual(results.length, testCase[3]); + + results = realm.objects('NullableBasicTypesObject').filtered(testCase[5]); + TestCase.assertEqual(results.length, testCase[6]); + }); + + realm.close(); + }, + + testResultUpdateDateColumn: function() { + const N = 5; + + var realm = new Realm({schema: [schemas.NullableBasicTypes]}); + + // date column + realm.write(() => { + for(var i = 0; i < N; i++) { + realm.create('NullableBasicTypesObject', { + dateCol: new Date(1000) + }); + } + }); + + var results = realm.objects('NullableBasicTypesObject').filtered('dateCol = $0', new Date(1000)); + TestCase.assertEqual(results.length, N); + + realm.write(() => { + results.update('dateCol', new Date(2000)); + }); + + TestCase.assertEqual(results.length, 0); + results = realm.objects('NullableBasicTypesObject').filtered('dateCol = $0', new Date(2000)); + TestCase.assertEqual(results.length, N); + + realm.close(); + }, + + testResultsUpdateDataColumn: function() { + const N = 5; + + var RANDOM_DATA = new Uint8Array([ + 0xd8, 0x21, 0xd6, 0xe8, 0x00, 0x57, 0xbc, 0xb2, 0x6a, 0x15, 0x77, 0x30, 0xac, 0x77, 0x96, 0xd9, + 0x67, 0x1e, 0x40, 0xa7, 0x6d, 0x52, 0x83, 0xda, 0x07, 0x29, 0x9c, 0x70, 0x38, 0x48, 0x4e, 0xff, + ]); + + var realm = new Realm({schema: [schemas.NullableBasicTypes]}); + + // date column + realm.write(() => { + for(var i = 0; i < N; i++) { + realm.create('NullableBasicTypesObject', { + dataCol: null + }); + } + }); + + var results = realm.objects('NullableBasicTypesObject'); + TestCase.assertEqual(results.length, N); + + realm.write(() => { + results.update('dataCol', RANDOM_DATA); + }); + + for(var i = 0; i < results.length; i++) { + TestCase.assertArraysEqual(new Uint8Array(results[i].dataCol), RANDOM_DATA); + } + + realm.close(); + }, + + testResultsUpdateEmpty() { + var realm = new Realm({schema: [schemas.NullableBasicTypes]}); + + var emptyResults = realm.objects('NullableBasicTypesObject').filtered('stringCol = "hello"'); + TestCase.assertEqual(emptyResults.length, 0); + + realm.write(() => { + emptyResults.update('stringCol', 'no-op'); + }); + + TestCase.assertEqual(emptyResults.length, 0); + TestCase.assertEqual(realm.objects('NullableBasicTypesObject').filtered('stringCol = "no-op"').length, 0); + + realm.close(); + }, + + testResultsUpdateInvalidated() { + var realm = new Realm({schema: [schemas.TestObject]}); + realm.write(function() { + for (var i = 10; i > 0; i--) { + realm.create('TestObject', [i]); + } + }); + + var resultsVariants = [ + realm.objects('TestObject'), + realm.objects('TestObject').filtered('doubleCol > 1'), + realm.objects('TestObject').filtered('doubleCol > 1').sorted('doubleCol'), + realm.objects('TestObject').filtered('doubleCol > 1').snapshot() + ]; + + // test isValid + resultsVariants.forEach(function(objects) { + TestCase.assertEqual(objects.isValid(), true); + }); + + // close and test update + realm.close(); + realm = new Realm({ + schemaVersion: 1, + schema: [schemas.TestObject, schemas.BasicTypes] + }); + + resultsVariants.forEach(function(objects) { + TestCase.assertEqual(objects.isValid(), false); + TestCase.assertThrows(function() { objects.update('doubleCol', 42); }); + }); + }, + + testResultsUpdateWrongProperty() { + var realm = new Realm({schema: [schemas.NullableBasicTypes]}); + + const N = 5; + realm.write(() => { + for(var i = 0; i < N; i++) { + realm.create('NullableBasicTypesObject', { + stringCol: 'hello' + }); + } + }); + + var results = realm.objects('NullableBasicTypesObject').filtered('stringCol = "hello"'); + TestCase.assertEqual(results.length, N); + + TestCase.assertThrows(function() { + realm.write(() => { + results.update('unknownCol', 'world'); + }); + }); + + TestCase.assertThrows(function() { + results.update('stringCol', 'world'); + }); + + realm.close(); + } };