npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

peta-orm

v0.6.0

Published

ORM for Bun, built on Kysely

Readme

peta-orm

npm version TypeScript License

A feature-rich ORM for Bun, built on Kysely with ArkType validation. ActiveRecord-style models, typed relations, lazy/eager loading, lifecycle hooks, soft deletes, timestamps, casting, serialization control, global scopes, polymorphic relations, pagination, collections, and more — all fully typed end-to-end.

const user = await User.insert({ name: "Alice", email: "[email protected]" })
const posts = await User.relations.posts.query(user).where("published", true).execute()
const page = await Post.query().with("author").orderBy("id", "asc").paginate(1, 20)

Quick Start

bun add peta-orm arktype kysely @libsql/kysely-libsql @libsql/client

Simple setup (examples, scripts)

import { createClient } from "@libsql/client"
import { LibsqlDialect } from "@libsql/kysely-libsql"
import { createORM, defineModel, t } from "peta-orm"

const User = defineModel("users", {
  columns: { id: t.integer().primaryKey(), name: t.string(255), email: t.text().unique() },
})

// Eager init — fine for scripts, one-off tasks
const client = createClient({ url: "file::memory:?cache=shared" })
await client.execute(
  "CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE)",
)

const orm = createORM({ dialect: new LibsqlDialect({ client }), models: { User } })

const user = await User.insert({ name: "Alice", email: "[email protected]" })

Production setup (apps, servers) — no module-level side effects

Module-level side effects (database connections, schema init, ORM setup at import time) cause problems with testing, HMR, and error recovery. Use createDb() for lazy, safe initialization:

import { createClient } from "@libsql/client"
import { LibsqlDialect } from "@libsql/kysely-libsql"
import { createDb, createORM, defineModel, t } from "peta-orm"

const User = defineModel("users", {
  columns: { id: t.integer().primaryKey(), name: t.string(255), email: t.text().unique() },
})

async function setup() {
  const client = createClient({ url: "file:my-app.db" })
  await client.execute(
    "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT NOT NULL UNIQUE)",
  )
  const orm = createORM({ dialect: new LibsqlDialect({ client }) })
  orm.registerAll(User)
  return orm
}

/** Lazy singleton — first call creates the connection, subsequent calls reuse it. */
export const db = createDb(setup)

// In route handlers:
// const orm = await db()
// const users = await User.query().execute()

The factory function runs once on the first await db() call. Importing models has zero side effects — no connection, no schema init, no unhandled promises.

[!TIP] For an existing Kysely instance (e.g. from a migration runner), pass it via the kysely config option:

const orm = createORM({ kysely: existingKysely })

[!TIP] See the 32 runnable examples for every feature. Run them with bun run examples/XX-*.ts.


Why peta-orm?

| Feature | Raw Kysely | peta-orm | |---------|-----------|----------| | Validation | Manual | Automatic from column definitions via ArkType | | Models | Row types only | Class instances with $save(), $delete(), $reload() | | Relations | Manual JOINs | Declarative hasMany, belongsTo, hasOne, manyToMany | | Eager loading | Manual batch | .with("posts.author") — one line, batched queries | | Hooks | — | beforeCreate, afterUpdate, beforeDelete, etc. | | Soft deletes | — | withTrashed(), onlyTrashed(), $restore(), $forceDelete() | | Casting | — | $casts: { meta: "json", flags: "boolean" } | | Serialization | — | $hidden, $visible, $appends, accessors | | Pagination | Manual offset/limit | .paginate(1, 20) — returns { data, total, perPage, ... } | | Error handling | Raw driver codes | DatabaseError with UNIQUE_CONSTRAINT across dialects | | Conditional queries | Manual if/else | .when(condition, qb => ...), .unless(condition, qb => ...) | | Global scopes | — | addGlobalScope("active", qb => ...) | | Polymorphic relations | — | morphTo, morphMany, morphOne |


Features

Column Types & Validation

Column definitions double as validation schemas — no separate validation step needed.


const User = defineModel("users", {
  columns: {
    id: t.integer().primaryKey(),
    name: t.string(255).min(2),                                     // min length
    email: t.text().email().unique(),                               // email format + unique
    age: t.integer().nullable().min(0).max(150).default(0),
    role: t.enum("admin", "user").default("user"),
    score: t.double().nullable(),
    ...t.timestamps(),                                              // createdAt, updatedAt
  },
})

