realm / realm-js

Realm is a mobile database: an alternative to SQLite & key-value stores
https://realm.io
Apache License 2.0
5.8k stars 577 forks source link

Typescript types from Schema's. #6059

Open Acetyld opened 1 year ago

Acetyld commented 1 year ago

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 commented 1 year ago

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.

takameyer commented 1 year ago

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.

Acetyld commented 1 year ago

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 :  type, optional, toJSON, description , and  12  more

caleb-harrelson commented 1 year ago

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.

kneth commented 1 year ago

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.

Acetyld commented 1 year ago

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>;
};
stichingsd-vitrion commented 10 months ago

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?

Arahis commented 3 months ago

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