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

Typescript types from Schema's.

Open Acetyld opened this issue 2 years ago • 8 comments

How frequently does the bug occur?

Always

Description

Lets say i do this:

  const createFakeTask = () => {
    realm.write(() => {
      const fakeTask: Task = {
        id: Math.floor(Math.random() * 10000) + 1,
        createdAt: new Date(),
        description: faker.lorem.sentence(),
        status: faker.helpers.enumValue(TaskStatusEnum),
        active: faker.helpers.enumValue(ActiveEnum),
        inside: faker.helpers.enumValue(InsideEnum),
        priority: faker.helpers.enumValue(PriorityEnum),
        position: faker.number.int(),
        quantity: faker.number.int(),
        deadline: faker.date.future(),
        updatedAt: faker.date.recent(),
        deletedAt: faker.date.past(),
        timeSensitive: faker.datatype.boolean(),
        blocking: faker.datatype.boolean(),
        hasDeadline: faker.datatype.boolean(),
      };

      realm.create(Task, fakeTask);
    });
  };

And my entity is

import { Department } from '@database/Department';
import { Location } from '@database/Location';
import { Mean } from '@database/Mean';
import { MediaObject } from '@database/MediaObject';
import { Objects } from '@database/Objects';
import { Report } from '@database/Report';
import { Reservation } from '@database/Reservation';
import { Sort } from '@database/Sort';
import { Space } from '@database/Space';
import { User } from '@database/User';
import { ActiveEnum, InsideEnum, PriorityEnum } from '@database/database.enums';
import { TaskStatusEnum } from '@features/tasks/task.interface';
import { Object } from 'realm';

export class Task extends Object<Task, 'id' | 'createdAt'> {
id!: number;
'@id'?: string;
description?: string;
status?: TaskStatusEnum;
active?: ActiveEnum;
inside?: InsideEnum;
priority?: PriorityEnum;
position?: number;
quantity?: number;
deadline?: Date;
createdAt!: Date;
updatedAt?: Date;
deletedAt?: Date;
object?: Objects;
sort?: Sort;
space?: Space;
mean?: Mean;
createdBy?: User;
reservation?: Reservation;
timeSensitive?: boolean;
blocking?: boolean;
hasDeadline?: boolean;
mediaObjects!: Realm.List<MediaObject>;
departments!: Realm.List<Department>;
teams!: Realm.List<Department>;
users!: Realm.List<Department>;
report?: Report;
type?: number;
openedAt?: Date;
location?: Location;

static schema = {
  name: 'Task',
  primaryKey: 'id',
  properties: {
    id: { type: 'int', indexed: true },
    '@id': 'string?',
    description: { type: 'string?', indexed: true },
    status: { type: 'int', default: TaskStatusEnum.Open, indexed: true },
    active: 'int?',
    inside: { type: 'int?', default: null, indexed: true },
    priority: { type: 'int?', default: PriorityEnum.None, indexed: true },
    position: 'int?',
    quantity: 'int?',
    deadline: 'date?',
    createdAt: 'date',
    updatedAt: { type: 'date?', indexed: true },
    deletedAt: 'date?',
    object: 'Object?',
    sort: 'Sort?',
    space: 'Space?',
    mean: 'Mean?',
    createdBy: 'User?',
    reservation: 'Reservation?',
    timeSensitive: 'bool?',
    blocking: { type: 'bool', default: false },
    hasDeadline: { type: 'bool?', default: false, indexed: true },
    mediaObjects: { type: 'MediaObject[]', default: [] },
    departments: { type: 'Department[]', default: [] },
    teams: { type: 'Team[]', default: [] },
    users: { type: 'User[]', default: [] },
    report: 'string?',
    type: 'int?',
    openedAt: 'date?',
    location: 'Location',
    __isDeleted: { type: 'bool', default: false, indexed: true },
    __isSynced: { type: 'bool', default: false, indexed: true },
  },
};
}

If i want some good typescript auto complete i need to prefix the type with :Task but this is causing another issue, i wants to me implement stuff like: linkingObjects, keys, toJson, entries, isValid, etc...

