diff --git a/CHANGELOG.md b/CHANGELOG.md index 73c7fff2b73..02039a30edd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,31 @@ x.x.x Release notes (yyyy-MM-dd) ============================================================= ### Breaking change -* Model classes passed as schema to the `Realm` constructor must now extend `Realm.Object`. +* Model classes passed as schema to the `Realm` constructor must now extend `Realm.Object` and will no longer have their constructors called when pulling an object of that type from the database. Existing classes already extending `Realm.Object` now need to call the `super` constructor passing two arguments: + - `realm`: The Realm to create the object in. + - `values`: Values to pass to the `realm.create` call when creating the object in the database. +* Renamed the `RealmInsertionModel` type to `Unmanaged` to simplify and highlight its usage. + +### Enhancements +* Class-based models (i.e. user defined classes extending `Realm.Object` and passed through the `schema` when opening a Realm), will now create object when their constructor is called: + +```ts +class Person extends Realm.Object { + name!: string; + + static schema = { + name: "Person", + properties: { name: "string" }, + }; +} + +const realm = new Realm({ schema: [Person] }); +realm.write(() => { + const alice = new Person(realm, { name: "Alice" }); + // A Person { name: "Alice" } is now persisted in the database + console.log("Hello " + alice.name); +}); +``` ### Enhancements * None. diff --git a/integration-tests/tests/src/schemas/person-and-dogs.ts b/integration-tests/tests/src/schemas/person-and-dogs.ts index 9b9e21a10b8..8a6bfcdc88c 100644 --- a/integration-tests/tests/src/schemas/person-and-dogs.ts +++ b/integration-tests/tests/src/schemas/person-and-dogs.ts @@ -38,16 +38,13 @@ export const PersonSchema: Realm.ObjectSchema = { }; export class Person extends Realm.Object { - name: string; - age: number; + name!: string; + age!: number; friends!: Realm.List; dogs!: Realm.Collection; - constructor(name: string, age: number) { - super(); - - this.name = name; - this.age = age; + constructor(realm: Realm, name: string, age: number) { + super(realm, { name, age }); } static schema: Realm.ObjectSchema = PersonSchema; @@ -69,16 +66,12 @@ export const DogSchema: Realm.ObjectSchema = { }; export class Dog extends Realm.Object { - name: string; - age: number; - owner: Person; - - constructor(name: string, age: number, owner: Person) { - super(); + name!: string; + age!: number; + owner!: Person; - this.name = name; - this.age = age; - this.owner = owner; + constructor(realm: Realm, name: string, age: number, owner: Person) { + super(realm, { name, age, owner }); } static schema: Realm.ObjectSchema = DogSchema; diff --git a/integration-tests/tests/src/tests/class-models.ts b/integration-tests/tests/src/tests/class-models.ts index 8bf6797fc6b..9c94d05cdb6 100644 --- a/integration-tests/tests/src/tests/class-models.ts +++ b/integration-tests/tests/src/tests/class-models.ts @@ -17,9 +17,10 @@ //////////////////////////////////////////////////////////////////////////// import { expect } from "chai"; - import Realm from "realm"; +import { openRealmBeforeEach } from "../hooks"; + describe("Class models", () => { describe("as schema element", () => { beforeEach(() => { @@ -72,4 +73,49 @@ describe("Class models", () => { new Realm({ schema: [Person] }); }); }); + + describe("#constructor", () => { + type UnmanagedPerson = Partial & Pick; + class Person extends Realm.Object { + id!: Realm.BSON.ObjectId; + name!: string; + age!: number; + friends!: Realm.List; + + static schema: Realm.ObjectSchema = { + name: "Person", + properties: { + id: { + type: "objectId", + default: new Realm.BSON.ObjectId(), // TODO: Make this a function + }, + name: "string", + age: { + type: "int", + default: 32, + }, + friends: "Person[]", + }, + }; + } + + openRealmBeforeEach({ schema: [Person] }); + + it("creates objects with values", function (this: RealmContext) { + this.realm.write(() => { + // Expect no persons in the database + const persons = this.realm.objects("Person"); + expect(persons.length).equals(0); + + const alice = new Person(this.realm, { name: "Alice" }); + expect(alice.name).equals("Alice"); + // Expect the first element to be the object we just added + expect(persons.length).equals(1); + expect(persons[0]._objectId()).equals(alice._objectId()); + expect(persons[0].name).equals("Alice"); + // Property value fallback to the default + expect(persons[0].age).equals(32); + }); + }); + }); }); diff --git a/packages/realm-react/src/useObject.tsx b/packages/realm-react/src/useObject.tsx index 57c4ac88b59..53ad8405527 100644 --- a/packages/realm-react/src/useObject.tsx +++ b/packages/realm-react/src/useObject.tsx @@ -45,7 +45,10 @@ export function createUseObject(useRealm: () => Realm) { * @param primaryKey - The primary key of the desired object which will be retrieved using {@link Realm.objectForPrimaryKey} * @returns either the desired {@link Realm.Object} or `null` in the case of it being deleted or not existing. */ - return function useObject(type: string | { new (): T }, primaryKey: PrimaryKey): (T & Realm.Object) | null { + return function useObject( + type: string | { new (...args: any): T }, + primaryKey: PrimaryKey, + ): (T & Realm.Object) | null { const realm = useRealm(); // Create a forceRerender function for the cachedObject to use as its updateCallback, so that diff --git a/packages/realm-react/src/useQuery.tsx b/packages/realm-react/src/useQuery.tsx index 8fb14dc0b67..d5a5ed87b44 100644 --- a/packages/realm-react/src/useQuery.tsx +++ b/packages/realm-react/src/useQuery.tsx @@ -48,7 +48,9 @@ export function createUseQuery(useRealm: () => Realm) { * @param type - The object type, depicted by a string or a class extending Realm.Object * @returns a collection of realm objects or an empty array */ - return function useQuery(type: string | ({ new (): T } & Realm.ObjectClass)): Realm.Results { + return function useQuery( + type: string | ({ new (...args: any): T } & Realm.ObjectClass), + ): Realm.Results { const realm = useRealm(); // Create a forceRerender function for the cachedCollection to use as its updateCallback, so that diff --git a/src/js_realm_object.hpp b/src/js_realm_object.hpp index 738fff2dc88..01334c9e3c4 100644 --- a/src/js_realm_object.hpp +++ b/src/js_realm_object.hpp @@ -62,6 +62,7 @@ struct RealmObjectClass : ClassDefinition> { using FunctionType = typename T::Function; using ObjectType = typename T::Object; using ValueType = typename T::Value; + using Context = js::Context; using String = js::String; using Value = js::Value; using Object = js::Object; @@ -72,6 +73,7 @@ struct RealmObjectClass : ClassDefinition> { static ObjectType create_instance(ContextType, realm::js::RealmObject); + static void constructor(ContextType, ObjectType, Arguments&); static void get_property(ContextType, ObjectType, const String&, ReturnValue&); static bool set_property(ContextType, ObjectType, const String&, ValueType); static std::vector get_property_names(ContextType, ObjectType); @@ -164,6 +166,44 @@ typename T::Object RealmObjectClass::create_instance(ContextType ctx, realm:: } } +/** + * @brief Implements the constructor for a Realm.Object, calling the `Realm#create` instance method to create an + * object in the database. + * + * @note This differs from `RealmObjectClass::create_instance` as it is executed when end-users construct a `new + * Realm.Object()` (or another user-defined class extending `Realm.Object`), whereas `create_instance` is called when + * reading objects from the database. + * + * @tparam T Engine specific types. + * @param ctx JS context. + * @param this_object JS object being returned to the user once constructed. + * @param args Arguments passed by the user when calling the constructor. + */ +template +void RealmObjectClass::constructor(ContextType ctx, ObjectType this_object, Arguments& args) +{ + // Parse aguments + args.validate_count(2); + auto constructor = Object::validated_get_object(ctx, this_object, "constructor"); + auto realm = Value::validated_to_object(ctx, args[0], "realm"); + auto values = Value::validated_to_object(ctx, args[1], "values"); + + // Create an object + std::vector create_args{constructor, values}; + Arguments create_arguments{ctx, create_args.size(), create_args.data()}; + ReturnValue result{ctx}; + RealmClass::create(ctx, realm, create_arguments, result); + ObjectType tmp_realm_object = Value::validated_to_object(ctx, result); + + // Copy the internal from the constructed object onto this_object + auto realm_object = get_internal>(ctx, tmp_realm_object); + // The finalizer on the ObjectWrap (applied inside of set_internal) will delete the `new_realm_object` which is + // why we create a new instance to avoid a double free (the first of which will happen when the `tmp_realm_object` + // destructs). + auto new_realm_object = new realm::js::RealmObject(*realm_object); + set_internal>(ctx, this_object, new_realm_object); +} + template void RealmObjectClass::get_property(ContextType ctx, ObjectType object, const String& property_name, ReturnValue& return_value) @@ -374,7 +414,7 @@ void RealmObjectClass::add_listener(ContextType ctx, ObjectType this_object, auto callback = Value::validated_to_function(ctx, args[0]); Protected protected_callback(ctx, callback); Protected protected_this(ctx, this_object); - Protected protected_ctx(Context::get_global_context(ctx)); + Protected protected_ctx(Context::get_global_context(ctx)); auto token = realm_object->add_notification_callback([=](CollectionChangeSet const& change_set, std::exception_ptr exception) { diff --git a/src/js_types.hpp b/src/js_types.hpp index 7ffd98f0a43..3409c468c0c 100644 --- a/src/js_types.hpp +++ b/src/js_types.hpp @@ -461,7 +461,7 @@ struct Object { static typename ClassType::Internal* get_internal(ContextType ctx, const ObjectType&); template - static void set_internal(ContextType ctx, const ObjectType&, typename ClassType::Internal*); + static void set_internal(ContextType ctx, ObjectType&, typename ClassType::Internal*); static ObjectType create_from_app_error(ContextType, const app::AppError&); static ValueType create_from_optional_app_error(ContextType, const util::Optional&); @@ -533,6 +533,8 @@ struct ReturnValue { void set(uint32_t); void set_null(); void set_undefined(); + + operator ValueType() const; }; template @@ -558,14 +560,35 @@ REALM_JS_INLINE typename T::Object create_instance_by_schema(typename T::Context return Object::template create_instance_by_schema(ctx, schema, internal); } +/** + * @brief Get the internal (C++) object backing a JS object. + * + * @tparam T Engine specific types. + * @tparam ClassType Class implementing the C++ interface backing the JS accessor object (passed as `object`). + * @param ctx JS context. + * @param object JS object with an internal object. + * @return Pointer to the internal object. + */ template REALM_JS_INLINE typename ClassType::Internal* get_internal(typename T::Context ctx, const typename T::Object& object) { return Object::template get_internal(ctx, object); } +/** + * @brief Set the internal (C++) object backing the JS object. + * + * @note Calling this transfer ownership of the object pointed to by `ptr` and links it to the lifetime of to the + * `object` passed as argument. + * + * @tparam T Engine specific types. + * @tparam ClassType Class implementing the C++ interface backing the JS accessor object (passed as `object`). + * @param ctx JS context. + * @param object JS object having its internal set. + * @param ptr A pointer to an internal object. + */ template -REALM_JS_INLINE void set_internal(typename T::Context ctx, const typename T::Object& object, +REALM_JS_INLINE void set_internal(typename T::Context ctx, typename T::Object& object, typename ClassType::Internal* ptr) { Object::template set_internal(ctx, object, ptr); diff --git a/src/jsc/jsc_class.hpp b/src/jsc/jsc_class.hpp index 8d9d6337946..15655dacde1 100644 --- a/src/jsc/jsc_class.hpp +++ b/src/jsc/jsc_class.hpp @@ -158,6 +158,7 @@ class ObjectWrap { } static Internal* get_internal(JSContextRef ctx, const JSObjectRef& object); + static void set_internal(JSContextRef ctx, JSObjectRef& instance, typename ClassType::Internal* internal); static void on_context_destroy(JSContextRef ctx, std::string realmPath); @@ -203,9 +204,6 @@ class ObjectWrap { return false; } - static void set_internal_property(JSContextRef ctx, JSObjectRef& instance, - typename ClassType::Internal* internal); - static void define_schema_properties(JSContextRef ctx, JSObjectRef constructorPrototype, const realm::ObjectSchema& schema, bool redefine); static void define_accessor_for_schema_property(JSContextRef ctx, JSObjectRef& target, jsc::String* name); @@ -626,17 +624,37 @@ typename ClassType::Internal* ObjectWrap::get_internal(JSContextRef c return realmObjectInstance->m_object.get(); } +/** + * @brief Stores data on the `instance` JS object, making it possible to retrieve the internal object instance at a + * later point, using `ObjectWrap::get_internal`. + * + * @note This hands over ownership of the object pointed to by the `internal` pointer. + * + * @tparam ClassType The class implementing the JS interface (from C++). + * @param ctx JS context + * @param instance JS object which is handed ownership of the internal object being pointed to by `internal`. + * @param internal A pointer to an instance of a C++ object being wrapped. + */ template -void ObjectWrap::set_internal_property(JSContextRef ctx, JSObjectRef& instance, - typename ClassType::Internal* internal) +void ObjectWrap::set_internal(JSContextRef ctx, JSObjectRef& instance, + typename ClassType::Internal* internal) { - // create a JS object that has a finializer to delete the internal reference - JSObjectRef internalObject = JSObjectMake(ctx, m_internalValueClass, new ObjectWrap(internal)); - const jsc::String* externalName = get_cached_property_name("_external"); - auto attributes = realm::js::PropertyAttributes::ReadOnly | realm::js::PropertyAttributes::DontDelete | - realm::js::PropertyAttributes::DontEnum; - Object::set_property(ctx, instance, *externalName, internalObject, attributes); + bool isRealmObjectClass = std::is_same>::value; + if (isRealmObjectClass) { + // create a JS object that has a finializer to delete the internal reference + JSObjectRef internalObject = JSObjectMake(ctx, m_internalValueClass, new ObjectWrap(internal)); + const jsc::String* externalName = get_cached_property_name("_external"); + auto attributes = realm::js::PropertyAttributes::ReadOnly | realm::js::PropertyAttributes::DontDelete | + realm::js::PropertyAttributes::DontEnum; + if (internal) { + Object::set_property(ctx, instance, *externalName, internalObject, attributes); + } + } + else { + auto wrap = static_cast*>(JSObjectGetPrivate(instance)); + *wrap = internal; + } } static inline JSObjectRef try_get_prototype(JSContextRef ctx, JSObjectRef object) @@ -686,13 +704,6 @@ bool ObjectWrap::has_instance(JSContextRef ctx, JSValueRef value) proto = try_get_prototype(ctx, proto); } - // handle RealmObjects using user defined ctors without extending RealmObject. - // In this case we just check for existing internal value to identify RealmObject instances - auto internal = ObjectWrap::get_internal(ctx, object); - if (internal != nullptr) { - return true; - } - // if there is no RealmObjectClass on the prototype chain and the object does not have existing internal value // then this is not an RealmObject instance return false; @@ -780,8 +791,7 @@ inline JSObjectRef ObjectWrap::create_instance_by_schema(JSContextRef definition.className = schema.name.c_str(); JSClassRef schemaClass = JSClassCreate(&definition); schemaObjectConstructor = JSObjectMakeConstructor(ctx, schemaClass, nullptr); - value = Object::get_property(ctx, schemaObjectConstructor, "prototype"); - constructorPrototype = Value::to_object(ctx, value); + constructorPrototype = Object::validated_get_object(ctx, schemaObjectConstructor, "prototype"); JSObjectSetPrototype(ctx, constructorPrototype, RealmObjectClassConstructorPrototype); JSObjectSetPrototype(ctx, schemaObjectConstructor, RealmObjectClassConstructor); @@ -797,16 +807,20 @@ inline JSObjectRef ObjectWrap::create_instance_by_schema(JSContextRef // hot path. The constructor for this schema object is already cached. schemaObjectType = schemaObjects->at(schemaName); schemaObjectConstructor = schemaObjectType->constructor; + constructorPrototype = Object::validated_get_object(ctx, schemaObjectConstructor, "prototype"); } - instance = Function::construct(ctx, schemaObjectConstructor, 0, {}); + // Construct a plain object, setting the internal and prototype afterwards to avoid calling the constructor + instance = JSObjectMake(ctx, nullptr, nullptr); + JSObjectSetPrototype(ctx, instance, constructorPrototype); // save the internal object on the instance - set_internal_property(ctx, instance, internal); + set_internal(ctx, instance, internal); return instance; } else { + constructorPrototype = Object::validated_get_object(ctx, constructor, "prototype"); // creating a RealmObject with user defined constructor bool schemaExists = schemaObjects->count(schemaName); if (schemaExists) { @@ -825,14 +839,18 @@ inline JSObjectRef ObjectWrap::create_instance_by_schema(JSContextRef schemaObjectType = schemaObjects->at(schemaName); schemaObjectConstructor = schemaObjectType->constructor; - instance = Function::construct(ctx, schemaObjectConstructor, 0, {}); - set_internal_property(ctx, instance, internal); + // Construct a plain object, setting the internal and prototype afterwards to avoid calling the + // constructor + instance = JSObjectMake(ctx, nullptr, new ObjectWrap(internal)); + JSObjectSetPrototype(ctx, instance, constructorPrototype); + + // save the internal object on the instance + set_internal(ctx, instance, internal); + return instance; } schemaObjectConstructor = constructor; - value = Object::get_property(ctx, constructor, "prototype"); - constructorPrototype = Value::to_object(ctx, value); define_schema_properties(ctx, constructorPrototype, schema, false); @@ -866,8 +884,13 @@ inline JSObjectRef ObjectWrap::create_instance_by_schema(JSContextRef } } - // create the instance - instance = Function::construct(ctx, schemaObjectConstructor, 0, {}); + // Construct a plain object, setting the internal and prototype afterwards to avoid calling the constructor + instance = JSObjectMake(ctx, nullptr, new ObjectWrap(internal)); + JSObjectSetPrototype(ctx, instance, constructorPrototype); + + // save the internal object on the instance + set_internal(ctx, instance, internal); + bool instanceOfSchemaConstructor = JSValueIsInstanceOfConstructor(ctx, instance, schemaObjectConstructor, &exception); if (exception) { @@ -878,9 +901,6 @@ inline JSObjectRef ObjectWrap::create_instance_by_schema(JSContextRef throw jsc::Exception(ctx, "Realm object constructor must not return another value"); } - // save the internal object on the instance - set_internal_property(ctx, instance, internal); - schemaObjectType = new SchemaObjectType(); schemaObjects->emplace(schemaName, schemaObjectType); JSValueProtect(ctx, schemaObjectConstructor); diff --git a/src/jsc/jsc_object.hpp b/src/jsc/jsc_object.hpp index 319d280aa02..93f0fca1cc0 100644 --- a/src/jsc/jsc_object.hpp +++ b/src/jsc/jsc_object.hpp @@ -171,10 +171,9 @@ inline typename ClassType::Internal* jsc::Object::get_internal(JSContextRef ctx, template <> template -inline void jsc::Object::set_internal(JSContextRef ctx, const JSObjectRef& object, typename ClassType::Internal* ptr) +inline void jsc::Object::set_internal(JSContextRef ctx, JSObjectRef& object, typename ClassType::Internal* ptr) { - auto wrap = static_cast*>(JSObjectGetPrivate(object)); - *wrap = ptr; + jsc::ObjectWrap::set_internal(ctx, object, ptr); } template <> diff --git a/src/node/node_class.hpp b/src/node/node_class.hpp index 3f379c32bbc..8fdec3bf2bc 100644 --- a/src/node/node_class.hpp +++ b/src/node/node_class.hpp @@ -52,6 +52,7 @@ namespace node { Napi::FunctionReference ObjectGetOwnPropertyDescriptor; node::Protected ExternalSymbol; +Napi::FunctionReference ObjectCreate; Napi::FunctionReference ObjectSetPrototypeOf; Napi::FunctionReference GlobalProxy; Napi::FunctionReference FunctionBind; @@ -63,6 +64,11 @@ static void node_class_init(Napi::Env env) ObjectSetPrototypeOf = Napi::Persistent(setPrototypeOf); ObjectSetPrototypeOf.SuppressDestruct(); + auto create = env.Global().Get("Object").As().Get("create").As(); + ObjectCreate = Napi::Persistent(create); + ObjectCreate.SuppressDestruct(); + + auto getOwnPropertyDescriptor = env.Global().Get("Object").As().Get("getOwnPropertyDescriptor").As(); ObjectGetOwnPropertyDescriptor = Napi::Persistent(getOwnPropertyDescriptor); @@ -205,7 +211,7 @@ class ObjectWrap { static bool is_instance(Napi::Env env, const Napi::Object& object); static Internal* get_internal(Napi::Env env, const Napi::Object& object); - static void set_internal(Napi::Env env, const Napi::Object& object, Internal* data); + static void set_internal(Napi::Env env, Napi::Object& object, Internal* data); static Napi::Value constructor_callback(const Napi::CallbackInfo& info); static bool has_native_method(const std::string& name); @@ -1274,10 +1280,12 @@ Napi::Object ObjectWrap::create_instance_by_schema(Napi::Env env, Nap } Napi::External externalValue = Napi::External::New(env, internal, internal_finalizer); - instance = schemaObjectConstructor.New({}); + Napi::Object constructorPrototype = schemaObjectConstructor.Get("prototype").As(); + instance = ObjectCreate.Call({constructorPrototype}).As(); instance.Set(externalSymbol, externalValue); } else { + Napi::Object constructorPrototype = constructor.Get("prototype").As(); // creating a RealmObject with user defined constructor bool schemaExists = schemaObjects->count(schemaName); @@ -1297,8 +1305,7 @@ Napi::Object ObjectWrap::create_instance_by_schema(Napi::Env env, Nap if (schemaExists) { schemaObjectType = schemaObjects->at(schemaName); schemaObjectConstructor = schemaObjectType->constructor.Value(); - - instance = schemaObjectConstructor.New({}); + instance = ObjectCreate.Call({constructorPrototype}).As(); Napi::External externalValue = Napi::External::New(env, internal, internal_finalizer); instance.Set(externalSymbol, externalValue); @@ -1307,52 +1314,18 @@ Napi::Object ObjectWrap::create_instance_by_schema(Napi::Env env, Nap } schemaObjectConstructor = constructor; - Napi::Object constructorPrototype = constructor.Get("prototype").As(); // get all properties from the schema std::vector properties = create_napi_property_descriptors(env, constructorPrototype, schema, false /*redefine*/); - Napi::Function realmObjectClassConstructor = ObjectWrap::create_constructor(env); - bool isInstanceOfRealmObjectClass = constructorPrototype.InstanceOf(realmObjectClassConstructor); - - // Skip if the user defined constructor inherited the RealmObjectClass. All RealmObjectClass members are - // available already. - if (!isInstanceOfRealmObjectClass) { - // setup all RealmObjectClass methods to the prototype of the object - for (auto& pair : s_class.methods) { - // don't redefine if exists - if (!constructorPrototype.HasOwnProperty(pair.first)) { - auto descriptor = Napi::PropertyDescriptor::Function( - env, constructorPrototype, Napi::String::New(env, pair.first) /*name*/, &method_callback, - napi_default | realm::js::PropertyAttributes::DontEnum, (void*)pair.second /*callback*/); - properties.push_back(descriptor); - } - } - - for (auto& pair : s_class.properties) { - // don't redefine if exists - if (!constructorPrototype.HasOwnProperty(pair.first)) { - napi_property_attributes napi_attributes = - napi_default | - (realm::js::PropertyAttributes::DontEnum | realm::js::PropertyAttributes::DontDelete); - auto descriptor = Napi::PropertyDescriptor::Accessor( - Napi::String::New(env, pair.first) /*name*/, napi_attributes, - (void*)&pair.second /*callback*/); - properties.push_back(descriptor); - } - } - } - // define the properties on the prototype of the schema object constructor if (properties.size() > 0) { constructorPrototype.DefineProperties(properties); } - instance = schemaObjectConstructor.New({}); - if (!instance.InstanceOf(schemaObjectConstructor)) { - throw Napi::Error::New(env, "Realm object constructor must not return another value"); - } + instance = ObjectCreate.Call({constructorPrototype}).As(); + Napi::External externalValue = Napi::External::New(env, internal, internal_finalizer); instance.Set(externalSymbol, externalValue); @@ -1411,8 +1384,7 @@ typename ClassType::Internal* ObjectWrap::get_internal(Napi::Env env, } template -void ObjectWrap::set_internal(Napi::Env env, const Napi::Object& object, - typename ClassType::Internal* internal) +void ObjectWrap::set_internal(Napi::Env env, Napi::Object& object, typename ClassType::Internal* internal) { bool isRealmObjectClass = std::is_same>::value; if (isRealmObjectClass) { @@ -1439,11 +1411,6 @@ Napi::Value ObjectWrap::constructor_callback(const Napi::CallbackInfo return scope.Escape(env.Null()); // return a value to comply with Napi::FunctionCallback } else { - bool isRealmObjectClass = std::is_same>::value; - if (isRealmObjectClass) { - return scope.Escape(env.Null()); // return a value to comply with Napi::FunctionCallback - } - throw Napi::Error::New(env, "Illegal constructor"); } } diff --git a/src/node/node_object.hpp b/src/node/node_object.hpp index 0b2a9169341..adca80842ef 100644 --- a/src/node/node_object.hpp +++ b/src/node/node_object.hpp @@ -224,8 +224,7 @@ inline typename ClassType::Internal* node::Object::get_internal(Napi::Env env, c template <> template -inline void node::Object::set_internal(Napi::Env env, const Napi::Object& object, - typename ClassType::Internal* internal) +inline void node::Object::set_internal(Napi::Env env, Napi::Object& object, typename ClassType::Internal* internal) { return node::ObjectWrap::set_internal(env, object, internal); } diff --git a/src/node/node_return_value.hpp b/src/node/node_return_value.hpp index 2a143573c27..3b0ce263023 100644 --- a/src/node/node_return_value.hpp +++ b/src/node/node_return_value.hpp @@ -41,7 +41,7 @@ class ReturnValue { { } - Napi::Value ToValue() + Napi::Value ToValue() const { // guard check. env.Empty() values cause node to fail in obscure places, so return undefined instead if (m_value.IsEmpty()) { @@ -117,6 +117,11 @@ class ReturnValue { set_undefined(); } } + + operator Napi::Value() const + { + return ToValue(); + } }; } // namespace js diff --git a/tests/.lock b/tests/.lock new file mode 100644 index 00000000000..4e04ca75240 Binary files /dev/null and b/tests/.lock differ diff --git a/tests/js/list-tests.js b/tests/js/list-tests.js index 3f3103918ef..9eeeea8a170 100644 --- a/tests/js/list-tests.js +++ b/tests/js/list-tests.js @@ -1840,58 +1840,42 @@ module.exports = { testClassObjectCreation: function () { class TodoItem extends Realm.Object { - constructor(description) { - super(); - this.id = new ObjectId(); - this.description = description; - this.done = false; + constructor(realm, description) { + super(realm, { done: false, description }); } } TodoItem.schema = { name: "TodoItem", properties: { - id: "objectId", description: "string", done: { type: "bool", default: false }, deadline: "date?", }, - primaryKey: "id", }; class TodoList extends Realm.Object { - constructor(name) { - super(); - this.id = new ObjectId(); - this.name = name; - this.items = []; + constructor(realm, name) { + super(realm, { name }); } } TodoList.schema = { name: "TodoList", properties: { - id: "objectId", name: "string", items: "TodoItem[]", }, - primaryKey: "id", }; const realm = new Realm({ schema: [TodoList, TodoItem] }); realm.write(() => { const list = realm.create(TodoList, { - id: new ObjectId(), name: "MyTodoList", }); - TestCase.assertThrowsContaining(() => { - list.items.push(new TodoItem("Fix that bug")); - }, "Cannot reference a detached instance of Realm.Object"); - - TestCase.assertThrowsContaining(() => { - realm.create(TodoItem, new TodoItem("Fix that bug")); - }, "Cannot create an object from a detached Realm.Object instance"); + list.items.push(new TodoItem(realm, "Fix that bug")); + realm.create(TodoItem, new TodoItem(realm, "Fix that bug")); }); }, }; diff --git a/tests/js/realm-tests.js b/tests/js/realm-tests.js index 36642f3d3d0..43d5aa38219 100644 --- a/tests/js/realm-tests.js +++ b/tests/js/realm-tests.js @@ -88,8 +88,8 @@ module.exports = { let constructorCalled = false; //test class syntax support class Car extends Realm.Object { - constructor() { - super(); + constructor(realm) { + super(realm); constructorCalled = true; } } @@ -128,7 +128,7 @@ module.exports = { let realm = new Realm({ schema: [Car, Car2] }); realm.write(() => { let car = realm.create("Car", { make: "Audi", model: "A4", kilometers: 24 }); - TestCase.assertTrue(constructorCalled); + TestCase.assertFalse(constructorCalled); TestCase.assertEqual(car.make, "Audi"); TestCase.assertEqual(car.model, "A4"); TestCase.assertEqual(car.kilometers, 24); @@ -144,21 +144,21 @@ module.exports = { constructorCalled = false; let car1 = realm.create("Car", { make: "VW", model: "Touareg", kilometers: 13 }); - TestCase.assertTrue(constructorCalled); + TestCase.assertFalse(constructorCalled); TestCase.assertEqual(car1.make, "VW"); TestCase.assertEqual(car1.model, "Touareg"); TestCase.assertEqual(car1.kilometers, 13); TestCase.assertInstanceOf(car1, Realm.Object, "car1 not an instance of Realm.Object"); let car2 = realm.create("Car2", { make: "Audi", model: "A4", kilometers: 24 }); - TestCase.assertTrue(calledAsConstructor); + TestCase.assertFalse(calledAsConstructor); TestCase.assertEqual(car2.make, "Audi"); TestCase.assertEqual(car2.model, "A4"); TestCase.assertEqual(car2.kilometers, 24); TestCase.assertInstanceOf(car2, Realm.Object, "car2 not an instance of Realm.Object"); let car2_1 = realm.create("Car2", { make: "VW", model: "Touareg", kilometers: 13 }); - TestCase.assertTrue(calledAsConstructor); + TestCase.assertFalse(calledAsConstructor); TestCase.assertEqual(car2_1.make, "VW"); TestCase.assertEqual(car2_1.model, "Touareg"); TestCase.assertEqual(car2_1.kilometers, 13); @@ -1172,20 +1172,18 @@ module.exports = { let object = realm.create("CustomObject", { intCol: 1 }); TestCase.assertTrue(object instanceof CustomObject); TestCase.assertTrue(Object.getPrototypeOf(object) == CustomObject.prototype); - TestCase.assertEqual(customCreated, 1); + TestCase.assertEqual(customCreated, 0); // Should be able to create object by passing in constructor. object = realm.create(CustomObject, { intCol: 2 }); TestCase.assertTrue(object instanceof CustomObject); TestCase.assertTrue(Object.getPrototypeOf(object) == CustomObject.prototype); - TestCase.assertEqual(customCreated, 2); + TestCase.assertEqual(customCreated, 0); }); - TestCase.assertThrowsContaining(() => { - realm.write(() => { - realm.create("InvalidObject", { intCol: 1 }); - }); - }, "Realm object constructor must not return another value"); + realm.write(() => { + realm.create("InvalidObject", { intCol: 1 }); + }); // Only the original constructor should be valid. function InvalidCustomObject() {} diff --git a/tests/spec/helpers/reporters.js b/tests/spec/helpers/reporters.js index f7090c8e0a0..f9906c27b40 100644 --- a/tests/spec/helpers/reporters.js +++ b/tests/spec/helpers/reporters.js @@ -31,5 +31,8 @@ jasmine.getEnv().addReporter( spec: { displayPending: true, }, + summary: { + displayStacktrace: true, + }, }), ); diff --git a/types/index.d.ts b/types/index.d.ts index 20644d73bf8..cc0b277c125 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -282,7 +282,12 @@ declare namespace Realm { * Object * @see { @link https://realm.io/docs/javascript/latest/api/Realm.Object.html } */ - abstract class Object { + abstract class Object { + /** + * Creates a new object in the database. + */ + constructor(realm: Realm, values: Unmanaged); + /** * @returns An array of the names of the object's properties. */ @@ -996,10 +1001,10 @@ type ExtractPropertyNamesOfType = { }[keyof T]; /** - * Exchanges properties defined as Realm.List with an optional Array>. + * Exchanges properties defined as Realm.List with an optional Array>. */ type RealmListsRemappedModelPart = { - [K in ExtractPropertyNamesOfType>]?: T[K] extends Realm.List ? Array> : never + [K in ExtractPropertyNamesOfType>]?: T[K] extends Realm.List ? Array> : never } /** @@ -1026,7 +1031,7 @@ type RemappedRealmTypes = * Joins T stripped of all keys which value extends Realm.Collection and all inherited from Realm.Object, * with only the keys which value extends Realm.List, remapped as Arrays. */ -type RealmInsertionModel = OmittedRealmTypes & RemappedRealmTypes; +type Unmanaged = OmittedRealmTypes & RemappedRealmTypes; declare class Realm { static defaultPath: string; @@ -1131,8 +1136,8 @@ declare class Realm { * @param {Realm.UpdateMode} mode? If not provided, `Realm.UpdateMode.Never` is used. * @returns T & Realm.Object */ - create(type: string, properties: RealmInsertionModel, mode?: Realm.UpdateMode.Never): T & Realm.Object; - create(type: string, properties: Partial | Partial>, mode: Realm.UpdateMode.All | Realm.UpdateMode.Modified): T & Realm.Object; + create(type: string, properties: Unmanaged, mode?: Realm.UpdateMode.Never): T & Realm.Object; + create(type: string, properties: Partial | Partial>, mode: Realm.UpdateMode.All | Realm.UpdateMode.Modified): T & Realm.Object; /** * @param {Class} type @@ -1140,8 +1145,8 @@ declare class Realm { * @param {Realm.UpdateMode} mode? If not provided, `Realm.UpdateMode.Never` is used. * @returns T */ - create(type: {new(...arg: any[]): T; }, properties: RealmInsertionModel, mode?: Realm.UpdateMode.Never): T; - create(type: {new(...arg: any[]): T; }, properties: Partial | Partial>, mode: Realm.UpdateMode.All | Realm.UpdateMode.Modified): T; + create(type: {new(...arg: any[]): T; }, properties: Unmanaged, mode?: Realm.UpdateMode.Never): T; + create(type: {new(...arg: any[]): T; }, properties: Partial | Partial>, mode: Realm.UpdateMode.All | Realm.UpdateMode.Modified): T; /** * @param {Realm.Object|Realm.Object[]|Realm.List|Realm.Results|any} object @@ -1174,7 +1179,7 @@ declare class Realm { objectForPrimaryKey(type: {new(...arg: any[]): T; }, key: Realm.PrimaryKey): T | undefined; // Combined definitions - objectForPrimaryKey(type: string | {new(...arg: any[]): T; }, key: Realm.PrimaryKey): (T & Realm.Object) | undefined; + objectForPrimaryKey(type: string | {new(...arg: any[]): T; }, key: Realm.PrimaryKey): (T & Realm.Object) | undefined; /** * @param {string} type