const Post = defineModel("posts", {
  columns: {
    id: t.integer().primaryKey(),
    userId: t.integer(),
    title: t.string(255),
    slug: t.string().unique(),
    published: t.boolean().default(false),
  },
})

Relations & Eager Loading

const User = defineModel("users", {
  columns: { id: t.integer().primaryKey(), name: t.string(255) },
  relations: {
    posts: hasMany(() => Post, { foreignKey: "userId" }),
    profile: hasOne(() => Profile, { foreignKey: "userId" }),
  },
})

// Eager load with dot notation
const users = await User.query().with("posts.author").execute()

// Lazy load after fetch
await user.$load("posts")

// Relation query
const posts = await User.relations.posts.query(user).where("published", true).execute()

// Existence filters
const authors = await User.query().has("posts").execute()
const active = await User.query().whereHas("posts", (q) => q.where("published", true)).execute()

Polymorphic Relations

const Comment = defineModel("comments", {
  columns: { id: t.integer().primaryKey(), body: t.text() },
  relations: {
    subject: morphTo(() => ({
      Post: { foreignKey: "postId" },
      Article: { foreignKey: "articleId" },
    })),
  },
})

const Post = defineModel("posts", {
  columns: { id: t.integer().primaryKey(), title: t.string(255) },
  relations: { comments: morphMany(() => Comment, { morphType: "post" }) },
})

CRUD & Pagination

// Insert
const user = await User.insert({ name: "Alice", email: "[email protected]" })

// Find
const found = await User.find(1)
const first = await User.query().where("email", "like", "%@b.com").first()

// Update
user.set("name", "Alice Updated")
await user.$save()
await User.update(1, { name: "Alice Updated" })

// Delete
await user.$delete()
await User.delete(1)

// Paginate
const page = await Post.query().orderBy("id", "asc").paginate(1, 20)
// → { data: Post[], total: 30, perPage: 20, currentPage: 1, lastPage: 2, hasMorePages: true }

Hooks & Timestamps

User.on("beforeCreate", (user) => { user.email = user.email.toLowerCase() })
User.on("afterCreate", (user) => { console.log("Created:", user.get("id")) })

// Timestamps plugin sets createdAt/updatedAt automatically
const Timestamped = defineModel("ts", {
  columns: { ...t.timestamps(), ...t.integer().primaryKey(), name: t.string(255) },
}).use(timestamps())

Soft Deletes

const SoftModel = defineModel("items", {
  columns: { id: t.integer().primaryKey(), name: t.string(255), ...t.timestamps() },
}).use(softDeletes())

await item.$delete()              // sets deletedAt
await item.$restore()             // clears deletedAt
await item.$forceDelete()         // actually deletes

const active = await SoftModel.query().execute()               // excludes deleted
const all = await SoftModel.query().withTrashed().execute()    // includes deleted
const trashed = await SoftModel.query().onlyTrashed().execute() // only deleted

Graph Operations

Insert or upsert nested models in a single call:

const user = await User.insertGraph({
  name: "Alice",
  posts: [{ title: "Post 1" }, { title: "Post 2" }],
})

const updated = await User.upsertGraph({
  id: user.get("id"),
  name: "Alice Updated",
  posts: [{ id: 1, title: "Post 1 Updated" }, { title: "New Post" }],
})

Casting & Serialization

const User = defineModel("users", {
  columns: {
    id: t.integer().primaryKey(), name: t.string(255),
    meta: t.json(), flags: t.boolean(), password: t.string(255),
  },
  $casts: { meta: "json", flags: "boolean" },
  $hidden: ["password"],
})

const json = user.$toJSON()  // password excluded, meta parsed from JSON

Error Handling

try {
  await Post.insert({ slug: "my-post" })
} catch (e) {
  if (e instanceof DatabaseError && e.code === "UNIQUE_CONSTRAINT") {
    return c.json({ error: "Slug taken" }, 400)
  }
  throw e
}

| Code | Meaning | Driver errors | |------|---------|--------------| | UNIQUE_CONSTRAINT | Duplicate value | SQLITE_CONSTRAINT_UNIQUE, PG 23505, MySQL ER_DUP_ENTRY | | FOREIGN_KEY_CONSTRAINT | Missing referenced row | SQLITE_CONSTRAINT_FOREIGNKEY, PG 23503, MySQL ER_NO_REFERENCED_ROW_2 |

