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:
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
{
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:
{
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:
{
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:
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:
const User = db.model('users')
Model Properties
Primary Key Access
The key
property provides direct access to the model's primary key value:
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:
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:
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.
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:
{
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:
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:
Method | Description | Example Callback |
---|---|---|
onChange | Subscribe a listener to events emitted when any of the properties of the model change. | (is, was) => void |
onDelta | Subscribe a listener to events emitting the delta of the model when any of the properties change. | ({ prop: { is, was }}) => void |
onPropertyChange | Subscribe a listener to events emitted when a specific property of the model changes. | (is, was) => void |
onceChange | Subscribe a listener to events emitted when any of the properties of the model change once. | (is, was) => void |
onceDelta | Subscribe a listener to events emitting the delta of the model when any of the properties change once. | ({ prop: { is, was }}) => void |
oncePropertyChange | Subscribe a listener to events emitted when a specific property of the model changes once. | (is, was) => void |
offChange | Unsubscribe a listener or all listeners from events emitted when any of the properties of the model change. | |
offDelta | Unsubscribe a listener or all listeners from events emitted when the delta of the model changes. | |
offPropertyChange | Unsubscribe 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:
// 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
- CRUD Operations: Learn how to create, read, update, and delete records
- Querying: Advanced query building and filtering
- Relationships: Working with model relationships
- TypeScript: Type safety and inference