In another place i manged to find a work around

type PlainUser = Omit<
  User,
  | keyof Realm.Object
  | 'departments'
  | 'teams'
  | 'managerTeams'
  | 'managerDepartments'
  | 'locations'
> & {
  departments?: Omit<Department, keyof Realm.Object>[];
  teams?: Omit<Team, keyof Realm.Object>[];
  managerDepartments?: Omit<Department, keyof Realm.Object>[];
  managerTeams?: Omit<Team, keyof Realm.Object>[];
  locations?: Omit<Location, keyof Realm.Object>[];
};

This will bassicly bypass the need of the realm.object stuff. I also tried using the babel transformer but as discussed in earlier of my post this caused more good then harm.

So am i doing something wrong? I just want good auto complete on realm.write, if i have a api response with TaskReponseItem and i do a ream.write i want to clearly know what i am doing wrong if fields dont match types.

Stacktrace & log output

No response

Can you reproduce the bug?

Always

Reproduction Steps

No response

Version

^11.10.1

What services are you using?

Local Database only

Are you using encryption?

No

Platform OS and version(s)

Newest expo 49

Build environment

Which debugger for React Native: ..

Cocoapods version

No response

Acetyld avatar Aug 11 '23 08:08 Acetyld

EDIT:

To clearify my issue, the goal is to make a helper function called "add" in the entity that will handle add logic, this is the reason why i need to have a "clean" typescript type of the Task Schema.

Acetyld avatar Aug 11 '23 08:08 Acetyld

We do have a type that isn't exported, but it's possible for you to duplicate it. It's the same type we use for the input for realm.create. Let me know if that helps.

takameyer avatar Aug 11 '23 09:08 takameyer

Lovely, altough it seems this is not working with nested relation

import { iriToId } from '@composables/utils/iriToId';
import { Department } from '@database/Department';
import { Location } from '@database/Location';
import { Team } from '@database/Team';
import { ActiveEnum } from '@database/database.enums';
import { Unmanaged } from '@database/realm.types';
import {
  UserGet,
  UserLocationGet,
  UserManagerGet,
  UserTeamGet,
} from '@features/user/user.interface';
import Realm from 'realm';

type PlainUser = Omit<
  User,
  | keyof Realm.Object
  | 'departments'
  | 'teams'
  | 'managerTeams'
  | 'managerDepartments'
  | 'locations'
> & {
  departments?: Omit<Department, keyof Realm.Object>[];
  teams?: Omit<Team, keyof Realm.Object>[];
  managerDepartments?: Omit<Department, keyof Realm.Object>[];
  managerTeams?: Omit<Team, keyof Realm.Object>[];
  locations?: Omit<Location, keyof Realm.Object>[];
};

export class User extends Realm.Object<User, 'id'> {
  id!: number;
  '@id'?: string;
  fullName?: string;
  avatar?: string;
  username?: string;
  email?: string;
  roles?: string[];
  active?: ActiveEnum;
  createdAt?: Date;
  updatedAt?: Date;
  deletedAt?: Date;
  departments?: Realm.List<Department>;
  teams?: Realm.List<Team>;
  managerDepartments?: Realm.List<Department>;
  managerTeams?: Realm.List<Team>;
  locations?: Realm.List<Location>;
  __isDeleted?: boolean;
  __isSynced?: boolean;

  static schema = {
    name: 'User',
    primaryKey: 'id',
    properties: {
      id: 'int',
      '@id': 'string?',
      fullName: { type: 'string?', indexed: true },
      email: 'string?',
      avatar: 'string?',
      username: 'string?',
      roles: 'string?[]',
      active: 'int?',
      createdAt: 'date?',
      updatedAt: 'date?',
      deletedAt: 'date?',
      departments: { type: 'Department[]', default: [] },
      teams: { type: 'Team[]', default: [] },
      managerDepartments: { type: 'Department[]', default: [] },
      managerTeams: { type: 'Team[]', default: [] },
      locations: { type: 'Location[]' },
      __isDeleted: { type: 'bool', default: false, indexed: true },
      __isSynced: { type: 'bool', default: false, indexed: true },
    },
  };

