From 643c19ea75b45b8e1fbe0b5247aa08e9088db8d1 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Wed, 11 Oct 2017 05:54:52 -0700 Subject: [PATCH] Add support for aggregates on arrays of primitives (#1401) * Fix an unused variable warning in js_sync.hpp * Add support for aggregates on arrays of primitives * Update documentation and typescript declarations * Update collection.js --- CHANGELOG.md | 1 + docs/collection.js | 44 +++++++--- lib/index.d.ts | 31 +------ src/js_list.hpp | 34 +------- src/js_results.hpp | 34 +------- src/js_sync.hpp | 16 ++-- src/js_types.hpp | 4 +- src/js_util.hpp | 58 ++++++------- src/jsc/jsc_return_value.hpp | 14 ++++ src/node/node_return_value.hpp | 13 +++ tests/js/list-tests.js | 147 +++++++++++++++++++-------------- 11 files changed, 194 insertions(+), 202 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f300600..0707f2df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ X.Y.Z Release notes ### Enhancements * Added `update` method to `Realm.Results` to support bulk updates (#808). +* Added support for aggregate functions on `Realm.Results` and `Realm.List` of primitive types. ### Bug fixes * None diff --git a/docs/collection.js b/docs/collection.js index ca2e6590..a3f524b3 100644 --- a/docs/collection.js +++ b/docs/collection.js @@ -239,8 +239,14 @@ class Collection { indexOf(object) {} /** - * Computes the minimum value of a property. - * @param {string} property - The name of the property. + * Returns the minimum value of the values in the collection or of the + * given property among all the objects in the collection, or `undefined` + * if the collection is empty. + * + * Only supported for int, float, double and date properties. `null` values + * are ignored entirely by this method and will not be returned. + * + * @param {string} [property] - For a collection of objects, the property to take the minimum of. * @throws {Error} If no property with the name exists or if property is not numeric/date. * @returns {number} the minimum value. * @since 1.12.1 @@ -248,8 +254,14 @@ class Collection { min(property) {} /** - * Computes the maximum value of a property. - * @param {string} property - The name of the property. + * Returns the maximum value of the values in the collection or of the + * given property among all the objects in the collection, or `undefined` + * if the collection is empty. + * + * Only supported for int, float, double and date properties. `null` values + * are ignored entirely by this method and will not be returned. + * + * @param {string} [property] - For a collection of objects, the property to take the maximum of. * @throws {Error} If no property with the name exists or if property is not numeric/date. * @returns {number} the maximum value. * @since 1.12.1 @@ -257,19 +269,29 @@ class Collection { max(property) {} /** - * Computes the sum of a property. - * @param {string} property - The name of the property. - * @throws {Error} If no property with the name exists or if property is not numeric/date. + * Computes the sum of the values in the collection or of the given + * property among all the objects in the collection, or 0 if the collection + * is empty. + * + * Only supported for int, float and double properties. `null` values are + * ignored entirely by this method. + * @param {string} [property] - For a collection of objects, the property to take the sum of. + * @throws {Error} If no property with the name exists or if property is not numeric. * @returns {number} the sum. * @since 1.12.1 */ sum(property) {} /** - * Computes the average of a property. - * @param {string} property - The name of the property. - * @throws {Error} If no property with the name exists or if property is not numeric/date. - * @returns {number} the average value. + * Computes the average of the values in the collection or of the given + * property among all the objects in the collection, or `undefined` if the collection + * is empty. + * + * Only supported for int, float and double properties. `null` values are + * ignored entirely by this method and will not be factored into the average. + * @param {string} [property] - For a collection of objects, the property to take the average of. + * @throws {Error} If no property with the name exists or if property is not numeric. + * @returns {number} the sum. * @since 1.12.1 */ avg(property) {} diff --git a/lib/index.d.ts b/lib/index.d.ts index 40177b48..18a07bc8 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -144,33 +144,10 @@ declare namespace Realm { */ isValid(): boolean; - /** - * Computes the minimum value. - * @param {string} property - * @returns number - */ - min(property: string): number; - - /** - * Computes the maximum value. - * @param {string} property - * @returns number - */ - max(property: string): number; - - /** - * Computes the sum. - * @param {string} property - * @returns number - */ - sum(property: string): number; - - /** - * Computes the average. - * @param {string} property - * @returns number - */ - avg(property: string): number; + min(property?: string): number|Date|null; + max(property?: string): number|Date|null; + sum(property?: string): number|null; + avg(property?: string): number; /** * @param {string} query diff --git a/src/js_list.hpp b/src/js_list.hpp index b96b103a..7f65ee44 100644 --- a/src/js_list.hpp +++ b/src/js_list.hpp @@ -78,12 +78,6 @@ struct ListClass : ClassDefinition, CollectionClass> { static void is_valid(ContextType, ObjectType, Arguments, ReturnValue &); static void index_of(ContextType, ObjectType, Arguments, 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 &); - static void sum(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void avg(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - // observable static void add_listener(ContextType, ObjectType, Arguments, ReturnValue &); static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &); @@ -102,10 +96,10 @@ struct ListClass : ClassDefinition, CollectionClass> { {"sorted", wrap}, {"isValid", wrap}, {"indexOf", wrap}, - {"min", wrap}, - {"max", wrap}, - {"sum", wrap}, - {"avg", wrap}, + {"min", wrap, AggregateFunc::Min>>}, + {"max", wrap, AggregateFunc::Max>>}, + {"sum", wrap, AggregateFunc::Sum>>}, + {"avg", wrap, AggregateFunc::Avg>>}, {"addListener", wrap}, {"removeListener", wrap}, {"removeAllListeners", wrap}, @@ -134,26 +128,6 @@ void ListClass::get_length(ContextType, ObjectType object, ReturnValue &retur return_value.set((uint32_t)list->size()); } -template -void ListClass::min(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - compute_aggregate_on_collection>(AggregateFunc::Min, ctx, this_object, argc, arguments, return_value); -} - -template -void ListClass::max(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - compute_aggregate_on_collection>(AggregateFunc::Max, ctx, this_object, argc, arguments, return_value); -} - -template -void ListClass::sum(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - compute_aggregate_on_collection>(AggregateFunc::Sum, ctx, this_object, argc, arguments, return_value); -} - -template -void ListClass::avg(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - compute_aggregate_on_collection>(AggregateFunc::Avg, ctx, this_object, argc, arguments, return_value); -} - template void ListClass::get_type(ContextType ctx, ObjectType object, ReturnValue &return_value) { auto list = get_internal>(object); diff --git a/src/js_results.hpp b/src/js_results.hpp index 876512e1..74daccd2 100644 --- a/src/js_results.hpp +++ b/src/js_results.hpp @@ -89,12 +89,6 @@ struct ResultsClass : ClassDefinition, CollectionClass< 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 &); - static void sum(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - static void avg(ContextType, FunctionType, ObjectType, size_t, const ValueType[], ReturnValue &); - // observable static void add_listener(ContextType, ObjectType, Arguments, ReturnValue &); static void remove_listener(ContextType, ObjectType, Arguments, ReturnValue &); @@ -112,10 +106,10 @@ struct ResultsClass : ClassDefinition, CollectionClass< {"filtered", wrap}, {"sorted", wrap}, {"isValid", wrap}, - {"min", wrap}, - {"max", wrap}, - {"sum", wrap}, - {"avg", wrap}, + {"min", wrap, AggregateFunc::Min>>}, + {"max", wrap, AggregateFunc::Max>>}, + {"sum", wrap, AggregateFunc::Sum>>}, + {"avg", wrap, AggregateFunc::Avg>>}, {"addListener", wrap}, {"removeListener", wrap}, {"removeAllListeners", wrap}, @@ -214,26 +208,6 @@ void ResultsClass::get_length(ContextType ctx, ObjectType object, ReturnValue return_value.set((uint32_t)results->size()); } -template -void ResultsClass::min(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - compute_aggregate_on_collection>(AggregateFunc::Min, ctx, this_object, argc, arguments, return_value); -} - -template -void ResultsClass::max(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - compute_aggregate_on_collection>(AggregateFunc::Max, ctx, this_object, argc, arguments, return_value); -} - -template -void ResultsClass::sum(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - compute_aggregate_on_collection>(AggregateFunc::Sum, ctx, this_object, argc, arguments, return_value); -} - -template -void ResultsClass::avg(ContextType ctx, FunctionType, ObjectType this_object, size_t argc, const ValueType arguments[], ReturnValue &return_value) { - compute_aggregate_on_collection>(AggregateFunc::Avg, ctx, this_object, argc, arguments, return_value); -} - template void ResultsClass::get_type(ContextType, ObjectType object, ReturnValue &return_value) { auto results = get_internal>(object); diff --git a/src/js_sync.hpp b/src/js_sync.hpp index d22afd4e..8e0310fd 100644 --- a/src/js_sync.hpp +++ b/src/js_sync.hpp @@ -18,10 +18,6 @@ #pragma once -#include -#include -#include -#include #include #include @@ -40,11 +36,11 @@ namespace realm { namespace js { inline realm::SyncManager& syncManagerShared() { - static bool configured = []{ + static std::once_flag flag; + std::call_once(flag, [] { ensure_directory_exists_for_file(default_realm_file_directory()); SyncManager::shared().configure_file_system(default_realm_file_directory(), SyncManager::MetadataMode::NoEncryption); - return true; - }(); + }); return SyncManager::shared(); } @@ -627,8 +623,10 @@ void SyncClass::populate_sync_config(ContextType ctx, ObjectType realm_constr std::string raw_realm_url = Object::validated_get_string(ctx, sync_config_object, "url"); if (shared_user->token_type() == SyncUser::TokenType::Admin) { - static std::regex tilde("/~/"); - raw_realm_url = std::regex_replace(raw_realm_url, tilde, "/__auth/"); + size_t pos = raw_realm_url.find("/~/"); + if (pos != std::string::npos) { + raw_realm_url.replace(pos + 1, 1, "__auth"); + } } bool client_validate_ssl = true; diff --git a/src/js_types.hpp b/src/js_types.hpp index 09b5e05a..64f78096 100644 --- a/src/js_types.hpp +++ b/src/js_types.hpp @@ -139,7 +139,7 @@ struct Value { static ValueType from_nonnull_binary(ContextType, BinaryData); static ValueType from_undefined(ContextType); static ValueType from_timestamp(ContextType, Timestamp); - static ValueType from_mixed(ContextType, util::Optional &); + static ValueType from_mixed(ContextType, const util::Optional &); static ObjectType to_array(ContextType, const ValueType &); static bool to_boolean(ContextType, const ValueType &); @@ -446,7 +446,7 @@ inline typename T::Value Value::from_timestamp(typename T::Context ctx, Times } template -inline typename T::Value Value::from_mixed(typename T::Context ctx, util::Optional& mixed) { +inline typename T::Value Value::from_mixed(typename T::Context ctx, const util::Optional& mixed) { if (!mixed) { return from_undefined(ctx); } diff --git a/src/js_util.hpp b/src/js_util.hpp index cf803798..59faf4aa 100644 --- a/src/js_util.hpp +++ b/src/js_util.hpp @@ -83,50 +83,42 @@ static inline void validate_argument_count_at_least(size_t count, size_t expecte } } -template -static inline void compute_aggregate_on_collection(AggregateFunc func, typename T::ContextType ctx, typename T::ObjectType this_object, size_t argc, const typename T::ValueType arguments[], typename T::ReturnValue &return_value) { - validate_argument_count(argc, 1); - std::string property_name = T::Value::validated_to_string(ctx, arguments[0]); +template +void compute_aggregate_on_collection(typename T::ContextType ctx, typename T::ObjectType this_object, + typename T::Arguments args, typename T::ReturnValue &return_value) { auto list = get_internal(this_object); - const ObjectSchema& object_schema = list->get_object_schema(); - const Property* property = object_schema.property_for_name(property_name); - if (!property) { - throw std::invalid_argument(util::format("No such property: %1", property_name)); + size_t column = 0; + if (list->get_type() == realm::PropertyType::Object) { + const ObjectSchema& object_schema = list->get_object_schema(); + std::string property_name = T::Value::validated_to_string(ctx, args[0]); + const Property* property = object_schema.property_for_name(property_name); + if (!property) { + throw std::invalid_argument(util::format("Property '%1' does not exist on object '%2'", + property_name, object_schema.name)); + } + column = property->table_column; + } + else { + args.validate_maximum(0); } util::Optional mixed; switch (func) { - case AggregateFunc::Min: { - mixed = list->min(property->table_column); + case AggregateFunc::Min: + return_value.set(list->min(column)); break; - } - case AggregateFunc::Max: { - mixed = list->max(property->table_column); + case AggregateFunc::Max: + return_value.set(list->max(column)); break; - } - case AggregateFunc::Sum: { - mixed = list->sum(property->table_column); + case AggregateFunc::Sum: + return_value.set(list->sum(column)); + break; + case AggregateFunc::Avg: + return_value.set(list->average(column)); break; - } - case AggregateFunc::Avg: { - util::Optional avg = list->average(property->table_column); - if (!avg) { - return_value.set_undefined(); - } - else { - return_value.set(*avg); - } - return; - } - default: { - REALM_ASSERT(false && "Unknown aggregate function"); - REALM_UNREACHABLE(); - } } - - return_value.set(T::Value::from_mixed(ctx, mixed)); } } // js diff --git a/src/jsc/jsc_return_value.hpp b/src/jsc/jsc_return_value.hpp index ec69cefc..03150e4b 100644 --- a/src/jsc/jsc_return_value.hpp +++ b/src/jsc/jsc_return_value.hpp @@ -53,12 +53,26 @@ class ReturnValue { void set(uint32_t number) { m_value = JSValueMakeNumber(m_context, number); } + void set(const util::Optional& mixed) { + m_value = Value::from_mixed(m_context, mixed); + } void set_null() { m_value = JSValueMakeNull(m_context); } void set_undefined() { m_value = JSValueMakeUndefined(m_context); } + + template + void set(const util::Optional& value) { + if (value) { + set(*value); + } + else { + set_undefined(); + } + } + operator JSValueRef() const { return m_value; } diff --git a/src/node/node_return_value.hpp b/src/node/node_return_value.hpp index e47ba700..dfb3a1d7 100644 --- a/src/node/node_return_value.hpp +++ b/src/node/node_return_value.hpp @@ -64,12 +64,25 @@ class ReturnValue { void set(uint32_t number) { m_value.Set(number); } + void set(realm::Mixed mixed) { + m_value.Set(Value::from_mixed(nullptr, mixed)); + } void set_null() { m_value.SetNull(); } void set_undefined() { m_value.SetUndefined(); } + + template + void set(util::Optional value) { + if (value) { + set(*value); + } + else { + m_value.SetUndefined(); + } + } }; } // js diff --git a/tests/js/list-tests.js b/tests/js/list-tests.js index b05fc302..145bc9ed 100644 --- a/tests/js/list-tests.js +++ b/tests/js/list-tests.js @@ -1009,17 +1009,14 @@ module.exports = { const NullableBasicTypesList = { name: 'NullableBasicTypesList', properties: { - list: {type: 'list', objectType: 'NullableBasicTypesObject'}, + list: 'NullableBasicTypesObject[]', } }; - var realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]}); - var object; - + const realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]}); const N = 50; - - var list = []; - for(var i = 0; i < N; i++) { + const list = []; + for (let i = 0; i < N; i++) { list.push({ intCol: i+1, floatCol: i+1, @@ -1028,6 +1025,7 @@ module.exports = { }); } + let object; realm.write(() => { object = realm.create('NullableBasicTypesList', {list: list}); }); @@ -1051,19 +1049,17 @@ module.exports = { const NullableBasicTypesList = { name: 'NullableBasicTypesList', properties: { - list: {type: 'list', objectType: 'NullableBasicTypesObject'}, + list: 'NullableBasicTypesObject[]', } }; - var realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]}); - var object; - var objectEmptyList; + const realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]}); const N = 50; const M = 10; - var list = []; - for(var i = 0; i < N; i++) { + const list = []; + for (let i = 0; i < N; i++) { list.push({ intCol: i+1, floatCol: i+1, @@ -1072,15 +1068,11 @@ module.exports = { }); } - for(var j = 0; j < M; j++) { - list.push({ - intCol: null, - floatCol: null, - doubleCol: null, - dateCol: null - }); + for (let j = 0; j < M; j++) { + list.push({}); } + let object, objectEmptyList; realm.write(() => { object = realm.create('NullableBasicTypesList', {list: list}); objectEmptyList = realm.create('NullableBasicTypesList', {list: []}); @@ -1088,7 +1080,6 @@ module.exports = { TestCase.assertEqual(object.list.length, N + M); - // int, float & double columns support all aggregate functions // the M null valued objects should be ignored ['intCol', 'floatCol', 'doubleCol'].forEach(colName => { @@ -1115,6 +1106,44 @@ module.exports = { TestCase.assertUndefined(objectEmptyList.list.max('dateCol')); }, + testPrimitiveListAggregateFunctions: function() { + const realm = new Realm({schema: [schemas.PrimitiveArrays]}); + let object; + realm.write(() => { + object = realm.create('PrimitiveArrays', { + int: [1, 2, 3], + float: [1.1, 2.2, 3.3], + double: [1.11, 2.22, 3.33], + date: [DATE1, DATE2, DATE3], + + optInt: [1, null, 2], + optFloat: [1.1, null, 3.3], + optDouble: [1.11, null, 3.33], + optDate: [DATE1, null, DATE3] + }); + }); + + for (let prop of ['int', 'float', 'double', 'date', 'optInt', 'optFloat', 'optDouble', 'optDate']) { + const list = object[prop]; + TestCase.assertSimilar(list.type, list.min(), list[0]); + TestCase.assertSimilar(list.type, list.max(), list[2]); + + if (list.type === 'date') { + TestCase.assertThrowsContaining(() => list.sum(), "Cannot sum 'date' array: operation not supported") + TestCase.assertThrowsContaining(() => list.avg(), "Cannot average 'date' array: operation not supported") + continue; + } + + const sum = list[0] + list[1] + list[2]; + const avg = sum / (list[1] === null ? 2 : 3); + TestCase.assertSimilar(list.type, list.sum(), sum); + TestCase.assertSimilar(list.type, list.avg(), avg); + } + + TestCase.assertThrowsContaining(() => object.bool.min(), "Cannot min 'bool' array: operation not supported") + TestCase.assertThrowsContaining(() => object.int.min("foo"), "Invalid arguments: at most 0 expected, but 1 supplied") + }, + testListAggregateFunctionsUnsupported: function() { const NullableBasicTypesList = { name: 'NullableBasicTypesList', @@ -1123,13 +1152,12 @@ module.exports = { } }; - var realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]}); - var object; + const realm = new Realm({schema: [schemas.NullableBasicTypes, NullableBasicTypesList]}); const N = 5; var list = []; - for(var i = 0; i < N; i++) { + for (let i = 0; i < N; i++) { list.push({ intCol: i+1, floatCol: i+1, @@ -1138,6 +1166,7 @@ module.exports = { }); } + let object; realm.write(() => { object = realm.create('NullableBasicTypesList', {list: list}); }); @@ -1145,38 +1174,33 @@ module.exports = { TestCase.assertEqual(object.list.length, N); // bool, string & data columns don't support 'min' - ['boolCol', 'stringCol', 'dataCol'].forEach(colName => { - TestCase.assertThrows(function() { - object.list.min(colName); - } - )}); + ['bool', 'string', 'data'].forEach(colName => { + TestCase.assertThrowsContaining(() => object.list.min(colName + 'Col'), + `Cannot min property '${colName}Col': operation not supported for '${colName}' properties`); + }); // bool, string & data columns don't support 'max' - ['boolCol', 'stringCol', 'dataCol'].forEach(colName => { - TestCase.assertThrows(function() { - object.list.max(colName); - } - )}); + ['bool', 'string', 'data'].forEach(colName => { + TestCase.assertThrowsContaining(() => object.list.max(colName + 'Col'), + `Cannot max property '${colName}Col': operation not supported for '${colName}' properties`); + }); // bool, string, date & data columns don't support 'avg' - ['boolCol', 'stringCol', 'dateCol', 'dataCol'].forEach(colName => { - TestCase.assertThrows(function() { - object.list.avg(colName); - } - )}); + ['bool', 'string', 'date', 'data'].forEach(colName => { + TestCase.assertThrowsContaining(() => object.list.avg(colName + 'Col'), + `Cannot average property '${colName}Col': operation not supported for '${colName}' properties`); + }); // bool, string, date & data columns don't support 'sum' - ['boolCol', 'stringCol', 'dateCol', 'dataCol'].forEach(colName => { - TestCase.assertThrows(function() { - object.list.sum(colName); - } - )}); + ['bool', 'string', 'date', 'data'].forEach(colName => { + TestCase.assertThrowsContaining(() => object.list.sum(colName + 'Col'), + `Cannot sum property '${colName}Col': operation not supported for '${colName}' properties`); + }); }, testListAggregateFunctionsWrongProperty: function() { - var realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); - var object; - var list; + const realm = new Realm({schema: [schemas.PersonObject, schemas.PersonList]}); + let object; realm.write(() => { object = realm.create('PersonList', {list: [ {name: 'Ari', age: 10}, @@ -1185,18 +1209,21 @@ module.exports = { ]}); }); - TestCase.assertThrows(function() { - object.list.min('foo') - }); - TestCase.assertThrows(function() { - object.list.max('foo') - }); - TestCase.assertThrows(function() { - object.list.sum('foo') - }); - TestCase.assertThrows(function() { - object.list.avg('foo') - }); - + TestCase.assertThrowsContaining(() => object.list.min('foo'), + "Property 'foo' does not exist on object 'PersonObject'"); + TestCase.assertThrowsContaining(() => object.list.max('foo'), + "Property 'foo' does not exist on object 'PersonObject'"); + TestCase.assertThrowsContaining(() => object.list.sum('foo'), + "Property 'foo' does not exist on object 'PersonObject'"); + TestCase.assertThrowsContaining(() => object.list.avg('foo'), + "Property 'foo' does not exist on object 'PersonObject'"); + TestCase.assertThrowsContaining(() => object.list.min(), + "JS value must be of type 'string', got (undefined)"); + TestCase.assertThrowsContaining(() => object.list.max(), + "JS value must be of type 'string', got (undefined)"); + TestCase.assertThrowsContaining(() => object.list.sum(), + "JS value must be of type 'string', got (undefined)"); + TestCase.assertThrowsContaining(() => object.list.avg(), + "JS value must be of type 'string', got (undefined)"); }, };