diff --git a/CHANGELOG.md b/CHANGELOG.md index cf3ad1d116..5231dd1895 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: Bump file format version to 21. NO DOWNGRADE PATH IS AVAILABLE. ### Enhancements * Adding Mixed types. ([#3389](https://github.com/realm/realm-js/issues/3389)) +* Added Set type ([#3378](https://github.com/realm/realm-js/issues/3378)). * Array of primitive lists will not be `snapshot()`'ed. * Added `ssl` option to `Realm.App.Sync` configuration. diff --git a/dependencies.list b/dependencies.list index 05df421b0c..646c99aedd 100644 --- a/dependencies.list +++ b/dependencies.list @@ -4,4 +4,4 @@ REALM_CORE_VERSION=10.1.4 REALM_SYNC_VERSION=10.1.6 NAPI_VERSION=4 OPENSSL_VERSION=1.1.1g -MDBREALM_TEST_SERVER_TAG=2021-04-07 \ No newline at end of file +MDBREALM_TEST_SERVER_TAG=2021-04-07 diff --git a/docs/realm.js b/docs/realm.js index 50cef23a5b..aece124a05 100644 --- a/docs/realm.js +++ b/docs/realm.js @@ -425,6 +425,9 @@ class Realm { * linkToObject: 'MyClass', * listOfObjects: 'MyClass[]', // or {type: 'list', objectType: 'MyClass'} * objectsLinkingToThisObject: {type: 'linkingObjects', objectType: 'MyClass', property: 'linkToObject'} + * + * setOfStrings: 'string<>', + * setOfOptionalStrings: 'string?<>', // or {type: 'set', objectType: 'string'} * } * }; */ @@ -433,16 +436,16 @@ class Realm { * @typedef Realm~ObjectSchemaProperty * @type {Object} * @property {Realm~PropertyType} type - The type of this property. - * @property {Realm~PropertyType} [objectType] - **Required** when `type` is `"list"` or `"linkingObjects"`, - * and must match the type of an object in the same schema, or, for `"list"` - * only, any other type which may be stored as a Realm property. + * @property {Realm~PropertyType} [objectType] - **Required** when `type` is `"list"`, `"set"` or `"linkingObjects"`, + * and must match the type of an object in the same schema, or, for `"list"` or `"set"`, + * other type which may be stored as a Realm property. * @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`. - * For `"list"` properties of non-object types, this instead signals whether the values inside the list may be assigned `null` or `undefined`. - * This is not supported for `"list"` properties of object types and `"linkingObjects"` properties. + * For `"list"` or `"set"` properties of non-object types, this instead signals whether the values inside the list may be assigned `null` or `undefined`. + * This is not supported for `"list"` or `"set"` properties of object types and `"linkingObjects"` properties. * @property {boolean} [indexed] - Signals if this property should be indexed. Only supported for * `"string"`, `"int"`, and `"bool"` properties. * @property {string} [mapTo] - Set this to the name of the underlying property in the Realm file if the Javascript property @@ -465,15 +468,18 @@ class Realm { * * When specifying property types in an {@linkplain Realm~ObjectSchema object schema}, you * may append `?` to any of the property types to indicate that it is optional - * (i.e. it can be `null` in addition to the normal values) and `[]` to - * indicate that it is instead a list of that type. For example, - * `optionalIntList: 'int?[]'` would declare a property which is a list of - * nullable integers. The property types reported by {@linkplain Realm.Collection - * collections} and in a Realm's schema will never + * (i.e. it can be `null` in addition to the normal values). + * Given a type, _T_, the following postfix operators may be used: + * * _T_ `[]` indicates that the property is a {@linkplain Realm.List} of values with of type _T_ + * * _T_ `<>` indicated that the property is a {@linkplain Realm.Set} of values with type _T_ + * + * For example, `optionalIntList: 'int?[]'` declares a property which is a list of + * nullable integers, while `optionalStringSet: 'string?<>'` declares a set of nullable strings. + * The property types reported by {@linkplain Realm.Collection collections} and in a Realm's schema will never * use these forms. * * @typedef Realm~PropertyType - * @type {("bool"|"int"|"float"|"double"|"string"|"decimal128"|"objectId"|"date"|"data"|"list"|"linkingObjects"|"")} + * @type {("bool"|"int"|"float"|"double"|"string"|"decimal128"|"objectId"|"date"|"data"|"list"|"set"|"linkingObjects"|"")} * * @property {Mixed} "mixed" - Property value that allow any of the following types (`"bool","int","float","double","string","decimal128","objectId","date","data"`), this type is nullable by default. * @property {boolean} "bool" - Property value may either be `true` or `false`. @@ -493,6 +499,8 @@ class Realm { * @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.Set} "set" - Property may be assigned an array (e.g., `Array`) 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}. diff --git a/docs/set.js b/docs/set.js new file mode 100644 index 0000000000..b9fc8a9fd1 --- /dev/null +++ b/docs/set.js @@ -0,0 +1,77 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2021 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. +// +//////////////////////////////////////////////////////////////////////////// + +/** + * Instances of this class will be returned when accessing object properties whose type is `"Set"` + * (see {@linkplain Realm~ObjectSchemaProperty ObjectSchemaProperty}). + * + + * Sets mostly behave like normal JavaScript Sets, with a few exceptions: + * They can only store values of a single type (indicated by the `type` + * and `optional` properties of the Set). + * They can only be modified inside a {@linkplain Realm#write write} transaction. + * Unlike JavaScript's Set, Realm~Set does NOT make any guarantees about the + * traversal order of `values()`, `entries()`, `keys()`, or `forEach` iterations. + * If values in a Set are required to have some order, it must be implemented + * by the developer by, for example, wrapping values in an object that holds + * a user-supplied insertion order. + * + * @extends Realm.Collection + * @memberof Realm + */ +class Set extends Collection { + /** + * Remove a value from the Set + * @param {T} value Value to delete from the Set + * @throws {Error} If not inside a write transaction. + * @returns {boolean}: true if the value existed in the Set, false otherwise + */ + delete(value) { } + + /** + * Remove all values from the Set + * @throws {Error} If not inside a write transaction. + * @returns {void} + */ + clear() { } + + /** + * Add a value to the Set + * + * @param {T} value Value to add to the Set + * @throws {TypeError} If a `value` is not of a type which can be stored in + * the Set, or if an object being added to the Set does not match the + * {@linkcode Realm~ObjectSchema object schema} for the Set. + * + * @throws {Error} If not inside a write transaction. + * @returns {Realm.Set}: The Set itself, after adding the element + */ + add(value) { } + + /** + * Check for the existence of a value in the Set + * + * @param {T} value Value to to search for in the Set + * @throws {TypeError} If a `value` is not of a type which can be stored in + * the Set, or if an object being added to the Set does not match the + * {@linkcode Realm~ObjectSchema object schema} for the Set. + * + * @returns {boolean}: True if the value exists in the Set, false otherwise + */ + has(value) { } +} diff --git a/lib/collection-methods.js b/lib/collection-methods.js index 28fad893ac..2eb4cda52d 100644 --- a/lib/collection-methods.js +++ b/lib/collection-methods.js @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////// // -// Copyright 2016 Realm Inc. +// Copyright 2021 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -55,7 +55,8 @@ Object.defineProperty(iteratorPrototype, Symbol.iterator, { ['entries', 'keys', 'values'].forEach(function(methodName) { var method = function() { - var self = (this.type === "object") ? this.snapshot() : this; + const isSet = this instanceof Realm.Set; + var self = (this.type === "object" || isSet) ? this.snapshot() : this; var index = 0; return Object.create(iteratorPrototype, { @@ -69,13 +70,18 @@ Object.defineProperty(iteratorPrototype, Symbol.iterator, { var value; switch (methodName) { case 'entries': - value = [index, self[index]]; + value = isSet ? + [self[index], self[index]] + : [index, self[index]]; break; case 'keys': - value = index; + value = isSet ? + undefined + : index; break; default: value = self[index]; + break; } index++; diff --git a/lib/extensions.js b/lib/extensions.js index 29e845a174..1a2f678f45 100644 --- a/lib/extensions.js +++ b/lib/extensions.js @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////// // -// Copyright 2016 Realm Inc. +// Copyright 2021 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -66,11 +66,13 @@ function openLocalRealm(realmConstructor, config) { module.exports = function(realmConstructor, environment) { // Add the specified Array methods to the Collection prototype. Object.defineProperties(realmConstructor.Collection.prototype, require('./collection-methods')); + Object.defineProperties(realmConstructor.Set.prototype, require('./set-methods')(realmConstructor)); setConstructorOnPrototype(realmConstructor.Collection); setConstructorOnPrototype(realmConstructor.List); setConstructorOnPrototype(realmConstructor.Results); setConstructorOnPrototype(realmConstructor.Object); + setConstructorOnPrototype(realmConstructor.Set); realmConstructor.BSON = require('bson'); realmConstructor._Decimal128 = realmConstructor.BSON.Decimal128; @@ -90,6 +92,7 @@ module.exports = function(realmConstructor, environment) { enumerable: false }); + const getInternalCacheId = (realmObj) => { const { name, primaryKey } = realmObj.objectSchema(); const id = primaryKey ? realmObj[primaryKey] : realmObj._objectId(); diff --git a/lib/set-methods.js b/lib/set-methods.js new file mode 100644 index 0000000000..562c274b39 --- /dev/null +++ b/lib/set-methods.js @@ -0,0 +1,37 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2021 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'; + +module.exports = function(realmConstructor) { + function forEach(callback) { + const elements = Array.from(this.values()); + elements.forEach(element => callback(element)); + }; + + function toJSON(_, cache = new Map()) { + const elementArray = Array.from(this.values()); + return elementArray.map((item, index) => + item instanceof realmConstructor.Object ? item.toJSON(index.toString(), cache) : item); + }; + + return { + forEach: {value: forEach, configurable: true, writable: true}, + toJSON: {value: toJSON, configurable: true, writable: true} + }; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4725a4aae9..bc409ab3a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3058,7 +3058,7 @@ }, "compare-versions": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "resolved": false, "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", "dev": true }, diff --git a/src/js_object_accessor.hpp b/src/js_object_accessor.hpp index e53d04d82b..6c778b936b 100644 --- a/src/js_object_accessor.hpp +++ b/src/js_object_accessor.hpp @@ -22,6 +22,7 @@ #include "js_mixed.hpp" #include "js_list.hpp" +#include "js_set.hpp" #include "js_realm_object.hpp" #include "js_schema.hpp" @@ -165,7 +166,7 @@ class NativeAccessor { return ResultsClass::create_instance(m_ctx, std::move(results)); } ValueType box(realm::object_store::Set set) { - throw std::runtime_error("'Set' type support is not implemented yet"); + return SetClass::create_instance(m_ctx, std::move(set)); } ValueType box(realm::object_store::Dictionary dictionart) { throw std::runtime_error("'Dictionary' type support is not implemented yet"); @@ -233,8 +234,12 @@ class NativeAccessor { return false; } - bool is_same_set(realm::object_store::Set const& set, ValueType const& value) const { - throw std::runtime_error("'Set' type support is not implemented yet"); + bool is_same_set(realm::object_store::Set const &set, ValueType const &value) const { + auto object = Value::validated_to_object(m_ctx, value); + if (js::Object::template is_instance>(m_ctx, object)) { + return set == *get_internal>(m_ctx, object); + } + return false; } bool is_same_dictionary(realm::object_store::Dictionary const& dictionary, ValueType const& value) const { diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 755c59347d..8feb73df98 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -23,6 +23,7 @@ #include "js_util.hpp" #include "js_realm_object.hpp" #include "js_list.hpp" +#include "js_set.hpp" #include "js_results.hpp" #include "js_schema.hpp" #include "js_observable.hpp" @@ -481,12 +482,14 @@ inline typename T::Function RealmClass::create_constructor(ContextType ctx) { FunctionType realm_constructor = ObjectWrap>::create_constructor(ctx); FunctionType collection_constructor = ObjectWrap>::create_constructor(ctx); FunctionType list_constructor = ObjectWrap>::create_constructor(ctx); + FunctionType set_constructor = ObjectWrap>::create_constructor(ctx); FunctionType realm_object_constructor = ObjectWrap>::create_constructor(ctx); FunctionType results_constructor = ObjectWrap>::create_constructor(ctx); PropertyAttributes attributes = ReadOnly | DontEnum | DontDelete; Object::set_property(ctx, realm_constructor, "Collection", collection_constructor, attributes); Object::set_property(ctx, realm_constructor, "List", list_constructor, attributes); + Object::set_property(ctx, realm_constructor, "Set", set_constructor, attributes); Object::set_property(ctx, realm_constructor, "Results", results_constructor, attributes); Object::set_property(ctx, realm_constructor, "Object", realm_object_constructor, attributes); diff --git a/src/js_schema.hpp b/src/js_schema.hpp index 0b0b8e3d5e..d70f0bb7a1 100644 --- a/src/js_schema.hpp +++ b/src/js_schema.hpp @@ -1,6 +1,6 @@ //////////////////////////////////////////////////////////////////////////// // -// Copyright 2016 Realm Inc. +// Copyright 2021 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ #pragma once #include - #include "js_types.hpp" #include @@ -80,6 +79,11 @@ static inline void parse_property_type(StringData object_name, Property& prop, S type = type.substr(0, type.size() - 2); } + if (type.ends_with("<>")) { + prop.type |= PropertyType::Set; + type = type.substr(0, type.size() - 2); + } + if (type.ends_with("?")) { prop.type |= PropertyType::Nullable; type = type.substr(0, type.size() - 1); @@ -159,6 +163,7 @@ static inline void parse_property_type(StringData object_name, Property& prop, S prop.type |= PropertyType::UUID | PropertyType::Array; prop.object_type = ""; } + else { if (is_nullable(prop.type)) { throw std::logic_error(util::format("List property '%1.%2' cannot be optional", object_name, prop.name)); @@ -169,6 +174,10 @@ static inline void parse_property_type(StringData object_name, Property& prop, S prop.type |= PropertyType::Object | PropertyType::Array; } } + else if (type == "set") { + // apply the correct properties for sets + realm::js::set::derive_property_type(object_name, prop); // may throw std::logic_error + } else if (type == "linkingObjects") { prop.type |= PropertyType::LinkingObjects | PropertyType::Array; } @@ -182,7 +191,7 @@ static inline void parse_property_type(StringData object_name, Property& prop, S } // Object properties are implicitly optional - if (prop.type == PropertyType::Object && !is_array(prop.type)) { + if (prop.type == PropertyType::Object && !is_array(prop.type) && !is_set(prop.type)) { prop.type |= PropertyType::Nullable; } } diff --git a/src/js_set.hpp b/src/js_set.hpp new file mode 100644 index 0000000000..05050fc298 --- /dev/null +++ b/src/js_set.hpp @@ -0,0 +1,513 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2021 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. +// +//////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "js_collection.hpp" +#include "js_object_accessor.hpp" +#include "js_realm_object.hpp" +#include "js_results.hpp" +#include "js_types.hpp" +#include "js_util.hpp" + +#include +#include + +namespace realm { +namespace js { + +template +class NativeAccessor; + +namespace set { + /** + * @brief Derive and apply property flags for \ref Set. + * + * @param object_name Name of the Set object (for error reporting purposes) + * @param prop (mutable) Property object that will be changed to be correct for \ref Set + * @exception std::logic_error Thrown if the the prop argument contains an invalid property configuration + */ +inline static void derive_property_type(StringData const &object_name, Property &prop) { + using realm::PropertyType; + + if (prop.object_type == "bool") { + prop.type |= PropertyType::Bool | PropertyType::Set; + prop.object_type = ""; + } + else if (prop.object_type == "int") { + prop.type |= PropertyType::Int | PropertyType::Set; + prop.object_type = ""; + } + else if (prop.object_type == "float") { + prop.type |= PropertyType::Float | PropertyType::Set; + prop.object_type = ""; + } + else if (prop.object_type == "double") { + prop.type |= PropertyType::Double | PropertyType::Set; + prop.object_type = ""; + } + else if (prop.object_type == "string") { + prop.type |= PropertyType::String | PropertyType::Set; + prop.object_type = ""; + } + else if (prop.object_type == "date") { + prop.type |= PropertyType::Date | PropertyType::Set; + prop.object_type = ""; + } + else if (prop.object_type == "data") { + prop.type |= PropertyType::Data | PropertyType::Set; + prop.object_type = ""; + } + else if (prop.object_type == "decimal128") { + prop.type |= PropertyType::Decimal | PropertyType::Set; + prop.object_type = ""; + } + else if (prop.object_type == "objectId") { + prop.type |= PropertyType::ObjectId | PropertyType::Set; + prop.object_type = ""; + } + else if (prop.object_type == "uuid") { + prop.type |= PropertyType::UUID | PropertyType::Set; + prop.object_type = ""; + } + else { + if (is_nullable(prop.type)) { + throw std::logic_error(util::format("Set property '%1.%2' cannot be optional", object_name, prop.name)); + } + if (is_array(prop.type)) { + throw std::logic_error(util::format("Set property '%1.%2' must have a non-list value type", object_name, prop.name)); + } + prop.type |= PropertyType::Object | PropertyType::Set; + } +} // validated_property_type() + +}; // set namespace + +/** + * @brief Glue class that provides an interface between \ref SetClass and \ref realm::object_store::Set + * + * The Set class itself is an internal glue that delegates operations from \ref SetClass to + * \ref realm::object_store::Set. It is used by Realm-JS's object management system, and it + * not meant to be instantiated directly. + * + * @tparam T The type of the elements that the Set will hold. Inherited from \ref SetClass + */ +template +class Set : public realm::object_store::Set { + public: + Set(const realm::object_store::Set &set) : realm::object_store::Set(set) {} + void derive_property_type(StringData const &object_name, Property &prop) const; + + std::vector, NotificationToken>> m_notification_tokens; +}; + + +/** + * @brief Implementation class for JavaScript's [Set](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) class + * + * @tparam T The type of the elements that the SetClass will hold. + */ +template +struct SetClass : ClassDefinition, CollectionClass> { + using Type = T; ///< type of the elements that the SetClass holds + using ContextType = typename T::Context; ///< JS context type + using ObjectType = typename T::Object; + using ValueType = typename T::Value; + using FunctionType = typename T::Function; + using Object = js::Object; + using Value = js::Value; + using ReturnValue = js::ReturnValue; + using Arguments = js::Arguments; + + static ObjectType create_instance(ContextType, realm::object_store::Set); + + // properties + static void get_size(ContextType, ObjectType, ReturnValue &); + static void get_optional(ContextType, ObjectType, ReturnValue &); + static void get_indexed(ContextType, ObjectType, uint32_t, ReturnValue &); + static void get_type(ContextType, ObjectType, ReturnValue &); + + // methods + static void add(ContextType, ObjectType, Arguments &, ReturnValue &); + static void get(ContextType, ObjectType, Arguments &, ReturnValue &); + static void clear(ContextType, ObjectType, Arguments &, ReturnValue &); + static void delete_element(ContextType, ObjectType, Arguments &, ReturnValue &); + static void has(ContextType, ObjectType, Arguments &, ReturnValue &); + + + static void filtered(ContextType, ObjectType, Arguments &, ReturnValue &); + static void snapshot(ContextType, ObjectType, Arguments &, ReturnValue &); + + // observable + static void add_listener(ContextType, ObjectType, Arguments &, ReturnValue &); + static void remove_listener(ContextType, ObjectType, Arguments &, ReturnValue &); + static void remove_all_listeners(ContextType, ObjectType, Arguments &, ReturnValue &); + + std::string const name = "Set"; + + MethodMap const methods = { + {"add", wrap}, + {"clear", wrap}, + {"delete", wrap}, + {"has", wrap}, + {"filtered", wrap}, + + {"min", wrap, AggregateFunc::Min>>}, + {"max", wrap, AggregateFunc::Max>>}, + {"sum", wrap, AggregateFunc::Sum>>}, + {"avg", wrap, AggregateFunc::Avg>>}, + + + {"snapshot", wrap}, + {"addListener", wrap}, + {"removeListener", wrap}, + {"removeAllListeners", wrap}, + }; + + PropertyMap const properties = { + {"size", {wrap, nullptr}}, + {"type", {wrap, nullptr}}, + {"optional", {wrap, nullptr}}, + }; + + IndexPropertyType const index_accessor = {nullptr, nullptr}; + +private: + static void validate_value(ContextType, realm::object_store::Set &, ValueType); +}; + +template +typename T::Object SetClass::create_instance(ContextType ctx, realm::object_store::Set set) { + return create_object>(ctx, new realm::js::Set(std::move(set))); +} + + +/** + * @brief Implements JavaScript Set's `.size` property + * + * Returns the number of elements in the SetClass. + * See [MDN's reference documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/size) + * + * @param ctx JS context + * @param object \ref ObjectType wrapping the SetClass itself + * @param return_value \ref ReturnValue wrapping an integer that gives the number of elements in the set to return to the JS context + */ +template +void SetClass::get_size(ContextType ctx, ObjectType object, ReturnValue &return_value) { + auto set = get_internal>(ctx, object); + return_value.set(static_cast(set->size())); +} + + +/** + * @brief Accessor for elements at a given index in the set. + * + * For internal use only! + * + * @param ctx JS context + * @param object \ref ObjectType wrapping the SetClass itself + * @param index Index of the element to retrieve + * @param return_value \ref ReturnValue wrapping an integer that gives the number of elements in the set to return to the JS context + */ +template +void SetClass::get_indexed(ContextType ctx, ObjectType object, uint32_t index, ReturnValue &return_value) { + auto set = get_internal>(ctx, object); + NativeAccessor accessor(ctx, *set); + return_value.set(set->get(accessor, index)); +} + + +/** + * @brief Check whether the Set's element type is marked as optional (`nullable`) + * + * @param ctx JS context + * @param object \ref ObjectType wrapping the SetClass itself + * @param return_value \ref ReturnValue wrapping a boolean that is true if the Set's element type is nullable, false otherwise + */ +template +void SetClass::get_optional(ContextType ctx, ObjectType object, ReturnValue &return_value) { + auto set = get_internal>(ctx, object); + return_value.set(is_nullable(set->get_type())); +} + + +/** + * @brief Implements JavaScript Set's add() method + * + * Adds a single element, `A`, of type `T` to the set. `A` will not be added if it + * already exists within the SetClass. + * See [MDN's reference documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add). + * + * @param ctx JS context + * @param this_object \ref ObjectType wrapping the SetClass itself + * @param args \ref Arguments structure containing a single new element of type `T` to add to the Set + * @param return_value \ref ReturnValue wrapping the Set itself, including the newly added element to return to the JS context + */ +template +void SetClass::add(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + args.validate_maximum(1); + + auto set = get_internal>(ctx, this_object); + + for (size_t i = 0; i < args.count; i++) { + validate_value(ctx, *set, args[i]); + } + + NativeAccessor accessor(ctx, *set); + for (size_t i = 0; i < args.count; i++) { + set->insert(accessor, args[i]); + } + + return_value.set(this_object); +} + + +/** + * @brief Index-based accessing to the Set + * + * Returns a single element found at the given index + * For internal use only! + * + * @param ctx JS context + * @param this_object \ref ObjectType wrapping the SetClass itself + * @param args \ref Arguments structure containing a single integer + * @param return_value \ref ReturnValue wrapping a `Mixed` object, wrapping the found element to return to the JS context + */ +template +void SetClass::get(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + args.validate_maximum(1); + + if (!Value::is_number(ctx, args[0])) { + throw std::invalid_argument("Argument to get() must be a number."); + } + + auto set = get_internal>(ctx, this_object); + NativeAccessor accessor(ctx, *set); + auto const value_type = set->get_type(); + + switch_on_type(value_type, [&](auto const type_indicator) -> void { + using element_type = std::remove_pointer_t; + int requested_index = Value::validated_to_number(ctx, args[0]); + realm::Mixed element_value = set->template get>(requested_index); + return_value.set(element_value); + }); +} + + +/** + * @brief Implements JavaScript Set's `clear()` method. Removes all elements from the set. + * + * Empties the set, removing all elements. + * Returns `undefined` to the JS context. + * See [MDN's reference documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear). + * + * @param ctx JS context + * @param this_object \ref ObjectType wrapping the SetClass itself + * @param args Empty \ref Arguments structure + * @param return_value \ref ReturnValue wrapping `undefined` to return to the JS context + */ +template +void SetClass::clear(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + args.validate_maximum(0); + + auto set = get_internal>(ctx, this_object); + set->remove_all(); + return_value.set_undefined(); +} + + +/** + * @brief Implements JavaScript Set's `delete()` method. Removes a single element from the set. + * + * Attempts to remove the given element from the set. + * Returns `true` if the element was present, `false` otherwise. + * See [MDN's reference documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete). + * + * @param ctx JS context + * @param this_object \ref ObjectType wrapping the SetClass itself + * @param args \ref Arguments structure containing a single element to remove + * @param return_value \ref ReturnValue wrapping the value to return to the JS context + */ +template +void SetClass::delete_element(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + args.validate_maximum(1); + + auto const set = get_internal>(ctx, this_object); + auto const index = args[0]; + + validate_value(ctx, *set, index); + NativeAccessor accessor(ctx, *set); + std::pair const success = set->remove(accessor, index); + + return_value.set(success.second); +} + + +/** + * @brief Implements JavaScript Set's has() method. + * + * has() checks whether the given element exists in the set. + * Sets return_value to true if the element is found, false otherwise. + * See [MDN's reference documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/has). + * + * @param ctx JS context + * @param this_object \ref ObjectType wrapping the SetClass itself + * @param args \ref Arguments structure containing a single element of type `T` to search for + * @param return_value \ref ReturnValue wrapping the value to return to the JS context + */ +template +void SetClass::has(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + args.validate_maximum(1); + + auto const set = get_internal>(ctx, this_object); + auto const value = args[0]; + + validate_value(ctx, *set, value); + NativeAccessor accessor(ctx, *set); + + // set->find will return npos if the element is not found + size_t const index = set->find(accessor, value); + return_value.set(index != npos); +} + + +/** + * @brief Creates a \ref ResultClass containing a subset of the set's elements + * + * Applies a filter to the elements in the SetClass and returns the elements that match the filter. + * Filters are only supported for sets of objects. + * Will throw `std::runtime_error` if the Set's element type is not objects. + * + * @param this_object \ref ObjectType wrapping the SetClass itself + * @param args \ref Arguments structure containing the filter that will be applied to the SetClass + * @param return_value \ref ReturnValue wrapping a \ref ResultClass containing matching objects to return to the JS context + * @exception std::runtime_error Thrown if the \ref SetClass does not contain objects + */ +template +void SetClass::filtered(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + args.validate_maximum(1); + + auto const set = get_internal>(ctx, this_object); + return_value.set(ResultsClass::create_filtered(ctx, *set, args)); +} + + +/** + * @brief Return a textual description of the value element type for the given set + * + * @param ctx JS context + * @param object \ref ObjectType wrapping the SetClass itself + * @param return_value \ref ReturnValue wrapping a static `const char *` descriping the set's element type + */ +template +void SetClass::get_type(ContextType ctx, ObjectType object, ReturnValue &return_value) { + auto const set = get_internal>(ctx, object); + return_value.set(local_string_for_property_type(set->get_type() & ~realm::PropertyType::Flags)); +} + + +/** + * @brief Utility function that validates that elements of a given type is eligible for insertion into the set + * + * Checks whether a given value type is legal for the given SetClass. + * Throws \ref TypeErrorException if the value is not legal. + * + * @param ctx JS context + * @param set \ref realm::object_store::Set that contains the valid value type + * @param value \ref ValueType that is to be checked whether it is valid for the set + * @exception TypeErrorException Thrown if `value` is not valid for the set + */ +template +void SetClass::validate_value(ContextType ctx, realm::object_store::Set &set, ValueType value) { + auto type = set.get_type(); + StringData object_type; + if (type == realm::PropertyType::Object) { + object_type = set.get_object_schema().name; + } + if (!Value::is_valid_for_property_type(ctx, value, type, object_type)) { + throw TypeErrorException("Property", object_type ? object_type : local_string_for_property_type(type), Value::to_string(ctx, value)); + } +} + + +/** + * @brief Create a snapshot of the Set in the Realm database + * + * @param ctx JS context + * @param set \ref realm::object_store::Set that contains the valid value type + * @param value \ref ValueType that is to be checked whether it is valid for the set + * @param args None -- must be empty + * @param return_value \ref ReturnValue wrapping a a new Set instance created by the snapshot + */ +template +void SetClass::snapshot(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + args.validate_maximum(0); + auto set = get_internal>(ctx, this_object); + return_value.set(ResultsClass::create_instance(ctx, set->snapshot())); +} + + +/** + * @brief Add a new listener on the Set + * + * @param ctx JS context + * @param set \ref realm::object_store::Set that contains the valid value type + * @param args A single argument containing a callback function + * @param return_value None + */ +template +void SetClass::add_listener(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + // args is validated by ResultClass + auto set = get_internal>(ctx, this_object); + ResultsClass::add_listener(ctx, *set, this_object, args); +} + + +/** + * @brief Remove a listener that was previously registered on the Set + * + * @param ctx JS context + * @param set \ref realm::object_store::Set that contains the valid value type + * @param args A single argument containing the callback function of the previously-registered listener + * @param return_value None + */ +template +void SetClass::remove_listener(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + // args is validated by ResultClass + auto set = get_internal>(ctx, this_object); + ResultsClass::remove_listener(ctx, *set, this_object, args); +} + + +/** + * @brief Remove all listeners registered on the Set + * + * @param ctx JS context + * @param set \ref realm::object_store::Set that contains the valid value type + * @param args None + * @param return_value None + */ +template +void SetClass::remove_all_listeners(ContextType ctx, ObjectType this_object, Arguments &args, ReturnValue &return_value) { + args.validate_maximum(0); + auto set = get_internal>(ctx, this_object); + set->m_notification_tokens.clear(); +} + +} // js +} // realm diff --git a/src/js_types.hpp b/src/js_types.hpp index ff6a163873..1a8873732e 100644 --- a/src/js_types.hpp +++ b/src/js_types.hpp @@ -56,6 +56,9 @@ struct ResultsClass; template struct ListClass; +template +struct SetClass; + enum PropertyAttributes : unsigned { None = 0, ReadOnly = 1 << 0, @@ -507,7 +510,7 @@ inline bool Value::is_valid_for_property_type(ContextType context, const Valu && (type != PropertyType::Object || list->get_object_schema().name == object_type); }; - if (!realm::is_array(type)) { + if (!realm::is_array(type) && !realm::is_set(type)) { return check_value(value); } diff --git a/tests/js/index.js b/tests/js/index.js index fd9f534869..04f72cd6f9 100644 --- a/tests/js/index.js +++ b/tests/js/index.js @@ -46,7 +46,8 @@ var TESTS = { AliasTests: require("./alias-tests"), BsonTests: require("./bson-tests"), // Garbagecollectiontests: require('./garbage-collection'), - ArrayBuffer: require("./array-buffer-tests") + ArrayBuffer: require("./array-buffer-tests"), + SetTests: require("./set-tests") }; //FIXME: MIXED: fix for JSC @@ -69,6 +70,7 @@ if (global.enableSyncTests) { TESTS.SessionTests = require("./session-tests"); TESTS.UUIDSyncTests= node_require("./uuid-sync-tests"); TESTS.PartitionValueTests = node_require("./partition-value-tests"); + TESTS.SetSyncTests = node_require("./set-sync-tests"); } } diff --git a/tests/js/set-sync-tests.js b/tests/js/set-sync-tests.js new file mode 100644 index 0000000000..e67b22edf2 --- /dev/null +++ b/tests/js/set-sync-tests.js @@ -0,0 +1,232 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2021 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'; +const AppConfig = require("./support/testConfig"); + +// Prevent React Native packager from seeing modules required with this +const require_method = require; +function nodeRequire(module) { + return require_method(module); +} + +const Realm = require('realm'); +const TestCase = require('./asserts'); + +const isNodeProcess = typeof process === 'object' && process + '' === '[object process]'; +const isElectronProcess = typeof process === 'object' && process.versions && process.versions.electron; +const fs = isNodeProcess ? nodeRequire('fs-extra') : require('react-native-fs'); + +module.exports = { + async testSetSyncedNonRequired() { + // test that we can create a synced realm with a Set + // that isn't required + if (!global.enableSyncTests) return; + + const schema = { + name: "SyncedNumbers", + primaryKey: "_id", + properties: { + _id: "int", + numbers: "int<>", + } + }; + + const appConfig = AppConfig.integrationAppConfig; + const app = new Realm.App(appConfig); + const credentials = Realm.Credentials.anonymous(); + + const user = await app.logIn(credentials); + const config = { + sync: { + user, + partitionValue: "_id", + _sessionStopPolicy: "immediately", // Make it safe to delete files after realm.close() + }, + schema: [schema] + }; + + const realm = await Realm.open(config); + realm.write(() => { + realm.deleteAll(); + }); + + let objects = realm.objects(schema.name); + TestCase.assertEqual(objects.length, 0, "Table should be empty"); + }, + + // + // test that deletions and additions to a Set are propagated correctly through Sync + async testSetSyncedAddDelete() { + // tests a synced realm while adding/deleting elements in a Set + if (!global.enableSyncTests) return; + + const schema = { + name: "SyncedNumbers", + primaryKey: "_id", + properties: { + _id: "int", + numbers: "int<>", + } + } + + const appConfig = AppConfig.integrationAppConfig; + const app = new Realm.App(appConfig); + const credentials = Realm.Credentials.anonymous(); + + const user = await app.logIn(credentials); + const config = { + sync: { + user, + partitionValue: "_id", + _sessionStopPolicy: "immediately", // Make it safe to delete files after realm.close() + }, + schema: [schema] + }; + const realm = await Realm.open(config); + + realm.write(() => { + realm.deleteAll(); + }); + + // TODO: fix Error: mySetfrew.mandatory must be of type 'number', got 'number' (2) + realm.write(() => { + realm.create(schema.name, { + _id: 77, + numbers: [2], + }); + }); + + await realm.syncSession.uploadAllLocalChanges(); + + let objects = realm.objects(schema.name); + TestCase.assertEqual(objects.length, 1, "There should be 1 object"); + + // add an element to the Set + realm.write(() => { + objects[0].numbers.add(5); + }); + await realm.syncSession.uploadAllLocalChanges(); + + // there should still only be one object + TestCase.assertEqual(objects.length, 1, "Number of objects should be 1"); + // .. but the object's Set should have two elements + TestCase.assertEqual(objects[0].numbers.size, 2, "Size of `numbers` should be 2"); + + // add an element to the Set, then delete another one + realm.write(() => { + objects[0].numbers.add(6).delete(2); + }); + await realm.syncSession.uploadAllLocalChanges(); + + objects = realm.objects(schema.name); + // there should still only be one object + TestCase.assertEqual(objects.length, 1, "Number of objects should be 1"); + // .. but the object's Set should have two elements + TestCase.assertEqual(objects[0].numbers.size, 2, "Size of `numbers` should be 2"); + + realm.write(() => { + objects[0].numbers.clear(); + }); + await realm.syncSession.uploadAllLocalChanges(); + objects = realm.objects(schema.name); + // there should still only be one object + TestCase.assertEqual(objects.length, 1, "Number of objects should still be 1"); + // .. but the object's Set should have two elements + TestCase.assertEqual(objects[0].numbers.size, 0, "Size of `numbers` should be 0"); + + realm.close(); + }, + + async testSetSyncedDownstream() { + if (!global.enableSyncTests) return; + + const schema = { + name: "SyncedNumbers", + primaryKey: "_id", + properties: { + _id: "int", + numbers: "int<>", + } + }; + + const appConfig = AppConfig.integrationAppConfig; + const app = new Realm.App(appConfig); + const credentials = Realm.Credentials.anonymous(); + const user = await app.logIn(credentials); + const config = { + sync: { + user, + partitionValue: "_id", + _sessionStopPolicy: "immediately", // Make it safe to delete files after realm.close() + }, + schema: [schema] + }; + const realm = await Realm.open(config); + + // clear out any lingering documents + realm.write(() => { + realm.deleteAll(); + }); + + const integerArray = [1, 2, 3, 4, 5, 6, 7]; + realm.write(() => { + realm.create(schema.name, { + _id: 0, + numbers: integerArray + }); + }); + // make sure everything is synced upstream + await realm.syncSession.uploadAllLocalChanges(); + + // make sure everything is in the database + let integers = realm.objects(schema.name)[0]; + TestCase.assertEqual(integers.numbers.size, 7, "There should be 7 integers"); + + // make sure we don't have a local copy of the realm + realm.close(); + Realm.deleteFile(config); + + // create a new local realm and sync from server + const syncedRealm = await Realm.open(config) + await syncedRealm.syncSession.downloadAllServerChanges(); + + // check that our set of integers is the same as before + let syncedIntegers = syncedRealm.objects(schema.name)[0].numbers; + TestCase.assertEqual(syncedIntegers.size, 7, "There still should be 7 integers"); + + const intsValues = Array.from(syncedIntegers.values()); + let locatedElements = 0; + // make sure that every element pulled from the database is also + // in the original array (this only works because all values in integerArray + // are unique) + intsValues.forEach(dbValue => { + if (integerArray.find(arrrayValue => arrrayValue == dbValue)) { + locatedElements++; + } + }); + TestCase.assertEqual(locatedElements, integerArray.length, "Downloaded integers should be the same as uploaded integers"); + + // clean up the objects we created + syncedRealm.write(() => { + syncedRealm.deleteAll(); + }); + await syncedRealm.syncSession.uploadAllLocalChanges(); + realm.close(); + }, +}; // module.exports diff --git a/tests/js/set-tests.js b/tests/js/set-tests.js new file mode 100644 index 0000000000..937fc75de6 --- /dev/null +++ b/tests/js/set-tests.js @@ -0,0 +1,481 @@ +//////////////////////////////////////////////////////////////////////////// +// +// Copyright 2021 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'; + +// Prevent React Native packager from seeing modules required with this +const require_method = require; +function nodeRequire(module) { + return require_method(module); +} + +const Realm = require('realm'); +const TestCase = require('./asserts'); + +const isNodeProcess = typeof process === 'object' && process + '' === '[object process]'; +const isElectronProcess = typeof process === 'object' && process.versions && process.versions.electron; +const fs = isNodeProcess ? nodeRequire('fs-extra') : require('react-native-fs'); + +module.exports = { + async testSetSchema() { + // test that short (JS) and canonical schema types yield + // the same results + const shorthandSchema = { + name: "ShorthandSchema", + properties: { + names: "string<>" + } + }; + + const canonicalSchema = { + name: "CanonicalSchema", + properties: { + names: {type: 'set', objectType: 'string'} + + } + }; + + const shorthandRealm = new Realm({ schema: [shorthandSchema] }); + const shSchema = shorthandRealm.schema; + shorthandRealm.close(); + + const canonicalRealm = new Realm({ schema: [canonicalSchema] }); + const canSchema = canonicalRealm.schema; + canonicalRealm.close(); + + TestCase.assertEqual(shSchema.properties, canSchema.properties, + "Canonical and shorthand schemas should have idendical properties"); + }, + + // + // test manipulation of Sets of strings + async testSetStringAddDelete() { + // test add/delete operations on Sets of type string + const teamSchema = { + name: "Team", + properties: { + names: "string<>" + } + }; + + const realm = new Realm({ schema: [teamSchema] }); + const schema = realm.schema; + + realm.write(() => { + // insert two people + realm.create(teamSchema.name, { + names: ["John", "Sue"] + }); + }); + + let teams = realm.objects(teamSchema.name); + TestCase.assertEqual(teams.length, 1, "There should be one team") + TestCase.assertEqual(teams[0].names.size, 2, "Team Set size should be 2"); + + // add another three names + realm.write(() => { + teams[0].names.add("Pernille").add("Magrethe").add("Wilbur"); + }); + teams = realm.objects(teamSchema.name); + TestCase.assertEqual(teams.length, 1, "There should be one team. We didn't add any") + TestCase.assertEqual(teams[0].names.size, 5, "Team Set size should be 5"); + + // remove two names + realm.write(() => { + teams[0].names.delete("John"); + teams[0].names.delete("Sue"); + }); + teams = realm.objects(teamSchema.name); + TestCase.assertEqual(teams.length, 1, "There should still be only one team"); + TestCase.assertEqual(teams[0].names.size, 3, "Team Set size should be 3"); + + realm.close(); + }, + + + // + // test manipulation of Sets of objects + async testSetObjectAddDelete() { + const personSchema = { + name: "Person", + properties: { + firstName: 'string', + age: 'int' + } + }; + + const teamSchema = { + name: 'Team', + properties: { + persons: 'Person<>' + } + }; + + const realm = new Realm({ schema: [personSchema, teamSchema] }); + const schema = realm.schema; + + realm.write(() => { + // insert two people + realm.create(teamSchema.name, {persons: [ + {firstName: "Joe", age: 4}, + {firstName: "Sue", age: 53}, + ]}); + }); + + let teams = realm.objects(teamSchema.name); + TestCase.assertEqual(teams.length, 1, "There should be one team"); + TestCase.assertEqual(teams[0].persons.size, 2, "Team Set size should be 2"); + + // add another person + realm.write(() => { + teams[0].persons.add({firstName: 'Bob', age: 99}); + }); + teams = realm.objects(teamSchema.name); + TestCase.assertEqual(teams.length, 1, "There should still be only one team"); + TestCase.assertEqual(teams[0].persons.size, 3, "Team Set size should be 3"); + + }, + + + // + // test filtering on object properties + async testSetObjectFilter() { + const personSchema = { + name: "Person", + properties: { + firstName: 'string', + age: 'int' + } + }; + + const teamSchema = { + name: 'Team', + properties: { + persons: 'Person<>' + } + }; + + const realm = new Realm({ schema: [personSchema, teamSchema] }); + const schema = realm.schema; + + realm.write(() => { + // insert two people + realm.create(teamSchema.name, {persons: [ + {firstName: "Joe", age: 4}, + {firstName: "Sue", age: 53}, + {firstName: 'Bob', age: 99}, + ]}); + }); + + + let teams = realm.objects(teamSchema.name); + let filteredSues = teams[0].persons.filtered('firstName = "Sue"'); + TestCase.assertEqual(filteredSues.length, 1, "There should be only one Sue"); + TestCase.assertEqual(filteredSues[0].age, 53, "Sue's age should be 53"); + + // add another Sue + teams = realm.objects(teamSchema.name); + realm.write(() => { + teams[0].persons.add({firstName: "Sue", age: 35}); + }); + filteredSues = teams[0].persons.filtered('firstName = "Sue"'); + TestCase.assertEqual(filteredSues.length, 2, "There should be two Sues"); + + // find people older than 50 + let olderPersons = teams[0].persons.filtered('age > 50'); + TestCase.assertEqual(olderPersons.length, 2, "There should be two people over 50"); + + + // cross-contamination test: create another team that also cointains a Sue + realm.write(() => { + realm.create(teamSchema.name, {persons: [ + {firstName: "Sue", age: 35}, + ]}); + }); + + teams = realm.objects(teamSchema.name); + TestCase.assertEqual(teams.length, 2, "There should be two teams"); + + // TODO: The tests below are waiting for LinkedObj support in Mixed + +// let one = people[0].Persons.get(0); +// let oij = people[0].Persons.has({FirstName: "Sue", Age: 53}); + +// realm.write(() => { +// people = realm.objects(people_schema.name); +// people[0].Persons.delete(one); +// // people[0].Persons.delete({FirstName: "Sue", Age: 53}); +// }); +// people = realm.objects(people_schema.name); + +// TestCase.assertEqual(1, people.length, "There should be one 'People' entry") +// TestCase.assertEqual(2, people[0].Persons.length, "Persons Set size should be 2"); + + // TODO: add another 'People' + }, + + + // + // test functions that are provided by Set as part of the MDN reference + async testSetUtilityFunctions() { + const peopleSchema = { + name: "Person", + properties: { + names: "string<>" + } + }; + + const realm = new Realm({ schema: [peopleSchema] }); +// const schema = realm.schema; + + // + // Set.has() functionality + // + + realm.write(() => { + // put some names in our database + realm.create(peopleSchema.name, { + names: ["Alice", "Bob", "Cecilia"] + }); + }); + + let teams = realm.objects(peopleSchema.name); + TestCase.assertEqual(teams.length, 1, "There should be only one team"); + let footballTeam = teams[0]; + TestCase.assertEqual(footballTeam.names.size, 3, "There should be 3 people in the football team"); + TestCase.assertEqual(true, footballTeam.names.has("Alice"), "Alice should be in the football team"); + TestCase.assertEqual(false, footballTeam.names.has("Daniel"), "Daniel shouldn't be in the football team"); + + // add one football team member, delete another one + realm.write(() => { + footballTeam.names.add("Daniel").delete("Alice"); + }); + teams = realm.objects(peopleSchema.name); + TestCase.assertEqual(teams.length, 1, "There should be only one football team"); + footballTeam = teams[0]; + TestCase.assertEqual(footballTeam.names.size, 3, "There should be 3 people in the football team after adding Daniel and removing Alice"); + TestCase.assertEqual(false, footballTeam.names.has("Alice"), "Alice shouldn't be in the football team"); + + // create another team with two people + realm.write(() => { + realm.create(peopleSchema.name, { + names: ["Daniel", "Felicia"] + }); + }); + teams = realm.objects(peopleSchema.name); + TestCase.assertEqual(teams.length, 2, "There should be two teams"); + footballTeam = teams[0]; + let handballTeam = teams[1]; + TestCase.assertEqual(footballTeam.names.size, 3, "There should be 3 people in the football team. It wasn't altered"); + TestCase.assertEqual(handballTeam.names.size, 2, "There should be 2 people in the handball team"); + + TestCase.assertFalse(handballTeam.names.has("Bob"), "Bob shouldn't be in the handball team"); + TestCase.assertTrue(handballTeam.names.has("Daniel"), "Daniel should be in the handball team"); + + // + // Set.clear() functionality + // + // remove everyone from the football team + realm.write(() => { + footballTeam.names.clear(); + }); + + teams = realm.objects(peopleSchema.name); + TestCase.assertEqual(teams.length, 2, "There should be two teams. Teams weren't cleared."); + footballTeam = teams[0]; + handballTeam = teams[1]; + TestCase.assertEqual(footballTeam.names.size, 0, "There should be no people in the football team. It was cleared"); + TestCase.assertEqual(handballTeam.names.size, 2, "There should be 2 people in the handball team. It was cleared"); + + realm.close(); + }, + + + async testSetAggregates() { + if (!isNodeProcess) { + // aggregate functions only work on node + return; + } + + const intSchema = { + name: "SetInt", + properties: { + intSet: "int<>", + } + }; + + const realm = new Realm({ schema: [intSchema] }); + + realm.write(() => { + realm.create(intSchema.name, { + intSet: [7, 9, 14], + }); + }); + + let myInts = realm.objects(intSchema.name)[0]; + + TestCase.assertEqual(myInts.intSet.sum(), 30, "Sum of intSet should be 30"); + TestCase.assertEqual(myInts.intSet.avg(), 10, "Avg of intSet should be 10"); + TestCase.assertEqual(myInts.intSet.min(), 7, "Min of intSet should be 7"); + TestCase.assertEqual(myInts.intSet.max(), 14, "Max of intSet should be 14"); + + // make sure that aggregation works after adding to a Set + realm.write(() => { + myInts.intSet.add(4).add(6); + }); + + TestCase.assertEqual(myInts.intSet.sum(), 40, "Sum of intSet should be 40 after adding elements"); + TestCase.assertEqual(myInts.intSet.avg(), 8, "Avg of intSet should be 8 after adding elements"); + TestCase.assertEqual(myInts.intSet.min(), 4, "Min of intSet should be 4 after adding elements"); + TestCase.assertEqual(myInts.intSet.max(), 14, "Max of intSet should be 14 after adding elements"); + + // make sure that aggregation works after deleting from a Set + realm.write(() => { + myInts.intSet.delete(4); + }); + + TestCase.assertEqual(myInts.intSet.sum(), 36, "Sum of intSet should be 33 after deleting elements"); + TestCase.assertEqual(myInts.intSet.avg(), 9, "Avg of intSet should be 9 after deleting elements"); + TestCase.assertEqual(myInts.intSet.min(), 6, "Min of intSet should be 6 after deleting elements"); + TestCase.assertEqual(myInts.intSet.max(), 14, "Max of intSet should be 14 after deleting elements"); + }, + + + // Test that iteration (`forEach`, `entries()`, `values()`) work as intended + async testSetIteration() { + const intSchema = { + name: "SetInt", + properties: { + intSet: "int<>", + } + }; + + const realm = new Realm({ schema: [intSchema] }); + + const myInts = [1, 2, 3, 7, 9, 13]; + + realm.write(() => { + realm.create(intSchema.name, { + intSet: myInts, + }); + }); + + let dbInts = realm.objects(intSchema.name)[0].intSet; + let intArray2 = Array.from(myInts); + let intCount = 0; + dbInts.forEach((element) => { + let foundIndex = intArray2.findIndex((value) => value == element); + TestCase.assertNotEqual(foundIndex, -1, element + " should have been present in dbInts"); + intArray2.splice(foundIndex, 1); + + intCount++; + }); + TestCase.assertEqual(intCount, dbInts.size, "`forEach` loop should execute on each set element") + + const intsValues = Array.from(dbInts.values()); + const sameInts = intsValues.map((value, index) => { + return value == myInts[index]; + }); + const isSameInts = sameInts.reduce((prev, curr) => { + return prev && curr; + }); + TestCase.assertTrue(isSameInts, "dbInts.values() should contain the same elements as myInts"); + + const intsEntries = Array.from(dbInts.entries()); + const sameEntries = intsEntries.map((value, index) => { + return value[0] == myInts[index] && value[1] == myInts[index]; + }); + const isCorrect = sameEntries.reduce((prev, curr) => { + return prev && curr; + }); + TestCase.assertTrue(isCorrect, "dbInts.entries() should contain the elements of type [myInts[x], myInts[x]]"); + + realm.close(); + }, + + // test that Set's .toJSON works as intended + async testSetSerialization() { + const intSchema = { + name: "SetInt", + properties: { + intSet: "int<>", + } + }; + + const myInts = [1, 2, 3, 7, 9, 13]; + + // test serialization of simple types + const intRealm = new Realm({ schema: [intSchema] }); + intRealm.write(() => { + intRealm.create(intSchema.name, { + intSet: myInts, + }); + }); + + let dbInts = intRealm.objects(intSchema.name)[0].intSet; + let dbString = JSON.stringify(dbInts); + let jsString = JSON.stringify(myInts); + TestCase.assertEqual(dbString, jsString, "JSON serialization of dbInts should be the same as jsInts"); + + intRealm.close(); + + // test serialization of objects + const itemSchema = { + name: "Item", + properties: { + title: 'string', + priority: 'int' + } + }; + const listSchema = { + name: "ItemList", + properties: { + itemSet: "Item<>", + } + }; + + let myItems = [ + { + title: 'Item 1', + priority: 1 + }, + { + title: 'Item 2', + priority: 8 + }, + { + title: 'Item 3', + priority: -4 + }, + ]; + + const itemRealm = new Realm({ schema: [itemSchema, listSchema] }); + itemRealm.write(() => { + itemRealm.create(listSchema.name, { + itemSet: myItems, + }); + }); + + let dbItems = itemRealm.objects(listSchema.name)[0].itemSet; + let dbItemString = JSON.stringify(dbItems); + let jsItemString = JSON.stringify(myItems); + TestCase.assertEqual(dbItemString, jsItemString, "Object serialization from Set and JS object should be the same") + + itemRealm.close(); + }, +} diff --git a/tests/mongodb/common-tests/config.json b/tests/mongodb/common-tests/config.json index cc6c9058f5..299ce68204 100644 --- a/tests/mongodb/common-tests/config.json +++ b/tests/mongodb/common-tests/config.json @@ -1,5 +1,5 @@ { - "app_id": "auth-integration-tests-pntad", + "app_id": "auth-integration-tests-fevvt", "config_version": 20200603, "name": "auth-integration-tests", "location": "US-VA", diff --git a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.Dog.json b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.Dog.json index 5247cc663e..9128fa6012 100644 --- a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.Dog.json +++ b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.Dog.json @@ -1,7 +1,7 @@ { "collection": "Dog", "database": "test_data", - "id": "606da3f7b1969d14a3b5fe02", + "id": "6079296ee2cbbda01842813b", "roles": [ { "name": "default", diff --git a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.MixedObject.json b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.MixedObject.json index 215f1f47d1..9c7d251420 100644 --- a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.MixedObject.json +++ b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.MixedObject.json @@ -1,7 +1,7 @@ { "collection": "MixedObject", "database": "test_data", - "id": "606da3f7b1969d14a3b5fe03", + "id": "6079296ee2cbbda01842813c", "roles": [ { "name": "default", diff --git a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.Person.json b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.Person.json index 8860cdfcdb..f8fd626e5b 100644 --- a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.Person.json +++ b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.Person.json @@ -1,7 +1,7 @@ { "collection": "Person", "database": "test_data", - "id": "606da3f7b1969d14a3b5fe04", + "id": "6079296ee2cbbda01842813d", "relationships": { "dogs": { "ref": "#/relationship/BackingDB/test_data/Dog", diff --git a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.SyncedNumbers.json b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.SyncedNumbers.json new file mode 100644 index 0000000000..81dc24636e --- /dev/null +++ b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.SyncedNumbers.json @@ -0,0 +1,36 @@ +{ + "collection": "SyncedNumbers", + "database": "test_data", + "id": "6079296ee2cbbda01842813e", + "roles": [ + { + "name": "default", + "apply_when": {}, + "write": true, + "insert": true, + "delete": true, + "additional_fields": {} + } + ], + "schema": { + "properties": { + "_id": { + "bsonType": "int" + }, + "numbers": { + "bsonType": "array", + "items": { + "bsonType": "int" + }, + "uniqueItems": true + }, + "realm_id": { + "bsonType": "string" + } + }, + "required": [ + "_id" + ], + "title": "SyncedNumbers" + } +} diff --git a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.UUIDObject.json b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.UUIDObject.json index ecabab3d4a..2e6616514f 100644 --- a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.UUIDObject.json +++ b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.UUIDObject.json @@ -1,7 +1,7 @@ { "collection": "UUIDObject", "database": "test_data", - "id": "606da3f7b1969d14a3b5fe05", + "id": "6079296ee2cbbda01842813f", "roles": [ { "name": "default", diff --git a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.testRemoteMongoClient.json b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.testRemoteMongoClient.json index 69fdaeb5d9..b2eb7dc42d 100644 --- a/tests/mongodb/common-tests/services/BackingDB/rules/test_data.testRemoteMongoClient.json +++ b/tests/mongodb/common-tests/services/BackingDB/rules/test_data.testRemoteMongoClient.json @@ -1,7 +1,7 @@ { "collection": "testRemoteMongoClient", "database": "test_data", - "id": "606da3f7b1969d14a3b5fe06", + "id": "6079296ee2cbbda018428140", "roles": [ { "name": "default", diff --git a/tests/mongodb/services/BackingDB/rules/test_data.SyncedSetInt.json b/tests/mongodb/services/BackingDB/rules/test_data.SyncedSetInt.json new file mode 100644 index 0000000000..b20b593a4e --- /dev/null +++ b/tests/mongodb/services/BackingDB/rules/test_data.SyncedSetInt.json @@ -0,0 +1,36 @@ +{ + "collection": "SyncedSetInt", + "database": "test_data", + "id": "6065b99d5c8e51ddce0366a3", + "roles": [ + { + "name": "default", + "apply_when": {}, + "write": true, + "insert": true, + "delete": true, + "additional_fields": {} + } + ], + "schema": { + "properties": { + "_id": { + "bsonType": "int" + }, + "intSet": { + "bsonType": "array", + "items": { + "bsonType": "int" + }, + "uniqueItems": true + }, + "realm_id": { + "bsonType": "string" + } + }, + "required": [ + "_id" + ], + "title": "SyncedSetInt" + } +} diff --git a/tests/package-lock.json b/tests/package-lock.json index 436dd8bd02..7738b7e7f8 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -3352,6 +3352,7 @@ }, "compare-versions": { "version": "3.6.0", + "resolved": false, "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==" }, "component-emitter": { diff --git a/types/index.d.ts b/types/index.d.ts index bf3498caf5..8a9ed781fe 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -299,7 +299,7 @@ declare namespace Realm { * List * @see { @link https://realm.io/docs/javascript/latest/api/Realm.List.html } */ - interface List extends Collection { + interface List extends Collection { [n: number]: T; pop(): T | null | undefined; @@ -330,6 +330,45 @@ declare namespace Realm { readonly prototype: List; }; + + /** + * Set + * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Set.html } + */ + interface Set extends Collection { + /** + * Delete a value from the Set + * @param {T} object Value to delete from the Set + * @returns Boolean: true if the value existed in the Set prior to deletion, false otherwise + */ + delete(object: T): boolean; + + /** + * Add a new value to the Set + * @param {T} object Value to add to the Set + * @returns The Realm.Set itself, after adding the new value + */ + add(object: T): Realm.Set; + + /** + * Clear all values from the Set + */ + clear(): void; + + /** + * Check for existence of a value in the Set + * @param {T} object Value to search for in the Set + * @returns Boolean: true if the value exists in the Set, false otherwise + */ + has(object: T): boolean; + + readonly size: number + } + + const Set: { + readonly prototype: Set; + }; + /** * Results * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Results.html }