Skip to content

Commit

Permalink
Class models: Realm.Object#constructor (#4427)
Browse files Browse the repository at this point in the history
* Adding a cast to Value operator on ReturnType

* Adding integration tests of class constructors

* Made the test reporter print stack

* Implemented Realm.Object constructor

* Added node implementation and fixed legacy tests

* Read from this.constructor instead of new.target

* Fixing JSC implementation

* Updating types and tests

* Fixed Person and Dog constructors

* Updated @realm/react useObject + useQuery

* Updated types to fix an issue

* Dead code removal

* Updated tests to use default values

* Making the insertion types a bit more loose

* Adding documentation

* Renamed realm_object_object

* Made the constructor "values" required

* Renamed "RealmInsertionModel" to "Unmanged"

* Adding a note to the changelog

* Apply suggestions to docstrings

Co-authored-by: Kenneth Geisshirt <k@zigzak.net>

* Add docstring of set_internal and get_internal

* Expect 2 arguments on the C++ code as well

Co-authored-by: Kenneth Geisshirt <k@zigzak.net>
  • Loading branch information
kraenhansen and kneth committed Jun 8, 2022
1 parent 37d04d3 commit 3f6c5e5
Show file tree
Hide file tree
Showing 17 changed files with 262 additions and 151 deletions.
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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<T>` type to `Unmanaged<T>` 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<Person> {
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.
Expand Down
25 changes: 9 additions & 16 deletions integration-tests/tests/src/schemas/person-and-dogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Person>;
dogs!: Realm.Collection<Dog>;

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;
Expand All @@ -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;
Expand Down
48 changes: 47 additions & 1 deletion integration-tests/tests/src/tests/class-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
////////////////////////////////////////////////////////////////////////////

import { expect } from "chai";

import Realm from "realm";

import { openRealmBeforeEach } from "../hooks";

describe("Class models", () => {
describe("as schema element", () => {
beforeEach(() => {
Expand Down Expand Up @@ -72,4 +73,49 @@ describe("Class models", () => {
new Realm({ schema: [Person] });
});
});

describe("#constructor", () => {
type UnmanagedPerson = Partial<Person> & Pick<Person, "name">;
class Person extends Realm.Object<UnmanagedPerson> {
id!: Realm.BSON.ObjectId;
name!: string;
age!: number;
friends!: Realm.List<Person>;

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>("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);
});
});
});
});
5 changes: 4 additions & 1 deletion packages/realm-react/src/useObject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(type: string | { new (): T }, primaryKey: PrimaryKey): (T & Realm.Object) | null {
return function useObject<T>(
type: string | { new (...args: any): T },
primaryKey: PrimaryKey,
): (T & Realm.Object<T>) | null {
const realm = useRealm();

// Create a forceRerender function for the cachedObject to use as its updateCallback, so that
Expand Down
4 changes: 3 additions & 1 deletion packages/realm-react/src/useQuery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(type: string | ({ new (): T } & Realm.ObjectClass)): Realm.Results<T & Realm.Object> {
return function useQuery<T>(
type: string | ({ new (...args: any): T } & Realm.ObjectClass),
): Realm.Results<T & Realm.Object> {
const realm = useRealm();

// Create a forceRerender function for the cachedCollection to use as its updateCallback, so that
Expand Down
42 changes: 41 additions & 1 deletion src/js_realm_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ struct RealmObjectClass : ClassDefinition<T, realm::js::RealmObject<T>> {
using FunctionType = typename T::Function;
using ObjectType = typename T::Object;
using ValueType = typename T::Value;
using Context = js::Context<T>;
using String = js::String<T>;
using Value = js::Value<T>;
using Object = js::Object<T>;
Expand All @@ -72,6 +73,7 @@ struct RealmObjectClass : ClassDefinition<T, realm::js::RealmObject<T>> {

static ObjectType create_instance(ContextType, realm::js::RealmObject<T>);

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<String> get_property_names(ContextType, ObjectType);
Expand Down Expand Up @@ -164,6 +166,44 @@ typename T::Object RealmObjectClass<T>::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<T>::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 <typename T>
void RealmObjectClass<T>::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<ValueType> create_args{constructor, values};
Arguments create_arguments{ctx, create_args.size(), create_args.data()};
ReturnValue result{ctx};
RealmClass<T>::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<T, RealmObjectClass<T>>(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<T>(*realm_object);
set_internal<T, RealmObjectClass<T>>(ctx, this_object, new_realm_object);
}

template <typename T>
void RealmObjectClass<T>::get_property(ContextType ctx, ObjectType object, const String& property_name,
ReturnValue& return_value)
Expand Down Expand Up @@ -374,7 +414,7 @@ void RealmObjectClass<T>::add_listener(ContextType ctx, ObjectType this_object,
auto callback = Value::validated_to_function(ctx, args[0]);
Protected<FunctionType> protected_callback(ctx, callback);
Protected<ObjectType> protected_this(ctx, this_object);
Protected<typename T::GlobalContext> protected_ctx(Context<T>::get_global_context(ctx));
Protected<typename T::GlobalContext> protected_ctx(Context::get_global_context(ctx));

auto token = realm_object->add_notification_callback([=](CollectionChangeSet const& change_set,
std::exception_ptr exception) {
Expand Down
27 changes: 25 additions & 2 deletions src/js_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ struct Object {
static typename ClassType::Internal* get_internal(ContextType ctx, const ObjectType&);

template <typename ClassType>
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<app::AppError>&);
Expand Down Expand Up @@ -533,6 +533,8 @@ struct ReturnValue {
void set(uint32_t);
void set_null();
void set_undefined();

operator ValueType() const;
};

template <typename T, typename ClassType>
Expand All @@ -558,14 +560,35 @@ REALM_JS_INLINE typename T::Object create_instance_by_schema(typename T::Context
return Object<T>::template create_instance_by_schema<ClassType>(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 <typename T, typename ClassType>
REALM_JS_INLINE typename ClassType::Internal* get_internal(typename T::Context ctx, const typename T::Object& object)
{
return Object<T>::template get_internal<ClassType>(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 <typename T, typename ClassType>
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<T>::template set_internal<ClassType>(ctx, object, ptr);
Expand Down
Loading

0 comments on commit 3f6c5e5

Please sign in to comment.