Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Class models: Realm.Object#constructor #4427

Merged
merged 22 commits into from
Mar 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b44fddf
Adding a cast to Value operator on ReturnType
kraenhansen Mar 16, 2022
93b322e
Adding integration tests of class constructors
kraenhansen Mar 16, 2022
3d2f618
Made the test reporter print stack
kraenhansen Mar 16, 2022
bfac8f3
Implemented Realm.Object constructor
kraenhansen Mar 16, 2022
e7b2cc5
Added node implementation and fixed legacy tests
kraenhansen Mar 16, 2022
e1b0a64
Read from this.constructor instead of new.target
kraenhansen Mar 17, 2022
dd3671b
Fixing JSC implementation
kraenhansen Mar 17, 2022
1834e77
Updating types and tests
kraenhansen Mar 17, 2022
a9d3167
Fixed Person and Dog constructors
kraenhansen Mar 18, 2022
2bc8460
Updated @realm/react useObject + useQuery
kraenhansen Mar 18, 2022
76e76fd
Updated types to fix an issue
kraenhansen Mar 18, 2022
ba06599
Dead code removal
kraenhansen Mar 18, 2022
0a7fbe6
Updated tests to use default values
kraenhansen Mar 24, 2022
05cdbe5
Making the insertion types a bit more loose
kraenhansen Mar 24, 2022
d065e00
Adding documentation
kraenhansen Mar 29, 2022
87ab310
Renamed realm_object_object
kraenhansen Mar 29, 2022
f4dedfb
Made the constructor "values" required
kraenhansen Mar 29, 2022
c08f63d
Renamed "RealmInsertionModel" to "Unmanged"
kraenhansen Mar 29, 2022
6f8cff2
Adding a note to the changelog
kraenhansen Mar 17, 2022
0c1ce10
Apply suggestions to docstrings
kraenhansen Mar 30, 2022
bca48d9
Add docstring of set_internal and get_internal
kraenhansen Mar 30, 2022
104ed35
Expect 2 arguments on the C++ code as well
kraenhansen Mar 30, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,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`.

### Enhancements
* None.
* 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);
});
```

### Fixed
* Fixed issue that could cause mangling of binary data on a roundtrip to/from the database ([#4278](https://github.com/realm/realm-js/issues/4278), since v10.1.4).
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", () => {
takameyer marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -47,7 +47,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),
takameyer marked this conversation as resolved.
Show resolved Hide resolved
): 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 @@ -60,6 +60,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 @@ -69,6 +70,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 @@ -162,6 +164,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.
fronck marked this conversation as resolved.
Show resolved Hide resolved
* @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>
fronck marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -372,7 +412,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*);
takameyer marked this conversation as resolved.
Show resolved Hide resolved

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