  static add(realm: Realm, userResponse: UserGet): Unmanaged<User> {
    const user: Unmanaged<User> = {
      id: userResponse.id,
      '@id': userResponse['@id'],
      fullName: userResponse.fullName,
      email: userResponse.email,
      username: userResponse.username,
      roles: userResponse.roles || [],
      active: userResponse.active,
      createdAt: new Date(userResponse.createdAt),
      updatedAt: new Date(userResponse.updatedAt),
      deletedAt: userResponse.deletedAt
        ? new Date(userResponse.deletedAt)
        : undefined,
      avatar: userResponse.avatar || undefined,
      __isSynced: true,
    };

    user.departments = mapUserRelation(realm, userResponse.departments || []);
    // user.managerDepartments = mapUserRelation(
    //   realm,
    //   userResponse.managerDepartments || [],
    // );
    // user.teams = mapUserRelation(realm, userResponse.teams || []);
    // user.managerTeams = mapUserRelation(realm, userResponse.managerTeams || []);
    // user.locations = mapUserRelation(realm, userResponse.locations || []);

    return user;
  }
}

const mapUserRelation = (
  realm: Realm,
  items: UserManagerGet[] | UserTeamGet[] | UserLocationGet[],
) => {
  return items.map(item => {
    const iri = item['@id'];
    const id = iriToId(iri);
    const entity = realm.objectForPrimaryKey(Department, id);
    if (!entity) {
      return { id };
    }
    return entity;
  });
};


The user.deparments still gives: TS2740: Type ((Department & Object<Department, never>) | { id: number; })[] is missing the following properties from type  List<Department> :  type, optional, toJSON, description , and  12  more

Acetyld avatar Aug 11 '23 09:08 Acetyld

Extending Realm.Object is what causes this. I hate it too, it makes it a PITA to work with the plain objects or copy one object into another collection (and keep types sane). What I do instead is keep a bare interface and a separate schema:

export interface User {
  id!: number
  username?: string
  email?: string
  departments: Array<Department>
  // etc.
}

export const UserSchema = {
  name: "User",
  properties: {
    id: "int",
    username: "string?",
    email: "string?",
    departments: "Department[]"
    // etc.
  }  
}

That allows you to use plain old typescript objects for your models. You pass UserSchema.name instead of the class when doing your queries and the queries come back with the type of User & Realm.Object. Doing a .toJSON on those will give you the type without the Realm.Object, but may require casting as User and is certainly inefficient if you're doing it on a set of data instead of a single object.

I hope they're able to get rid of the requirement to extend Realm.Object in Realm 12 as it certainly adds complications in my project.

caleb-harrelson avatar Aug 11 '23 19:08 caleb-harrelson

able to get rid of the requirement to extend Realm.Object in Realm 12

We don't intent to change it in v12. It is a complete rewrite of the code base, and we don't want to change behavior AND code base at the same time (so we can avoid going insane). But we plan to change it in v13.

kneth avatar Aug 14 '23 13:08 kneth

Yhea the method provided above is a bit hard to test, bcs no types are exported, i put somet ime into copying from remote, and trying to get it to work, without success so i stuk to me:

type PlainMean = Omit<
  Mean,
  keyof Realm.Object | 'space' | 'object' | 'location'
> & {
  space?: Omit<Space, keyof Realm.Object>;
  object?: Omit<Objects, keyof Realm.Object>;
  location?: Omit<Location, keyof Realm.Object>;
};

Acetyld avatar Aug 23 '23 17:08 Acetyld

able to get rid of the requirement to extend Realm.Object in Realm 12

We don't intent to change it in v12. It is a complete rewrite of the code base, and we don't want to change behavior AND code base at the same time (so we can avoid going insane). But we plan to change it in v13.

Is there already any upcoming update above above?

stichingsd-vitrion avatar Jan 10 '24 08:01 stichingsd-vitrion

Hi! I faced the same issue! Maybe someone can advise a library (if it exists), one like OpenApi that generates types from schemas

Arahis avatar Aug 07 '24 12:08 Arahis