Keelv0.86.0
Docs / ORM

Keel's ORM is a compact active record over the query builder: a model is a class pointed at a table, and its rows come back as typed objects with methods. There's no mapper to configure and no separate schema layer — a model is the row plus behaviour. It runs on whatever connection you registered, so the same code works on Node and the edge.

import { Model } from "@shaferllc/keel/core";

class User extends Model {
  static table = "users";
  declare id: number;
  declare email: string;

  posts() { return this.hasMany(Post); }
}

const user = await User.find(1);
await user.posts();                 // relations are awaitable
if (await user.subscribed()) { /* … */ }

This page is the map; each capability has a deep-dive in Models.

What the ORM gives you

Area What you get Guide
CRUD find / all / create / save / update / delete, firstOrCreate, updateOrCreate Models → Reading/Writing
Casts boolean / int / json / date … columns round-trip as real JS types Models → Attribute casts
Mass assignment fillable / guarded allow/deny lists guard untrusted input Models → Mass assignment
Serialization hidden / visible / appends shape toJSON() Models → Serializing
Relationships hasOne / hasMany / belongsTo / belongsToMany + polymorphic morphOne / morphMany / morphTo Models → Relationships
Eager loading with("posts.comments") (nested), withCount, Model.load — no N+1 Models → Eager loading
Relationship queries whereHas / has / doesntHave Models → Querying relationships
Lifecycle events creating/saved/deleting/… hooks and observers, inherited by subclasses Models → Lifecycle events
Scopes global scopes (tenancy, published-only) + local scope methods Models → Query scopes
Soft deletes deleted_at, withTrashed / onlyTrashed / restore / forceDelete Models → Soft deletes

How it relates to the rest

  • The query builder is the layer underneath. Model.query() returns a model-aware builder, and everything an ORM query can't express (raw joins, aggregates, bulk writes) is one db() call away.
  • Migrations define the tables models read and write.
  • Factories & seeders generate model rows for tests and demos.
  • API resources turn models into a REST API; transformers control their serialized shape at the boundary.

When to drop down

The ORM is deliberately small — enough for CRUD, relationships, and the common query shapes without an ORM dependency. For a gnarly one-off report, reach for the query builder or a raw connection().select(sql); the model layer never gets in the way.

A worked example

A blog with authors and posts — enough to see CRUD, a relation, and eager loading together:

import { Model } from "@shaferllc/keel/core";

class Post extends Model {
  static table = "posts";
  static fillable = ["title", "body"];
  declare id: number;
  declare title: string;
  declare user_id: number;

  author() { return this.belongsTo(User); }
}

class User extends Model {
  static table = "users";
  declare id: number;
  declare email: string;

  posts() { return this.hasMany(Post); }
}

const ada = await User.create({ email: "ada@example.com" });
await ada.posts().create({ title: "Notes on engines", body: "…" });

const withPosts = await User.with("posts").where("id", ada.id).first();
for (const post of (withPosts as User & { posts: Post[] }).posts) {
  console.log(post.title);
}

For the full surface — casts, soft deletes, scopes, polymorphic relations — see Models.