From 199ce7a5bc61d20ab066e16c922198f790d6ed72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kr=C3=A6n=20Hansen?= Date: Wed, 1 Dec 2021 09:00:27 +0100 Subject: [PATCH] [10.20.0-beta.0] Bump version Hermes: Fixing a couple of breaking tests (#4120) * Relaxing error message asserting tests * Fixing testListSubscriptSetters Catching missing libjsi.so as librealm.so loads (#4123) Updated RN peer dependency version (#4124) Fix classes not extending `Realm.Object` (#4125) * Fixing custom classes not extending Realm.Object * Adding a note to the changelog * Update CHANGELOG.md Co-authored-by: FFranck Co-authored-by: FFranck Fixed throwing "Illegal constructor" (#4128) Clarifying the get_internal early return Refixed throwing "Illegal constructor" Various JSI fixes to greenlight more tests (#4129) Flipper doesnt support inlined sourcemaps Return an existing s_ctor Testing with Hermes on CI (#4106) * Adding hermes as target branch for the integration tests workflow * Reading environment variable when enabling Hermes * Adding test app to watchable directories * Adding a hermes variant when testing React Native * Making the ccache engine specific * Moved react-native entry point to fix lint error * Attempt at fixing ReactTestAppTests * Update Podfile to work around the Catalyst issue Fixes for type conversions and minor cleanups (#4137) --- .github/workflows/integration-tests.yml | 27 +++-- .watchmanconfig | 3 +- CHANGELOG.md | 22 ++++ RealmJS.podspec | 3 +- dependencies.list | 2 +- .../react-native/android/app/build.gradle | 2 +- .../environments/react-native/ios/Podfile | 2 +- .../ios/RealmReactNativeTests/AppDelegate.m | 3 +- lib/react-native/.eslintrc.json | 6 + .../index.js} | 6 +- package-lock.json | 6 +- package.json | 8 +- .../realm-network-transport/package-lock.json | 2 +- .../java/io/realm/react/RealmReactModule.java | 9 +- src/js_object_accessor.hpp | 26 ++-- src/js_realm.hpp | 28 +++++ src/jsi/jsi_class.hpp | 113 +++++++++++++----- src/jsi/jsi_types.hpp | 4 + src/jsi/jsi_value.hpp | 70 ++++++++++- tests/ReactTestApp/android/app/build.gradle | 2 +- tests/ReactTestApp/ios/Podfile | 8 +- .../ios/ReactTestAppTests/ReactTestAppTests.m | 2 +- tests/js/asserts.js | 3 +- tests/js/dictionary-tests.js | 26 ++-- tests/js/mixed-tests.js | 9 +- tests/js/object-tests.js | 61 +++++++++- tests/js/realm-tests.js | 10 +- 27 files changed, 353 insertions(+), 110 deletions(-) create mode 100644 lib/react-native/.eslintrc.json rename lib/{react-native.js => react-native/index.js} (95%) diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 46a7827d77..e22e9bc111 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -24,6 +24,7 @@ on: push: branches: - master + - hermes jobs: node: @@ -147,7 +148,7 @@ jobs: MOCHA_REMOTE_CONTEXT: missingServer MOCHA_REMOTE_REPORTER: mocha-github-actions-reporter react-native-ios: - name: React Native on ${{matrix.platform.name}} (${{ matrix.type }}) + name: React Native on ${{matrix.platform.name}} (${{ matrix.type }} using ${{ matrix.platform.engine }}) runs-on: macos-latest timeout-minutes: 60 strategy: @@ -158,12 +159,18 @@ jobs: type: [Release] platform: - name: ios + engine: hermes + build-configuration: simulator + - name: ios + engine: jsc build-configuration: simulator - name: catalyst + engine: jsc build-configuration: catalyst env: # Pin the Xcode version DEVELOPER_DIR: /Applications/Xcode_12.5.1.app + HERMES_ENABLED: ${{ matrix.platform.engine == 'hermes' && 'true' || 'false' }} steps: - uses: actions/checkout@v2 with: @@ -191,7 +198,7 @@ jobs: - name: ccache uses: hendrikmuhs/ccache-action@v1 with: - key: ${{ github.job }}-${{ matrix.platform.name }}-${{ matrix.type }} + key: ${{ github.job }}-${{ matrix.platform.engine }}-${{ matrix.platform.name }}-${{ matrix.type }} - name: Prepend ccache executables to the PATH run: echo PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" >> $GITHUB_ENV @@ -226,14 +233,20 @@ jobs: RETRIES: 5 RETRY_DELAY: 300000 # 5 min react-native-android: - name: React Native on Android (${{ matrix.type }}) + name: React Native on Android (${{ matrix.type }} using ${{ matrix.engine }}) runs-on: macos-latest strategy: fail-fast: false matrix: - #TODO: Reactivate debug when builds are optimized - #type: [Release, Debug] - type: [Release] + type: + - Release + # TODO: Reactivate debug when builds are optimized + # - Debug + engine: + - hermes + - jsc + env: + HERMES_ENABLED: ${{ matrix.engine == 'hermes' && 'true' || 'false' }} timeout-minutes: 60 steps: - uses: actions/checkout@v2 @@ -268,7 +281,7 @@ jobs: - name: ccache uses: hendrikmuhs/ccache-action@v1 with: - key: ${{ github.job }}-${{ matrix.type }} + key: ${{ github.job }}-${{ matrix.engine }}-${{ matrix.type }} - name: Prepend ccache executables to the PATH run: echo PATH="/usr/lib/ccache:/usr/local/opt/ccache/libexec:$PATH" >> $GITHUB_ENV diff --git a/.watchmanconfig b/.watchmanconfig index 74e6de3cdb..57969b3225 100644 --- a/.watchmanconfig +++ b/.watchmanconfig @@ -3,7 +3,6 @@ "react-native/node_modules", "packages/realm-app-importer", "packages/realm-web", - "packages/realm-web-integration-tests", - "tests" + "packages/realm-web-integration-tests" ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b7137d769c..c3f4512ab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ + x.x.x Release notes (yyyy-MM-dd) ============================================================= + ### Enhancements * `Realm.writeCopyTo()` now supports creating snapshots of synced Realms, thus allowing apps to be shipped with partially-populated synced databases. ([#3782](https://github.com/realm/realm-js/issues/3782) * Added beta support for flexible sync ([#4220](https://github.com/realm/realm-js/pull/4220)). @@ -18,6 +20,26 @@ x.x.x Release notes (yyyy-MM-dd) ### Internal * None. +10.20.0-beta.0 Release notes (2021-12-21) +============================================================= +NOTE: This release is rebased on our `10.11.0` release and as such contain the same enhancements and fixes. + +### Enhancements +* Catching missing libjsi.so when loading the librealm.so and rethrowing a more meaningful error, instructing users to upgrade their version of React Native. + +### Fixed +* Fixed support of user defined classes that don't extend `Realm.Object`. +* Fixed throwing "Illegal constructor" when `new` constructing anything other than `Realm` and `Realm.Object`. + +### Compatibility +* MongoDB Realm Cloud. +* Realm Studio v11.0.0. +* APIs are backwards compatible with all previous releases of Realm JavaScript in the 10.5.x series. +* File format: generates Realms with format v22 (reads and upgrades file format v5 or later for non-synced Realm, upgrades file format v10 or later for synced Realms). + +### Internal +* Upgraded Realm Core to v11.7.0. + 10.20.0-alpha.2 Release notes (2021-11-25) ============================================================= NOTE: DO NOT USE THIS RELEASE IN PRODUCTION! diff --git a/RealmJS.podspec b/RealmJS.podspec index 96f9f15f70..c2ea1657e5 100644 --- a/RealmJS.podspec +++ b/RealmJS.podspec @@ -42,6 +42,7 @@ Pod::Spec.new do |s| s.source = { :http => 'https://github.com/realm/realm-js/blob/master/CONTRIBUTING.md#how-to-debug-react-native-podspec' } s.source_files = 'react-native/ios/RealmReact/*.mm' + s.public_header_files = 'react-native/ios/RealmReact/*.h' s.frameworks = uses_frameworks ? ['React'] : [] @@ -57,8 +58,6 @@ Pod::Spec.new do |s| # Header search paths are prefixes to the path specified in #include macros 'HEADER_SEARCH_PATHS' => [ '"$(PODS_TARGET_SRCROOT)/react-native/ios/RealmReact/"', - '"$(PODS_TARGET_SRCROOT)/src/"', - '"$(PODS_TARGET_SRCROOT)/src/jsi/"', '"$(PODS_ROOT)/Headers/Public/React-Core/"' #"'#{app_path}/ios/Pods/Headers/Public/React-Core'" # Use this line instead of 👆 while linting ].join(' ') diff --git a/dependencies.list b/dependencies.list index 7ba6d96d4d..ad442cee5a 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,5 +1,5 @@ PACKAGE_NAME=realm-js -VERSION=10.20.0-alpha.2 +VERSION=10.20.0-beta.0 REALM_CORE_VERSION=11.9.0 NAPI_VERSION=4 OPENSSL_VERSION=1.1.1g diff --git a/integration-tests/environments/react-native/android/app/build.gradle b/integration-tests/environments/react-native/android/app/build.gradle index 8f531d3f4b..89e9719868 100644 --- a/integration-tests/environments/react-native/android/app/build.gradle +++ b/integration-tests/environments/react-native/android/app/build.gradle @@ -78,7 +78,7 @@ import com.android.build.OutputFile */ project.ext.react = [ - enableHermes: true, // clean and rebuild if changing + enableHermes: System.getenv().getOrDefault("HERMES_ENABLED", "true") == "true", // default: true ] apply from: "../../node_modules/react-native/react.gradle" diff --git a/integration-tests/environments/react-native/ios/Podfile b/integration-tests/environments/react-native/ios/Podfile index 374853a386..e5773cdfc6 100644 --- a/integration-tests/environments/react-native/ios/Podfile +++ b/integration-tests/environments/react-native/ios/Podfile @@ -12,7 +12,7 @@ target 'RealmReactNativeTests' do use_react_native!( :path => config[:reactNativePath], # to enable hermes on iOS, change `false` to `true` and then install pods - :hermes_enabled => true + :hermes_enabled => (ENV['HERMES_ENABLED'] || 'true') == 'true' # default: true ) target 'RealmReactNativeTestsTests' do diff --git a/integration-tests/environments/react-native/ios/RealmReactNativeTests/AppDelegate.m b/integration-tests/environments/react-native/ios/RealmReactNativeTests/AppDelegate.m index c1e540f879..d7a1938512 100644 --- a/integration-tests/environments/react-native/ios/RealmReactNativeTests/AppDelegate.m +++ b/integration-tests/environments/react-native/ios/RealmReactNativeTests/AppDelegate.m @@ -53,8 +53,9 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( - (NSURL *)sourceURLForBridge:(RCTBridge *)bridge { #if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil]; // Fetching with inlineSourceMap=true to ease debugging. - return [NSURL URLWithString:[[[[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil] absoluteString] stringByAppendingString:@"&inlineSourceMap=true" ]]; + // return [NSURL URLWithString:[[[[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil] absoluteString] stringByAppendingString:@"&inlineSourceMap=true" ]]; #else return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; #endif diff --git a/lib/react-native/.eslintrc.json b/lib/react-native/.eslintrc.json new file mode 100644 index 0000000000..69944eabf4 --- /dev/null +++ b/lib/react-native/.eslintrc.json @@ -0,0 +1,6 @@ +{ + "extends": [ + "@react-native-community", + "../../.eslintrc" + ] +} \ No newline at end of file diff --git a/lib/react-native.js b/lib/react-native/index.js similarity index 95% rename from lib/react-native.js rename to lib/react-native/index.js index 882a8d0d36..00943b2e23 100644 --- a/lib/react-native.js +++ b/lib/react-native/index.js @@ -16,8 +16,6 @@ // //////////////////////////////////////////////////////////////////////////// -// eslint sourceType: module - import { Platform, NativeModules } from "react-native"; //switch how babel transpiled code creates children objects. @@ -42,9 +40,9 @@ if (typeof global.Realm === "undefined") { ); } -require("./extensions")(global.Realm); +require("../extensions")(global.Realm); -const utils = require("./utils"); +const utils = require("../utils"); const versions = utils.getVersions(); global.Realm.App._setVersions(versions); diff --git a/package-lock.json b/package-lock.json index db8c24c475..28844c06dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "realm", - "version": "10.20.0-alpha.2", + "version": "10.20.0-beta.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "realm", - "version": "10.20.0-alpha.2", + "version": "10.20.0-beta.0", "hasInstallScript": true, "license": "See the actual license in the file LICENSE", "dependencies": { @@ -73,7 +73,7 @@ "npm": ">=7" }, "peerDependencies": { - "react-native": ">=0.60" + "react-native": ">=0.66.0" }, "peerDependenciesMeta": { "react-native": { diff --git a/package.json b/package.json index 5f6def0bf1..33dfb954c8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "realm", "description": "Realm is a mobile database: an alternative to SQLite and key-value stores", - "version": "10.20.0-alpha.2", + "version": "10.20.0-beta.0", "license": "See the actual license in the file LICENSE", "homepage": "https://realm.io", "keywords": [ @@ -31,7 +31,7 @@ }, "types": "types/index.d.ts", "main": "lib/index.js", - "react-native": "lib/react-native.js", + "react-native": "lib/react-native/index.js", "files": [ "cmake", "lib", @@ -102,7 +102,7 @@ "url-parse": "^1.4.4" }, "peerDependencies": { - "react-native": ">=0.60" + "react-native": ">=0.66.0" }, "peerDependenciesMeta": { "react-native": { @@ -162,4 +162,4 @@ 4 ] } -} \ No newline at end of file +} diff --git a/packages/realm-network-transport/package-lock.json b/packages/realm-network-transport/package-lock.json index f7ffce886a..a9586bffa8 100644 --- a/packages/realm-network-transport/package-lock.json +++ b/packages/realm-network-transport/package-lock.json @@ -1462,4 +1462,4 @@ "dev": true } } -} \ No newline at end of file +} diff --git a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java index 02e4283ceb..2b2559304c 100644 --- a/react-native/android/src/main/java/io/realm/react/RealmReactModule.java +++ b/react-native/android/src/main/java/io/realm/react/RealmReactModule.java @@ -51,7 +51,14 @@ class RealmReactModule extends ReactContextBaseJavaModule { private final AssetManager assetManager; static { - SoLoader.loadLibrary("realm"); + try { + SoLoader.loadLibrary("realm"); + } catch (UnsatisfiedLinkError err) { + if (err.getMessage().contains("library \"libjsi.so\" not found")) { + throw new LinkageError("This version of Realm JS needs at least React Native version 0.66.0", err); + } + throw err; + } } public RealmReactModule(ReactApplicationContext reactContext) { diff --git a/src/js_object_accessor.hpp b/src/js_object_accessor.hpp index 460c0cb066..7a41246c25 100644 --- a/src/js_object_accessor.hpp +++ b/src/js_object_accessor.hpp @@ -554,23 +554,21 @@ struct Unbox { auto current_realm = native_accessor->m_realm; auto js_object = Value::validated_to_object(native_accessor->m_ctx, value); - auto realm_object = get_internal>(native_accessor->m_ctx, js_object); - auto is_ros_instance = + auto is_realm_object = Object::template is_instance>(native_accessor->m_ctx, js_object); - if (is_ros_instance && realm_object && realm_object->realm() == current_realm) { - return realm_object->obj(); - } - - if (is_ros_instance && !policy.copy && !policy.update && !policy.create) { - throw std::runtime_error("Realm object is from another Realm"); - } - - // if our RealmObject isn't in ObjectStore, it's a detached object - // (not in to database), and we can't add it - if (is_ros_instance && !realm_object) { - throw std::runtime_error("Cannot reference a detached instance of Realm.Object"); + if (is_realm_object) { + auto realm_object = get_internal>(native_accessor->m_ctx, js_object); + if (realm_object && realm_object->realm() == current_realm) { + return realm_object->obj(); + } + else if (!policy.copy && !policy.update && !policy.create) { + throw std::runtime_error("Realm object is from another Realm"); + } + else if (!realm_object) { + throw std::runtime_error("Cannot reference a detached instance of Realm.Object"); + } } if (!policy.create) { diff --git a/src/js_realm.hpp b/src/js_realm.hpp index 607ba02595..f8c722741b 100644 --- a/src/js_realm.hpp +++ b/src/js_realm.hpp @@ -343,6 +343,11 @@ class RealmClass : public ClassDefinition> { static void get_schema_name_from_object(ContextType, ObjectType, Arguments&, ReturnValue&); static void update_schema(ContextType, ObjectType, Arguments&, ReturnValue&); + // NOTE: __to_object and __to_boolean are shims that allow type conversion tests + // on unit tests / CI. They probably shouldn't be available in production + static void __to_object(ContextType, ObjectType, Arguments&, ReturnValue&); + static void __to_boolean(ContextType, ObjectType, Arguments&, ReturnValue&); + #if REALM_ENABLE_SYNC static void async_open_realm(ContextType, ObjectType, Arguments&, ReturnValue&); #endif @@ -419,6 +424,11 @@ class RealmClass : public ClassDefinition> { {"deleteModel", wrap}, {"_updateSchema", wrap}, {"_schemaName", wrap}, + + // NOTE: __to_object and __to_boolean are shims that allow type conversion tests + // on unit tests / CI. They probably shouldn't be available in production + {"__to_object", wrap<__to_object>}, + {"__to_boolean", wrap<__to_boolean>}, }; PropertyMap const properties = { @@ -1435,6 +1445,24 @@ void RealmClass::get_schema_name_from_object(ContextType ctx, ObjectType this return_value.set(object_schema.name); } +template +void RealmClass::__to_object(ContextType ctx, ObjectType this_object, Arguments& args, ReturnValue& return_value) +{ + args.validate_count(1); + ObjectType newobj = Value::to_object(ctx, args[0]); + + return_value.set(newobj); +} + +template +void RealmClass::__to_boolean(ContextType ctx, ObjectType this_object, Arguments& args, ReturnValue& return_value) +{ + args.validate_count(1); + bool is_bool = Value::to_boolean(ctx, args[0]); + + return_value.set(is_bool); +} + /** * Updates the schema. * diff --git a/src/jsi/jsi_class.hpp b/src/jsi/jsi_class.hpp index 9de70254d2..631eccd8e7 100644 --- a/src/jsi/jsi_class.hpp +++ b/src/jsi/jsi_class.hpp @@ -128,7 +128,7 @@ inline void copyProperty(JsiEnv env, const fbjsi::Object& from, const fbjsi::Obj { auto prop = ObjectGetOwnPropertyDescriptor(env, from, name); REALM_ASSERT_RELEASE(prop); - defineProperty(env, to, "name", *prop); + defineProperty(env, to, name, *prop); } inline constexpr const char g_internal_field[] = "__Realm_internal"; @@ -222,18 +222,48 @@ class ObjectWrap { using Internal = typename T::Internal; using ParentClassType = typename T::Parent; - // XXX if this is static, it won't support multiple runtimes. + // NOTE: if this is static, it won't support multiple runtimes. // Also, may need to suppress destruction. inline static std::optional s_ctor; + /** + * @brief callback for invalid access to index setters + * Throws an error when a users attemps to write to an index on a type that + * doesn't support it. + * + * @return nothing; always throws + */ + static fbjsi::Value readonly_index_setter_callback(fbjsi::Runtime& env, const fbjsi::Value& thisVal, + const fbjsi::Value* args, size_t count) + { + throw fbjsi::JSError(env, "Cannot assign to index"); + } + + /** + * @brief callback for invalid access to property setters + * Trows an error when a user attempts to write to a read-only property + * + * @param propname name of the property the user is trying to write to + * @return nothin; always throws + */ + static fbjsi::Value readonly_setter_callback(fbjsi::Runtime& env, const fbjsi::Value& thisVal, + const fbjsi::Value* args, size_t count, std::string const& propname) + { + throw fbjsi::JSError(env, util::format("Cannot assign to read only property '%1'", propname)); + } + static JsiFunc create_constructor(JsiEnv env) { + if (s_ctor) { + return *s_ctor; + } + auto& s_type = get_class(); auto nativeFunc = !bool(s_type.constructor) ? fbjsi::Value() : fbjsi::Function::createFromHostFunction( - env, propName(env, s_type.name), /* XXX paramCount */ 0, + env, propName(env, s_type.name), /* paramCount verified by callback */ 0, [](fbjsi::Runtime& rt, const fbjsi::Value&, const fbjsi::Value* args, size_t count) -> fbjsi::Value { REALM_ASSERT_RELEASE(count >= 1); @@ -248,11 +278,9 @@ class ObjectWrap { .call(env, "nativeFunc", util::format(R"( return function %1(...args) { - // "use strict"; - if (!nativeFunc && false) // XXX only disable check for Realm.Object - throw TypeError("%1() cannot be constructed directly from javascript"); - if (!new.target && false) { // XXX find another way to detect this correctly - throw TypeError("%1() must be called as a constructor"); + // Allow explicit construction only for classes with a constructor + if (new.target && !nativeFunc) { + throw TypeError("Illegal constructor"); } if (nativeFunc) nativeFunc(this, ...args); @@ -281,12 +309,20 @@ class ObjectWrap { if (prop.setter) { desc.setProperty(env, "set", funcVal(env, "set_" + name, 1, prop.setter)); } + else { + desc.setProperty( + env, "set", + funcVal(env, "set_" + name, 0, + std::bind(ObjectWrap::readonly_setter_callback, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, name))); + } defineProperty(env, *s_ctor, name, desc); } for (auto&& [name, method] : s_type.static_methods) { auto desc = fbjsi::Object(env); - desc.setProperty(env, "value", funcVal(env, name, /* XXX paramCount */ 0, method)); + desc.setProperty(env, "value", + funcVal(env, name, /* paramCount must be verified by callback */ 0, method)); defineProperty(env, *s_ctor, name, desc); } @@ -300,12 +336,20 @@ class ObjectWrap { if (prop.setter) { desc.setProperty(env, "set", funcVal(env, "set_" + name, 1, prop.setter)); } + else { + desc.setProperty( + env, "set", + funcVal(env, "set_" + name, 0, + std::bind(ObjectWrap::readonly_setter_callback, std::placeholders::_1, + std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, name))); + } defineProperty(env, proto, name, desc); } for (auto&& [name, method] : s_type.methods) { auto desc = fbjsi::Object(env); - desc.setProperty(env, "value", funcVal(env, name, /* XXX paramCount */ 0, method)); + desc.setProperty(env, "value", + funcVal(env, name, /* paramCount must be verified by callback */ 0, method)); defineProperty(env, proto, name, desc); } @@ -329,10 +373,11 @@ class ObjectWrap { // XXX Do we want to trap things like ownKeys() and getOwnPropertyDescriptors() to support for...in? auto [getter, setter] = s_type.index_accessor; auto desc = fbjsi::Object(env); - desc.setProperty(env, "value", - globalType(env, "Function") - .call(env, "getter", "setter", R"( - const integerPattern = /^\d+$/; + desc.setProperty( + env, "value", + globalType(env, "Function") + .call(env, "getter", "setter", R"( + const integerPattern = /^-?\d+$/; function getIndex(prop) { if (typeof prop === "string" && integerPattern.test(prop)) { return parseInt(prop, 10); @@ -372,24 +417,26 @@ class ObjectWrap { const index = getIndex(prop); if (Number.isNaN(index)) { return Reflect.set(...arguments); - } else if (setter) { - return setter(target, index, value); + } else if (index < 0) { + // This mimics realm::js::validated_positive_index + throw new Error(`Index ${index} cannot be less than zero.`); } else { - return false; + return setter(target, index, value); } } } return (obj) => new Proxy(obj, handler); )") - .asObject(env) - .asFunction(env) - .call(env, funcVal(env, "getter", 0, getter), funcVal(env, "setter", 1, setter)) - .asObject(env) - .asFunction(env)); + .asObject(env) + .asFunction(env) + .call(env, funcVal(env, "getter", 0, getter), + funcVal(env, "setter", 1, setter ? setter : ObjectWrap::readonly_index_setter_callback)) + .asObject(env) + .asFunction(env)); defineProperty(env, *s_ctor, "_proxyWrapper", desc); } - return env((*s_ctor)->getFunction(env)); + return *s_ctor; } static JsiObj create_instance(JsiEnv env, Internal* ptr = nullptr) @@ -432,13 +479,16 @@ class ObjectWrap { { auto internal = object->getProperty(env, g_internal_field); if (internal.isUndefined()) { - if constexpr (std::is_same_v>) // XXX comment why + // In the case of a user opening a Realm with a class-based model, + // the user defined constructor will get called before the "internal" property has been set. + if constexpr (std::is_same_v>) return nullptr; throw fbjsi::JSError(env, "no internal field"); } - if (!JsiObj(object)->instanceOf(env, *s_ctor)) { - throw fbjsi::JSError(env, "calling method on wrong type of object"); - } + // The following check is disabled to support user defined classes that doesn't extend Realm.Object + // if (!JsiObj(object)->instanceOf(env, *s_ctor)) { + // throw fbjsi::JSError(env, "calling method on wrong type of object"); + // } return unwrapUnique(env, std::move(internal)); } static void set_internal(JsiEnv env, const JsiObj& object, Internal* data) @@ -533,13 +583,10 @@ class ObjectWrap { if (!maybeConstructor) { // 1.Check by name if the constructor is already created for this RealmObject if (!schemaObjects.count(schemaName)) { - // 2.Create the constructor - - // create the RealmObject function by name - // XXX May need to escape/sanitize schema.name to avoid code injection + // create an anonymous RealmObject function auto schemaObjectConstructor = globalType(env, "Function") - .callAsConstructor(env, "return function " + schema.name + "() {}") + .callAsConstructor(env, "return function () {}") .asObject(env) .asFunction(env) .call(env) @@ -622,7 +669,7 @@ class ObjectWrap { inline static auto& get_schemaObjectTypes() { - // XXX this being static prevents using multiple runtimes. + // NOTE: this being static prevents using multiple runtimes. static std::unordered_map> s_schemaObjectTypes; return s_schemaObjectTypes; } diff --git a/src/jsi/jsi_types.hpp b/src/jsi/jsi_types.hpp index a739100bbc..21bbf8efe1 100644 --- a/src/jsi/jsi_types.hpp +++ b/src/jsi/jsi_types.hpp @@ -118,6 +118,10 @@ class JsiWrap { { return &m_val; } + const T* operator*() const + { + return &m_val; + } /*implicit*/ operator const T&() const& { diff --git a/src/jsi/jsi_value.hpp b/src/jsi/jsi_value.hpp index 371acb9fc3..cb379f0299 100644 --- a/src/jsi/jsi_value.hpp +++ b/src/jsi/jsi_value.hpp @@ -20,7 +20,7 @@ #include "jsi_string.hpp" #include "jsi_types.hpp" -//#include "node_buffer.hpp" +#include "realm/util/to_string.hpp" namespace realm { namespace js { @@ -166,7 +166,7 @@ inline bool realmjsi::Value::is_binary(JsiEnv env, const JsiVal& value) template <> inline bool realmjsi::Value::is_valid(const JsiVal& value) { - return true; // XXX + return (*value) != nullptr; } template <> @@ -229,7 +229,40 @@ inline JsiVal realmjsi::Value::from_uuid(JsiEnv env, const UUID& uuid) template <> inline bool realmjsi::Value::to_boolean(JsiEnv env, const JsiVal& value) { - return value->getBool(); // XXX should do conversion. + if (value->isBool()) { + return value->getBool(); + } + + // boolean conversions as specified by + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean ... + + // trivial conversions to false + if (value->isUndefined() || value->isNull()) { + return false; + } + + if (value->isObject()) { + // not null, as checked above + return true; + } + + if (value->isString()) { + // only the empty string is false + return value->toString(env).utf8(env) != ""; + } + + if (value->isNumber()) { + double const dblval = value->asNumber(); + if (std::isnan(dblval)) { + return false; + } + + std::string const stringval = value->toString(env).utf8(env); + return (stringval != "0" && stringval != "-0"); + } + + throw fbjsi::JSError(env, + util::format("TypeError: cannot convert type %1 to boolean", Value::typeof(env, value))); } template <> @@ -285,10 +318,37 @@ inline OwnedBinaryData realmjsi::Value::to_binary(JsiEnv env, const JsiVal& valu throw std::runtime_error("Can only convert ArrayBuffer and ArrayBufferView objects to binary"); } +/** + * @brief convert a JSI value to an object + * Will try to convert a given value to a JavaScript object according to + * https://tc39.es/ecma262/#sec-toobject. Most primitive types will be wrapped + * in their corresponding object types (e.g., string -> String). + * + * @param env JSI runtime environment + * @param value JSI value that will be converted to object + * @return JsiObj + */ template <> -inline JsiObj realmjsi::Value::to_object(JsiEnv env, const JsiVal& value) +inline JsiObj realmjsi::Value::to_object(JsiEnv env, JsiVal const& value) { - return env(value->asObject(env)); // XXX convert? + if (value->isObject()) { + return env(value->asObject(env)); + } + + // trivial non-conversions + if (value->isNull() || value->isUndefined()) { + throw fbjsi::JSError(env, util::format("TypeError: cannot convert '%1' to object", + realmjsi::Value::typeof(env, value))); // throw TypeError + } + + // use JavaScript's `Object()` to wrap types in their corresponding object types + auto objectCtor = env->global().getPropertyAsFunction(env, "Object"); + fbjsi::Value wrappedValue = objectCtor.callAsConstructor(env, value); + if (!wrappedValue.isObject()) { + throw fbjsi::JSError( + env, util::format("TypeError: cannot wrap %1 in Object", realmjsi::Value::typeof(env, value))); + } + return env(wrappedValue.asObject(env)); } template <> diff --git a/tests/ReactTestApp/android/app/build.gradle b/tests/ReactTestApp/android/app/build.gradle index b1030b2dd6..1d3fc6cac2 100644 --- a/tests/ReactTestApp/android/app/build.gradle +++ b/tests/ReactTestApp/android/app/build.gradle @@ -78,7 +78,7 @@ import com.android.build.OutputFile */ project.ext.react = [ - enableHermes: true, // clean and rebuild if changing + enableHermes: System.getenv().getOrDefault("HERMES_ENABLED", "true") == "true", // default: true ] apply from: "../../node_modules/react-native/react.gradle" diff --git a/tests/ReactTestApp/ios/Podfile b/tests/ReactTestApp/ios/Podfile index 5e85d8e1e5..078e6dc925 100644 --- a/tests/ReactTestApp/ios/Podfile +++ b/tests/ReactTestApp/ios/Podfile @@ -9,7 +9,7 @@ target 'ReactTestApp' do use_react_native!( :path => config[:reactNativePath], # to enable hermes on iOS, change `false` to `true` and then install pods - :hermes_enabled => true + :hermes_enabled => (ENV['HERMES_ENABLED'] || 'true') == 'true' # default: true ) # Since this is a project within RealmJS, we need to add the pod manually. @@ -38,6 +38,12 @@ target 'ReactTestApp' do config.build_settings['CODE_SIGN_IDENTITY[sdk=macosx*]'] = '-' end end + # Applying https://github.com/facebook/folly/issues/1470#issuecomment-943123653 + if target.name == "RCT-Folly" + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'FOLLY_HAVE_CLOCK_GETTIME=1'] + end + end end end end diff --git a/tests/ReactTestApp/ios/ReactTestAppTests/ReactTestAppTests.m b/tests/ReactTestApp/ios/ReactTestAppTests/ReactTestAppTests.m index 58b1080e97..647878ccf9 100644 --- a/tests/ReactTestApp/ios/ReactTestAppTests/ReactTestAppTests.m +++ b/tests/ReactTestApp/ios/ReactTestAppTests/ReactTestAppTests.m @@ -19,7 +19,7 @@ #import #import -#import +#import #import #import diff --git a/tests/js/asserts.js b/tests/js/asserts.js index 60dd173530..b8ec644767 100644 --- a/tests/js/asserts.js +++ b/tests/js/asserts.js @@ -177,7 +177,7 @@ module.exports = { func(); } catch (e) { caught = true; - if (!e.message.includes(expectedMessage)) { + if (!e.message.includes(expectedMessage) && !e.name.includes(expectedMessage)) { throw new TestFailureError( `Expected exception "${expectedMessage}" not thrown - instead caught: "${e}"`, depth, @@ -189,6 +189,7 @@ module.exports = { throw new TestFailureError(`Expected exception "${expectedMessage}" not thrown`, depth); } }, + assertThrowsAsyncContaining: async function (func, expectedMessage, depth) { let caught = false; try { diff --git a/tests/js/dictionary-tests.js b/tests/js/dictionary-tests.js index 70ca4a31ca..b4d906c134 100644 --- a/tests/js/dictionary-tests.js +++ b/tests/js/dictionary-tests.js @@ -162,14 +162,13 @@ module.exports = { TestCase.assertEqual(data.a.y, 2, "Should be an equals to a.y = 2"); TestCase.assertEqual(data.a.z, 4, "Should be an equals to a.z = 4"); - let err = new Error("Property must be of type 'number', got (error)"); - TestCase.assertThrowsException( + TestCase.assertThrowsContaining( () => realm.write(() => realm.create(DictIntSchema.name, { a: { c: "error" } })), - err, + "Property must be of type 'number', got (error)", ); - TestCase.assertThrowsException( + TestCase.assertThrowsContaining( () => realm.write(() => (data.a = "cc")), - new Error("Dictionary.a must be of type 'number{}', got 'string' ('cc')"), + "Dictionary.a must be of type 'number{}', got 'string' ('cc')", ); realm.close(); @@ -182,13 +181,9 @@ module.exports = { a: "wwwww{}", }, }; - let err = new Error( - "Schema validation failed due to the following errors:\n- Property 'Dictionary.a' of type 'dictionary' has unknown object type 'wwwww'", - ); - let _defer = () => { - let r = new Realm({ schema: [DictWrongSchema] }); - }; - TestCase.assertThrowsException(_defer, err); + TestCase.assertThrowsContaining(() => { + new Realm({ schema: [DictWrongSchema] }); + }, "Schema validation failed due to the following errors:\n- Property 'Dictionary.a' of type 'dictionary' has unknown object type 'wwwww'"); }, testDictionaryMutability() { @@ -746,8 +741,11 @@ module.exports = { testDictionaryErrorHandling() { let realm = new Realm({ schema: [DictSchema] }); - let err = new Error("Only Realm instances are supported."); - TestCase.assertThrowsException(() => realm.write(() => realm.create(DictSchema.name, { a: { x: {} } })), err); + TestCase.assertThrowsContaining(() => { + realm.write(() => { + realm.create(DictSchema.name, { a: { x: {} } }); + }); + }, "Only Realm instances are supported."); realm.write(() => realm.create(DictSchema.name, { a: { x: null } })); let data = realm.objects(DictSchema.name)[0].a; TestCase.assertEqual(data.x, null, "Should be an equals to mutable.x = null"); diff --git a/tests/js/mixed-tests.js b/tests/js/mixed-tests.js index c392e066a3..edc51f51b4 100644 --- a/tests/js/mixed-tests.js +++ b/tests/js/mixed-tests.js @@ -166,9 +166,9 @@ module.exports = { testMixedWrongType() { let realm = new Realm({ schema: [SingleSchema] }); - TestCase.assertThrowsException( + TestCase.assertThrowsContaining( () => realm.write(() => realm.create(SingleSchema.name, { a: Object.create({}) })), - new Error("Only Realm instances are supported."), + "Only Realm instances are supported.", ); }, @@ -211,12 +211,11 @@ module.exports = { TestCase.assertEqual(objectsBefore.length, 0); // check if the understandable error message is thrown - const error = new Error("A mixed property cannot contain an array of values."); - TestCase.assertThrowsException(() => { + TestCase.assertThrowsContaining(() => { realm.write(() => { realm.create("MixedClass", { value: [123, false, "hello"] }); }); - }, error); + }, "A mixed property cannot contain an array of values."); // verify that the transaction has been rolled back const objectsAfter = realm.objects(MixedSchema.name); diff --git a/tests/js/object-tests.js b/tests/js/object-tests.js index 5d93f9e6e8..fe1bec71a4 100644 --- a/tests/js/object-tests.js +++ b/tests/js/object-tests.js @@ -436,6 +436,63 @@ module.exports = { }); }, + testObjectConversion: function () { + const realm = new Realm({ schema: [schemas.TestObject] }); + TestCase.assertInstanceOf( + realm.__to_object("This is a string"), + String, + "__to_object(string) should return String Object", + ); + TestCase.assertTrue( + realm.__to_object("Foo") == String("Foo"), + '__to_object(string("Foo")) should return String("Foo") Object', + ); + TestCase.assertInstanceOf(realm.__to_object(12345), Number, "__to_object(int) should return Number Object"); + TestCase.assertTrue( + realm.__to_object(12345) == Number(12345), + "__to_object(int(12345)) should return Number(12345) Object", + ); + TestCase.assertInstanceOf(realm.__to_object(false), Boolean, "__to_object(bool) should return Boolean Object"); + TestCase.assertTrue( + realm.__to_object(false) == Boolean(false), + "__to_object(bool(false)) should return Boolean(false) Object", + ); + TestCase.assertInstanceOf(realm.__to_object(new Date()), Date, "__to_object(Date) should return Date Object"); + + TestCase.assertThrowsContaining(() => { + realm.__to_object(null); + }, "TypeError"); + + TestCase.assertThrowsContaining(() => { + realm.__to_object(undefined); + }, "TypeError"); + + realm.close(); + }, + + // tests conversion of various types to boolean as specified in + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean + // it resides here for lack of a better place, and because the direct type conversion + // operations have been made available through the Realm object + testBooleanConversion: function () { + const realm = new Realm({ schema: [schemas.TestObject] }); + TestCase.assertEqual(realm.__to_boolean(""), false, '__to_boolean("") should return false'); + TestCase.assertEqual(realm.__to_boolean(0), false, "__to_boolean(0) should return false"); + TestCase.assertEqual(realm.__to_boolean(-0), false, "__to_boolean(-0) should return false"); + TestCase.assertEqual(realm.__to_boolean(null), false, "__to_boolean(null) should return false"); + TestCase.assertEqual(realm.__to_boolean(false), false, "__to_boolean(false) should return false"); + TestCase.assertEqual(realm.__to_boolean(NaN), false, "__to_boolean(NaN) should return false"); + TestCase.assertEqual(realm.__to_boolean(undefined), false, "__to_boolean(undefined) should return false"); + + TestCase.assertEqual(realm.__to_boolean("false"), true, '__to_boolean("false") should return true'); + TestCase.assertEqual(realm.__to_boolean(1), true, "__to_boolean(1) should return true"); + TestCase.assertEqual(realm.__to_boolean(-1), true, "__to_boolean(-1) should return true"); + TestCase.assertEqual(realm.__to_boolean([]), true, "__to_boolean([]) should return true"); + TestCase.assertEqual(realm.__to_boolean(Object()), true, "__to_boolean(Object()) should return true"); + + realm.close(); + }, + testObjectSchema: function () { const realm = new Realm({ schema: [schemas.TestObject] }); var obj; @@ -773,8 +830,8 @@ module.exports = { }); // property that does not exist - TestCase.assertThrowsException(() => { + TestCase.assertThrowsContaining(() => { obj.getPropertyType("foo"); - }, new Error("No such property: foo")); + }, "No such property: foo"); }, }; diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 532ba61905..cc7db32a78 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -1580,22 +1580,22 @@ module.exports = { testErrorMessageFromInvalidWrite: function () { const realm = new Realm({ schema: [schemas.PersonObject] }); - TestCase.assertThrowsException(() => { + TestCase.assertThrowsContaining(() => { realm.write(() => { const p1 = realm.create("PersonObject", { name: "Ari", age: 10 }); p1.age = "Ten"; }); - }, new Error("PersonObject.age must be of type 'number', got 'string' ('Ten')")); + }, "PersonObject.age must be of type 'number', got 'string' ('Ten')"); }, testErrorMessageFromInvalidCreate: function () { const realm = new Realm({ schema: [schemas.PersonObject] }); - TestCase.assertThrowsException(() => { + TestCase.assertThrowsContaining(() => { realm.write(() => { - const p1 = realm.create("PersonObject", { name: "Ari", age: "Ten" }); + realm.create("PersonObject", { name: "Ari", age: "Ten" }); }); - }, new Error("PersonObject.age must be of type 'number', got 'string' ('Ten')")); + }, "PersonObject.age must be of type 'number', got 'string' ('Ten')"); }, testValidTypesForListProperties: function () {