Skip to content

Models

Models in ReActive Record represent tables in your database. They provide an intuitive API for working with your data and managing relationships between different tables.

Defining Models

Models are defined in your database configuration through the models property:

typescript
const db = new ReactiveDatabase({
  models: {
    users: {
      schema: '++id, email, name',
      properties: ['id', 'email', 'name'],
      primaryKey: 'id',
      relationships: {
        posts: [HasMany, 'posts', 'user_id']
      }
    }
  }
})

Schema Definition

The schema property defines the IndexedDB table structure using Dexie's schema syntax:

  • ++ for auto-incrementing fields
  • & for unique fields
  • * for indexed fields
typescript
{
  schema: '++id, &email, *name, createdAt'
}

For more information about the Dexie schema syntax, please see the Dexie Documentation.

Warning

You should only define "Indexable Types" in your schema. Per Dexie's documentation:

Only properties of certain types can be indexed. This includes string, number, Date and Array but NOT boolean, null or undefined.

That means that, unlike in the properties definition, you do not need to define all of the properties of the model.

Properties

The properties array defines all fields that can be accessed on the model:

typescript
{
  properties: ['id', 'email', 'name', 'createdAt', 'updatedAt']
}

This includes both indexed and non-indexed fields. All properties listed here will be available on model instances.

Primary Key

The primaryKey property specifies which field serves as the model's unique identifier:

typescript
{
  primaryKey: 'id'
}

This field must be included in both the schema and properties definitions.

Constraints

The constraints property defines validation rules for the model using joi schemas. Use makeModelConstraints to create type-safe validation schemas:

typescript
import { joi, makeModelConstraints } from '@nhtio/web-re-active-record/constraints'

interface User {
  id: number
  email: string
  name: string
}

{
  constraints: makeModelConstraints<User>({
    id: joi.number().required(),
    email: joi.string().email().required(),
    name: joi.string().min(2).required()
  })
}

When validation fails, a ReactiveModelFailedConstraintsException is thrown.

Accessing Models

After configuration, models are accessed using the model() method:

typescript
const User = db.model('users')

Model Properties

Primary Key Access

The key property provides direct access to the model's primary key value:

typescript
const user = await User.find(1)
console.log(user.key) // Same as user[user.constructor.primaryKey]

Pending Changes

The pending property tracks unsaved changes to the model:

typescript
const user = new User({ name: 'John' })
console.log(user.pending) // { name: 'John' }

await user.save()
console.log(user.pending) // {}

user.name = 'Jane'
console.log(user.pending) // { name: 'Jane' }

String Representation

Models can be converted to a string format using toString(). This returns a string that starts with the model name followed by encrypted data:

typescript
const user = await User.find(1)
console.log(user.toString()) // 'ReactiveUser...[encrypted data]'

Type Safety

Models provide full TypeScript support through the type parameter passed to the ReactiveDatabase class. The type of the model is inferred from the ObjectMap provided when creating the database instance.

typescript
interface User {
  id: number
  email: string
  name: string
  createdAt: Date
  updatedAt: Date
}

const db = new ReactiveDatabase<{
  users: User
}>({
  // ... configuration
})

const User = db.model('users')

// The type of `User` is inferred from the `ObjectMap`
// so you get full type safety without passing a type parameter to `model()`

Relationships

Models can define relationships with other models through the relationships property:

typescript
{
  relationships: {
    // One-to-Many: User has many posts
    posts: [HasMany, 'posts', 'user_id'],
    
    // One-to-One: User has one profile
    profile: [HasOne, 'profiles', 'user_id'],
    
    // Many-to-Many: User has many roles through user_roles
    roles: [ManyToMany, 'roles', 'user_roles', 'user_id', 'role_id']
  }
}

For detailed information about relationships, see the Relationships Documentation.

Reactivity

Models are reactive by default, allowing you to track changes and respond to updates:

typescript
const user = await User.find(1)
if (user) {
  // Listen to model changes
  user.onChange((newState) => {
    console.log('User updated:', newState)
  })

  // Listen to specific property
  user.onPropertyChange('email', (newValue, oldValue) => {
    console.log('Email changed:', { newValue, oldValue })
  })
}

Note

Reactivity cannot be disabled. It is a core component of the ReActive Record ORM (hence the ReActive in the name).

Reactivity Methods

In order to subscribe and unsubscribe from changes, each reactive model offers the following methods:

MethodDescriptionExample Callback
onChangeSubscribe a listener to events emitted when any of the properties of the model change.(is, was) => void
onDeltaSubscribe a listener to events emitting the delta of the model when any of the properties change.({ prop: { is, was }}) => void
onPropertyChangeSubscribe a listener to events emitted when a specific property of the model changes.(is, was) => void
onceChangeSubscribe a listener to events emitted when any of the properties of the model change once.(is, was) => void
onceDeltaSubscribe a listener to events emitting the delta of the model when any of the properties change once.({ prop: { is, was }}) => void
oncePropertyChangeSubscribe a listener to events emitted when a specific property of the model changes once.(is, was) => void
offChangeUnsubscribe a listener or all listeners from events emitted when any of the properties of the model change.
offDeltaUnsubscribe a listener or all listeners from events emitted when the delta of the model changes.
offPropertyChangeUnsubscribe a listener or all listeners from events emitted when a specific property of the model changes.

Cross-Tab Synchronization

Changes to models are automatically synchronized across browser tabs, ensuring consistent state:

typescript
// Tab 1
const user = await User.find(1)
user.onChange(() => {
  console.log('User updated in another tab')
})

// Tab 2
const sameUser = await User.find(1)
await sameUser.save({ name: 'New Name' })
// The onChange handler in Tab 1 will be triggered

Next Steps