realm-js icon indicating copy to clipboard operation
realm-js copied to clipboard

Class-based Models do not Support Embedded Objects in Typescript

Open takameyer opened this issue 2 years ago • 9 comments
trafficstars

How frequently does the bug occur?

Seen once

Description

Currently, it is not possible to add an new embedded object to a model without casting a plain javascript object as the class based model. This is due to not being able to call new on an embedded object, even when setting it directly to its parent class. For example:

realm.write(() => {
  item.child = new Child(realm, {param1: "foo"});
});

This will throw an error that it's not possible to create an embedded object. Also:

realm.write(() => {
  item.child = { param1: "foo"};
});

Will be successful in runtime, but it will show a typescript error.

Currently the only work around is:

realm.write(() => {
  item.child = { param1: "foo" } as Child;
});

Where Child is the linked object of child on item and extends Realm.Object<Child>.

Stacktrace & log output

No response

Can you reproduce the bug?

Yes, always

Reproduction Steps

No response

Version

11.0.0

What SDK flavour are you using?

Local Database only

Are you using encryption?

No, not using encryption

Platform OS and version(s)

All

Build environment

Which debugger for React Native: ..

Cocoapods version

No response

takameyer avatar Dec 02 '22 17:12 takameyer

any updates?

PrgrmmrOleg avatar Jan 25 '23 17:01 PrgrmmrOleg

Any update on this ?

Seeing this on 11.8.0.

const createParent = (): void => {
        realm.write(() => {
            //Type '{ label: string; }' is missing the following properties from type 'Child': keys, entries, toJSON, isValid, and 8 more.
            realm.create<Parent>(Parent, { subSchema: { label: 'label' } }); // <== this is the problem
        });
    };

Shemas :

export class Parent extends Realm.Object<Parent> {
    static schema: Realm.ObjectSchema = {
        name: 'Parent',
        primaryKey: '_id',
        properties: {
            _id: { type: 'objectId', default: () => new Realm.BSON.ObjectId() },
            subSchema: 'Child'
        }
    };
    _id!: Realm.BSON.ObjectId;
    subSchema!: Child;
}
export class Child extends Realm.Object<Child, 'label'> {
    static schema: Realm.ObjectSchema = {
        name: 'Child',
        embedded: true,
        properties: {
            label: 'string'
        }
    };
    label!: string;
}

Thanks !

lclnrd avatar Apr 25 '23 16:04 lclnrd

Any updates or advice from the team on this? I am also getting the same Type error of ...keys, entries, toJSON, isValid, and 8 more... and am wondering what the recommended approach is for typing embedded objects before writing them into a new object assuming there is a way other than using an as assertion? Thanks!

GitMurf avatar Aug 24 '23 13:08 GitMurf

Still an issue in version 12.3

szkieb avatar Nov 23 '23 13:11 szkieb

Any updates on this? Same problem here. This is a pretty common scenario. Spring would like to use this feature

gfrancischini avatar Jan 12 '24 19:01 gfrancischini

Hey, adding a little bit more context here. Not being able to use class based models to add embedded objects also kills the use of setter and getters. We are trying to implement something like this in our data models:

class Model extends Realm.object {
    myField: string
 
    setMyField(value: string) {
       this.myField = value;
       // ... other validations or updates
   }
}


On a normal class this pattern of having a setter that will validate the value and might also set other fields work quite well.

But if we know expand this to embedded objects or list we have this scenario:

class Model extends Realm.object {
    _myField: string;  // this is mapped as a realm field on the static schema
    _embModel: EmbeddedModel | null;
    set myField(value: string) {
       this.myField = value;
       // ... other validations or updates
   }
}

class EmbeddedModel extends Realm.object {
    _embField: number  // this is mapped as a realm field on the static schema

    set embField(value: number) {
       this._embField = value;
       // ... other validations or updates
   }

}

The setter embField will never get triggered because when we are creating the embedded field we don't create the class instance itself, we sent a dummy wrong object

realm.write(() => {
    const model = new Model(realm)
    model.embField = {
      // we cannot create this object with new constructor
    }
});


So we basically need the ability to user our own constructor, setter and getters on this kind of objects

gfrancischini avatar May 15 '24 17:05 gfrancischini

@kneth sorry to tag you, but we are really desperate about this issue. A lot of places where we could have setter validations are being duplicated.

Is there any workaround to be able to use setter to create the realm elements?

gfrancischini avatar May 21 '24 21:05 gfrancischini

@gfrancischini Sorry for the late reply.

Currently it is not possible to use embedded objects together with class-based models. The constructor of our class-based models takes a Realm instance as argument, which means that the constructor will create a managed/Realm object. For embedded objects, creating the object without a parent it by definition not possible, and this issue is about that.

A workaround is to not use classes which extends Realm.Object. A small example is:

import Realm from "realm";

class Contact {
    name: string;
    address?: Address;

    constructor(name: string, address?: Address) {
        this.name = name;
        if (address) {
            this.address = address;
        }
    }

    static schema: Realm.ObjectSchema = {
        name: "Contact",
        properties: {
            name: "string",
            address: "Address",
        },
    };
}

class Address {
    street: string;
    city: string;

    constructor(street: string, city: string) {
        this.street = street;
        this.city = city;
    }

    static schema: Realm.ObjectSchema = {
        name: "Address",
        embedded: true,
        properties: {
            street: "string",
            city: "string",
        },
    };
}

Realm.deleteFile({});
let realm = new Realm({ schema: [Contact.schema, Address.schema] });
realm.write(() => {
    const parkvej = new Address("Grøndals Parkvej 2A", "Vanløse");
    const alice = new Contact("Alice", parkvej);
    realm.create(Contact.schema.name, alice);

    const griffensfeldsgade = new Address("Griffenfeldsgade 14B", "København N");
    const bob = new Contact("Bob", griffensfeldsgade);
    realm.create(Contact.schema.name, bob);
});

realm.objects<Contact>(Contact.schema.name).forEach((c) => {
    console.log(`${c.name} at ${c.address?.street}, ${c.address?.city}`);
});

kneth avatar May 22 '24 14:05 kneth

Hey thanks, when using that way what are the downsides of not extending from Realm.object?

is purely the Contact.schema.name instead of just Contact in the realm.create? And also not having ts visibility on exposed methods from the realm.object?

gfrancischini avatar May 22 '24 16:05 gfrancischini