Docs / Models

Models

One file defines the table, the row type, the validator.

A model is one file. It defines the table, the validation, the TypeScript row type, and (optionally) the REST endpoints.

// app/models/post.ts
import { model, text, boolean, belongsTo } from '@hopak/core';

export default model('post', {
  title: text().required().min(3).max(200),
  content: text().required(),
  published: boolean().default(false),
  author: belongsTo('user'),
});

Field types

TypeNotes
text()Free-form string (use .min/.max/.pattern to constrain)
email()String with email-format validation
url()String with URL-format validation
phone()String — no built-in regex; add .pattern(...) for strict formats
number(), money()Numbers with min/max (money stored as real)
boolean()Scalar
date(), timestamp()Coerced from ISO strings; rejects invalid dates
enumOf('a', 'b')TypeScript literal union, DB enum
json<T>()Typed JSON column
belongsTo('user'), hasOne('profile'), hasMany('post')Relations
password(), secret(), token()Auto-excluded from JSON responses
file(), image()Stored as JSON metadata { url, mimeType, size, name? }

Modifiers

Chain on any field:

text().required().min(3).max(200).unique().index()
number().required().min(0).max(100).default(0)
text().pattern(/^[a-z]+$/)
date().default('now')
file().maxSize('5MB')
image().maxSize(2_097_152)   // bytes also accepted

Options

model('post', { /* fields */ }, {
  timestamps: true,   // add createdAt + updatedAt columns (default: true)
});

Lifecycle hooks

Added in 1.0

Hooks run around single-row writes, wherever they come from — CRUD endpoints or your own ctx.db.model(...) calls. before* hooks may return a replacement payload; after* hooks are for side effects:

// app/models/user.ts
import { model, text, email, password } from '@hopak/core';

export default model('user', {
  name: text().required(),
  email: email().required().unique(),
  password: password().required().min(8),
}, {
  hooks: {
    async beforeCreate(data) {
      return { ...data, password: await Bun.password.hash(String(data.password)) };
    },
    afterCreate(row) {
      console.log(`user #${row.id} signed up`);
    },
  },
});

Available: beforeCreate, afterCreate, beforeUpdate(data, id), afterUpdate, beforeDelete(id), afterDelete(id). Bulk operations (createMany / updateMany / deleteMany) skip hooks — they translate to a single SQL statement.

Hashing passwords? Use hashPassword from @hopak/auth rather than Bun.password.hash directly. It skips values that already are hashes, so a hook and credentialsSignup can both run without double-hashing — a double hash never matches at login.

Table names

Added in 1.0

The physical table is the pluralized model name (postposts), available as post.tableName. You need it whenever you write SQL by hand — see Database → Table names in raw SQL.