Model
A Model class is the application-facing shape of one table in your database.
In the library example used throughout the docs:
Bookmaps to thebookstableChaptermaps to thechapterstable
Before defining a model, make sure the matching table already exists in your Schema.
Start with the table name
import { Model } from '@hypertill/db'
export class Book extends Model {
static table = 'books'
}
Then include that model when you create the database:
const database = new Database({
adapter,
modelClasses: [Book],
})
Add associations
Relations are always defined on both sides.
export class Book extends Model {
static table = 'books'
static associations = {
chapters: { type: 'has_many', foreignKey: 'book_id' },
} as const
}
export class Chapter extends Model {
static table = 'chapters'
static associations = {
books: { type: 'belongs_to', key: 'book_id' },
} as const
}
- use
has_manyon the parent model - use
belongs_toon the child model - both sides point at the same foreign key column
Add fields
Fields map model properties to schema columns:
import { field, text } from '@hypertill/db/decorators'
export class Book extends Model {
static table = 'books'
@text('title') title!: string
@text('author') author!: string
@text('status') status!: string
}
@text vs @field
- use
@textfor user-entered strings like titles and names - use
@fieldfor numbers, booleans, ids, and plain scalar values
The column name stays explicit on purpose, because database columns are usually snake_case while application properties are usually camelCase.
Date fields
If you store timestamps and want a JavaScript Date in the model, use @date:
import { date } from '@hypertill/db/decorators'
class Book extends Model {
@date('last_opened_at') lastOpenedAt!: Date | null
}
That assumes your schema contains:
{ name: 'last_opened_at', type: 'number', isOptional: true }
Built-in timestamps
Every persisted Model reserves these getters:
createdAtupdatedAtdeletedAt
When the matching metadata columns exist, they return Date | null. With createPlatformAdapter() and the built-in SQLite/Loki adapters, those columns are normalized for you automatically, so you do not declare timestamp fields on each model.
const created = book.createdAt
const updated = book.updatedAt
const deleted = book.deletedAt
Avoid reusing those property names for unrelated fields.
To-one relations
Use @relation or @immutableRelation when a record belongs to another record:
import type { Relation } from '@hypertill/db'
import { relation, immutableRelation } from '@hypertill/db/decorators'
class Chapter extends Model {
@relation('books', 'book_id') book!: Relation<Book>
}
class ShelfEntry extends Model {
@immutableRelation('books', 'book_id') book!: Relation<Book>
}
Use @immutableRelation when the foreign key should never change after creation.
To-many relations with @children
Use @children to expose a query for related rows:
import { Query } from '@hypertill/db'
import { children } from '@hypertill/db/decorators'
class Book extends Model {
static associations = {
chapters: { type: 'has_many', foreignKey: 'book_id' },
} as const
@children('chapters') chapters!: Query<Chapter>
}
@children('chapters') does not load rows immediately. It gives you a Query, which you can fetch, observe, extend, or count.
Custom queries
You can build model-level queries with @lazy:
import { Q } from '@hypertill/db'
import { children, lazy } from '@hypertill/db/decorators'
class Book extends Model {
@children('chapters') chapters!: Query<Chapter>
@lazy introChapters = this.chapters.extend(
Q.where('title', Q.like('%Intro%')),
Q.sortBy('position', Q.asc),
)
}
Use @lazy for derived queries so they are created once and reused.
Writer methods
Put repeated mutations on the model itself:
import { writer } from '@hypertill/db/decorators'
class Book extends Model {
@writer async rename(title: string) {
await this.update((book) => {
book.title = title
})
}
}
Methods that change the database should be marked @writer.
A complete small example
import { Model, Query, Relation } from '@hypertill/db'
import { children, field, relation, text } from '@hypertill/db/decorators'
export class Book extends Model {
static table = 'books'
static associations = {
chapters: { type: 'has_many', foreignKey: 'book_id' },
} as const
@text('title') title!: string
@text('author') author!: string
@text('status') status!: string
@children('chapters') chapters!: Query<Chapter>
}
export class Chapter extends Model {
static table = 'chapters'
static associations = {
books: { type: 'belongs_to', key: 'book_id' },
} as const
@field('book_id') bookId!: string
@text('title') title!: string
@field('position') position!: number
@relation('books', 'book_id') book!: Relation<Book>
}
export const modelClasses = [Book, Chapter]