Skip to content

Instantly share code, notes, and snippets.

@jonathan-beebe
Created November 7, 2024 14:53
Show Gist options
  • Save jonathan-beebe/cca6053be5374e0f34015fef723b0ff0 to your computer and use it in GitHub Desktop.
Save jonathan-beebe/cca6053be5374e0f34015fef723b0ff0 to your computer and use it in GitHub Desktop.
A WatermelonDB + TypeScript example showing typed relationships

WatermelonDB + Typescript

TLDR: Jump to Full Sample Coe to just see the final code.

I have a React + TypeScript + WatermelonDB project and want to:

  • Create a typesafe association between related models.
  • Observe a collection for changes so react components automitally re-render when items are added or removed.

I generally found WatermelonDB + TypeScript examples to be lacking, so I put together this brief example to show how I got this working.

My project uses:

  • node: 20
  • react: 19.0.0-rc-02c0e824-20241028
  • @nozbe/watermelondb: ^0.27.1
  • typescript: ^5

First, here are the schema and models without the relationships. I will show how the relationships get added below.

1. Defining the Schema

https://watermelondb.dev/docs/Schema

// ./schema.ts
import { appSchema, tableSchema } from '@nozbe/watermelondb'

export const schema = appSchema({
  version: 1,
  tables: [
    tableSchema({
      name: 'users',
      columns: [
        { name: 'name', type: 'string' },
        { name: 'email', type: 'string' },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' },
      ],
    }),
    tableSchema({
      name: 'appointments',
      columns: [
        { name: 'scheduled', type: 'number' },
        { name: 'status', type: 'string' },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' },
      ],
    }),
  ],
})

2. Defining the Models

https://watermelondb.dev/docs/Model

User

// ./User.ts
import { Model, Query } from '@nozbe/watermelondb'
import { readonly, text, date } from '@nozbe/watermelondb/decorators'

export default class User extends Model {
  static table = 'users'
  @text('name') name!: string
  @text('email') email!: string
  @readonly @date('created_at') createdAt!: number
  @readonly @date('updated_at') updatedAt!: number
}

Appointment

// ./Appointment.ts
import { Model } from '@nozbe/watermelondb'
import { readonly, text, date } from '@nozbe/watermelondb/decorators'

export default class Appointment extends Model {
  static table = 'appointments'
  @date('scheduled') scheduled!: number
  @text('status') status!: string
  @readonly @date('created_at') createdAt!: number
  @readonly @date('updated_at') updatedAt!: number
}

3. Add to the Adapter

https://watermelondb.dev/docs/Setup

Don’t forget to add your model classes to the adapter.

// ./adapter.ts
...
const database = new Database({
  adapter,
+  modelClasses: [Appointment, User]
})
...

4. Adding the Relationships

https://watermelondb.dev/docs/Relation

Now we begin the work of associating the appointments with their user. While this isn’t overly complicated, I find this isn’t a well documented workflow when using Typescript, so it took a while to work out this exact syntax. Hopefully you find this example helpful!

Let’s update the schema to add the relationship associating an appointment with a user.

    tableSchema({
      name: 'appointments',
      columns: [
+       { name: 'user_id', type: 'string', isIndexed: true },
        ...
      ],
    }),

Now let’s update the models so they know about the relationship.

User model

Notice the appointments field is typed as a Query.

import { Model, Query } from '@nozbe/watermelondb'
- import { readonly, text, date } from '@nozbe/watermelondb/decorators'
+ import { readonly, text, date, children } from '@nozbe/watermelondb/decorators'
+ import Appointment from './Appointment'

export default class User extends Model {
  static table = 'users'

+  static associations = {
+    appointments: { type: 'has_many', foreignKey: 'user_id' },
+  } as const
+
+  @children('appointments') appointments!: Query<Appointment>
  @text('name') name!: string
  @text('email') email!: string
  @readonly @date('created_at') createdAt!: number
  @readonly @date('updated_at') updatedAt!: number
}

The type Query<Appointment> is important to enable user.appointments.query() for fetching the associated collection of Appointment[].

Appointment model

Notice:

  • The user relationship is typed as a Relationship.
  • The ! tells us that user should always be defined. It is up to you to ensure you set this when creating a new appointment instance.
import { Model } from '@nozbe/watermelondb'
- import { readonly, text, date } from '@nozbe/watermelondb/decorators'
+ import { readonly, text, date, relation } from '@nozbe/watermelondb/decorators'
+ import User from './User'
+ import { Relation } from '@nozbe/watermelondb'

export default class Appointment extends Model {
  static table = 'appointments'

+  static associations = {
+    appointments: { type: 'belongs_to', key: 'user_id' },
+  } as const
+
+  @relation('users', 'user_id') user!: Relation<User>
  @date('scheduled') scheduled!: number
  @text('status') status!: string
  @readonly @date('created_at') createdAt!: number
  @readonly @date('updated_at') updatedAt!: number
}

5. How to Use the Relationships

Now, when you have fetched a user, you can query the associated collections like this.

const appointments = await user.appointments.fetch()

And the appointments object can be used with enhanced reactive components.

TODO: demonstrate using the relationship in an enhanced list component.

Full Sample Code

// ./schema.ts
import { appSchema, tableSchema } from '@nozbe/watermelondb'

export const schema = appSchema({
  version: 1,
  tables: [
    tableSchema({
      name: 'users',
      columns: [
        { name: 'name', type: 'string' },
        { name: 'email', type: 'string' },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' },
      ],
    }),
    tableSchema({
      name: 'appointments',
      columns: [
        { name: 'user_id', type: 'string', isIndexed: true },
        { name: 'scheduled', type: 'number' },
        { name: 'status', type: 'string' },
        { name: 'created_at', type: 'number' },
        { name: 'updated_at', type: 'number' },
      ],
    }),
  ],
})
// ./User.ts
import { Model, Query } from '@nozbe/watermelondb'
import { readonly, text, date, children } from '@nozbe/watermelondb/decorators'
import Appointment from './Appointment'

export default class User extends Model {
  static table = 'users'

  static associations = {
    appointments: { type: 'has_many', foreignKey: 'user_id' },
  } as const

  @children('appointments') appointments!: Query<Appointment>
  @text('name') name!: string
  @text('email') email!: string
  @readonly @date('created_at') createdAt!: number
  @readonly @date('updated_at') updatedAt!: number
}
// ./Appointment.ts
import { Model } from '@nozbe/watermelondb'
import { readonly, text, date, relation } from '@nozbe/watermelondb/decorators'
import User from './User'
import { Relation } from '@nozbe/watermelondb'

export default class Appointment extends Model {
  static table = 'appointments'

  static associations = {
    appointments: { type: 'belongs_to', key: 'user_id' },
  } as const

  @relation('users', 'user_id') user!: Relation<User>
  @date('scheduled') scheduled!: number
  @text('status') status!: string
  @readonly @date('created_at') createdAt!: number
  @readonly @date('updated_at') updatedAt!: number
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment