Typed end to end
Row types, preloaded associations and select results are inferred from the schema classes. There is no generate step and no schema file to keep in sync.
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.
npm install egon-orm @egon-orm/postgres pgNode.js 22.12 or later. For SQLite use @egon-orm/sqlite with better-sqlite3, for MySQL @egon-orm/mysql with mysql2.
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.
User.changeset) filters and validates input, and declares that a duplicate email is a form error.from(User, "u")…) is an immutable value; preload loads associations without N+1 queries.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.