Collections

const col = await User.query().orderBy("id", "asc").collect()
col.pluck("name")       // ["Alice", "Bob"]
col.groupBy("role")     // { admin: [...], user: [...] }
col.load("posts")       // eager load relations
col.sum("score")
col.chunk(10)           // split into batches

Global Scopes & Conditional Chaining

User.addGlobalScope("active", (qb) => qb.where("active", "=", 1))
await User.query().withoutGlobalScope("active").execute()

const posts = await Post.query()
  .when(sort?.length, (q) => q.orderBy(sort[0]!, "asc"))
  .unless(sort?.length, (q) => q.orderBy("createdAt", "desc"))
  .execute()

Migrations

See the peta-migrate package for migration generation and running.

import { createMigrationRunner, createMigrationGenerator } from "peta-migrate"

Examples

All self-contained (inline SQLite, run directly):

bun run examples/01-basic-setup.ts
bun run examples/04-relations.ts
bun run examples/07-soft-deletes.ts

| # | Example | Topic | |---|---------|-------| | 01 | basic-setup | ORM init + SQLite setup | | 02 | model-definition | Columns, types, modifiers, timestamps | | 03 | crud | insert, find, update, delete, paginate | | 04 | relations | hasMany, belongsTo, hasOne, eager loading | | 05 | query-builder | where, orderBy, join, has, whereHas | | 06 | hooks-timestamps | beforeCreate, afterCreate, timestamps | | 07 | soft-deletes | $delete, $restore, $forceDelete, withTrashed | | 08 | collection-paginator | Collection, Paginator, .collect() | | 09 | hono-integration | Hono app + DatabaseError handling | | 10 | elysia-integration | Elysia app stub | | 11 | many-to-many | ManyToMany via pivot table | | 12 | transactions | Model.transaction(), rollback | | 13 | casting | $casts, $hidden, $appends, accessors | | 14 | global-scopes | addGlobalScope(), withoutGlobalScope() | | 15 | batch | insertMany | | 16 | discover | peta.discover(), rest params | | 17 | instance-methods | fill, dirty, reset, $reload, $load | | 18 | advanced-queries | groupBy/having, aggregate helpers, chunk | | 19 | collections-deep | Full Collection + Paginator API | | 20 | advanced-relations | HasManyThrough, polymorphic morphs | | 21 | migrations | MigrationRunner, MigrationGenerator | | 22 | related-query-builder | $related() — scoped relation queries | | 23 | attach-detach-sync | Many-to-many pivot management | | 24 | computed-columns | Runtime + batch async computed columns | | 25 | static-hooks | asFindQuery() + cancelQuery() | | 26 | repository-pattern | createRepo() — custom query methods | | 27 | plugins-and-helpers | .use() plugin system + makeHelper() | | 28 | nested-create-update | Create/update with related data in one call | | 29 | allow-graph | allowGraph() — recursive eager load whitelist | | 30 | polymorphic-relations | MorphMany/MorphOne/MorphTo | | 31 | graph-operations | insertGraph()/upsertGraph() with #id/#ref | | 32 | accessors-mutators | Attribute.make({ get, set }) |


Database Support

| Database | Dialect package | Status | |----------|----------------|--------| | SQLite | @libsql/kysely-libsql + @libsql/client | ✅ Tested | | PostgreSQL | pg | ✅ Tested via Docker | | MySQL | mysql2 | ✅ Tested via Docker |

docker compose up -d     # PostgreSQL 16 + MySQL 8.0
cd packages/orm
bun test test/integration/

Environment Variables

| Variable | Default | Description | |----------|---------|-------------| | INTEGRATION_PG_URL | postgres://postgres:postgres@localhost:5432/peta_orm_test | PostgreSQL connection string | | INTEGRATION_MYSQL_URL | mysql://root:mysqlroot@localhost:3306/peta_orm_test | MySQL connection string | | INTEGRATION_SKIP_PG | — | Set to 1 to skip PostgreSQL integration tests | | INTEGRATION_SKIP_MYSQL | — | Set to 1 to skip MySQL integration tests |

See .env.example for a copyable template.


Related packages

  • peta-auth — Encrypted cookie sessions, JWT, OAuth
  • peta-docs — OpenAPI 3.1 spec generation + Scalar UI
  • peta-migrate — Standalone migration runner and generator