Skip to content

Egon ORMTyped data access for TypeScript

Schemas, changesets, composable queries and a repo, typed end to end with no code generation. For Postgres, SQLite and MySQL.

Work in progress

Egon ORM is under construction and nothing is published to npm yet.

Install ​

sh
npm install egon-orm @egon-orm/postgres pg

Node.js 22.12 or later. For SQLite use @egon-orm/sqlite with better-sqlite3, for MySQL @egon-orm/mysql with mysql2.

A first look ​

ts
import { postgres } from "@egon-orm/postgres";
import { belongsTo, cast, count, createRepo, from, hasMany, Schema, t, timestamps } from "egon-orm";

// Over two tables with serial (32-bit) keys, which t.integer() reads.
class User extends Schema("users", {
  id: t.integer().primaryKey().dbGenerated(),
  email: t.string(),
  name: t.string().nullable(),
  ...timestamps(),
}) {
  static posts = hasMany(() => Post, { foreignKey: "authorId" });

  static changeset(user: User | typeof User, params: unknown) {
    return cast(user, params, ["email", "name"]).validateRequired(["email"]).uniqueConstraint("email");
  }
}

class Post extends Schema("posts", {
  id: t.integer().primaryKey().dbGenerated(),
  authorId: t.integer(),
  title: t.string(),
}) {
  static author = belongsTo(() => User, { foreignKey: "authorId" });
}

const repo = createRepo({ adapter: postgres({ connectionString: process.env.DATABASE_URL }) });

const result = await repo.insert(User.changeset(User, { email: "ada@example.com" }));
if (!result.success) console.log(result.error.errorMessages()); // if the email is taken: { email: ["has already been taken"] }

const authors = await repo.all(
  from(User, "u")
    .where(({ u }) => u.name.isNotNull())
    .preload("posts"),
);
authors[0]?.posts[0]?.title; // string: the type knows "posts" was preloaded

const stats = await repo.all(
  from(User, "u")
    .leftJoinAssoc("u", "posts", "p")
    .groupBy(({ u }) => u.id)
    .select(({ u, p }) => ({ email: u.email, posts: count(p.id) })),
); // { email: string; posts: number }[]

The tables are not shown: Getting started creates them with a migration.

  • A schema class describes a table; its instances are rows.
  • A changeset (User.changeset) filters and validates input, and declares that a duplicate email is a form error.
  • A query (from(User, "u")…) is an immutable value; preload loads associations without N+1 queries.
  • The repo runs queries and writes changesets. insert returns { success: true, data } or { success: false, error }.

Next: Getting started in five minutes, or the Tutorial in thirty. Coming from another ORM? Read the page for Prisma, Drizzle or Sequelize.