Latest commit

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Stabilize ORM

A Modern, Type-Safe, and Expressive ORM for Bun

Stabilize ORM Logo

NPM VersionLicenseStabilize CLIPostgreSQLMySQLSQLiteBuild StatusMIT License

Stabilize is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for PostgreSQL, MySQL, and SQLite. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.


🚀 Features

  • Unified API: Write once, run on PostgreSQL, MySQL, or SQLite.
  • Programmatic Model Definitions: Define models and columns using the defineModel API with the DataTypes enum for database-agnostic schemas.
  • Full-Featured CLI: Generate models, manage migrations, seed data, and reset your database from the command line with stabilize-cli.
  • Automatic Migrations: Generate database-specific SQL schemas directly from your model definitions.
  • Versioned Models & Time-Travel: Enable versioning in your model configuration for automatic history tables and snapshot queries.
  • Retry Logic: Automatic exponential backoff for database queries to handle transient connection issues.
  • Connection Pooling: Efficient connection management for PostgreSQL and MySQL.
  • Transactional Integrity: Built-in support for atomic transactions with automatic rollback on failure.
  • Advanced Query Builder: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
  • Pagination Helper: Easily paginate any query with .paginate(page, pageSize) and get { data, total, page, pageSize }.
  • Advanced Model Validation: Enforce rules like required, minLength, maxLength, pattern, and custom validators—errors are thrown on invalid input.
  • Model Relationships: Define OneToOne, ManyToOne, OneToMany, and ManyToMany relationships in the model configuration.
  • Soft Deletes: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
  • Lifecycle Hooks: Define hooks in the model configuration or as class methods for lifecycle events like beforeCreate, afterUpdate, etc.
  • Pluggable Logging: Includes a robust StabilizeLogger with support for file-based, rotating logs.
  • Custom Errors: StabilizeError provides clear, consistent error handling.
  • Caching Layer: Optional Redis-backed caching with cache-aside and write-through strategies.
  • Custom Query Scopes: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
  • Timestamps: Automatically manage createdAt and updatedAt columns for tracking record creation and update times.
  • SQL Default Expressions: Support database-side default expressions (e.g., gen_random_uuid(), NOW()) for columns using the sqlDefault() helper.
  • Nested Relations (Eager Loading): Load deeply nested relations using dot notation like "roles.permissions".
  • AutoMigrate with Index Management: Automatically create, detect, and remove indexes and unique constraints during migration.
  • Advanced Query Builder Filters: Chainable .orWhere(), .whereIn(), .whereNotIn(), .whereNull(), .whereNotNull(), .whereBetween(), .groupBy(), .having(), .lock() methods.
  • Optimistic Locking: Add optimisticLock: true to a version column to automatically detect concurrent modification conflicts and throw CONCURRENT_MODIFICATION errors.
  • findAndCount: Get paginated results with a total count in one call.
  • findOneBy / findBy: TypeORM-style conditional finders without writing raw SQL.
  • Aggregate Queries: Run count(), sum(), avg(), min(), max() directly from the repository or query builder.
  • Cursor-Based Pagination: Efficient forward/backward cursor pagination for large datasets (Prisma-style findMany).
  • exists: Check if a record exists without loading it.
  • recoverAll: Bulk restore all soft-deleted records.
  • truncate: Clear all rows from a table.
  • seed / defineSeed: Laravel-style seeding framework with defineSeed and runSeeds.
  • resetDatabase: Drop tables, re-migrate, and optionally re-seed for development.
  • healthCheck: Get database and table health status with latency for monitoring endpoints.
  • rawQuery / rawExec: Execute raw SQL directly from the Stabilize instance.
  • bulkUpsert: Upsert multiple records in a single transaction.
  • findMany: Prisma-style query with where, cursor, take, skip, orderBy.
  • countDistinct: Count unique values in a column.
  • increment / decrement: Atomically increment or decrement a numeric field.
  • pluck: Get an array of a single column's values (Rails-style).
  • selectColumns: Get only specific columns from a query.
  • toggle: Toggle a boolean field (Rails-style).
  • updateBy / deleteBy: Conditional bulk updates and deletes without writing SQL.
  • restoreBy: Restore soft-deleted records matching conditions.
  • findDeleted / withTrashed: Query soft-deleted or all records.
  • upsertMany: Batch upsert in configurable batch sizes.
  • map / each / eachBatch: Transform, iterate, and batch-process query results.
  • lockForUpdate: Pessimistic row locking for read-modify-write.
  • firstOrCreate / updateOrCreate: Laravel-style find-or-create patterns.
  • first / last / random: Single-record shortcuts.
  • StabilizeEmitter: Event system for query, error, connection:open/close, transaction:start/complete/error.
  • TransactionIsolationLevel: Type for READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE.
  • generateUUID: Cross-runtime UUID generation helper.
  • poolStats: Get connection pool statistics.
  • Database Backup & Restore: db:backup and db:restore commands for database backup management.
  • API Generation: generate:api command scaffolds full CRUD REST API routes from models.
  • Fresh Migrations: migrate:fresh drops all tables and re-runs migrations without seeding.
  • Database Size Analysis: db:size command shows table sizes and row count statistics.

📦 Installation

Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).

# Using Bun
bun add stabilize-orm
# Using npm
npm install stabilize-orm

📃 Documentation & Community

Examples

  • Blog - Blog with users, posts, comments, and versioning
  • E-Commerce - Products, categories, orders with transactions
  • REST API - Express.js REST API with pagination and optimistic locking
  • SaaS - Multi-tenant SaaS with tenants, members, and scoped projects
  • CMS - Content management with authors, categories, articles, and versioning
  • Analytics - Event tracking with aggregations and metrics

⚙️ Configuration

Create a database configuration file.

// config/database.tsimport{DBType,typeDBConfig}from"stabilize-orm";constdbConfig: DBConfig={type: DBType.Postgres,connectionString:
process.env.DATABASE_URL||"postgres://user:password@localhost:5432/mydb",retryAttempts: 3,retryDelay: 1000,};exportdefaultdbConfig;

Next, create a central ORM instance for your application.

// db.tsimport{Stabilize,typeCacheConfig,typeLoggerConfig,LogLevel,}from"stabilize-orm";importdbConfigfrom"./database";constcacheConfig: CacheConfig={enabled: process.env.CACHE_ENABLED==="true",redisUrl: process.env.REDIS_URL,ttl: 60,};constloggerConfig: LoggerConfig={level: LogLevel.Info,filePath: "logs/stabilize.log",maxFileSize: 5*1024*1024,// 5MBmaxFiles: 3,};exportconstorm=newStabilize(dbConfig,cacheConfig,loggerConfig);

🏗️ Models & Relationships

Define your tables as classes using the defineModel function. The DataTypes enum ensures database-agnostic schemas.

Example: Users and Roles (Many-to-Many) with Versioning

// models/User.tsimport{defineModel,DataTypes,RelationType}from"stabilize-orm";import{UserRole}from"./UserRole";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true,unique: true,},},relations: [{type: RelationType.OneToMany,target: ()=>UserRole,property: "roles",foreignKey: "userId",},],hooks: {beforeCreate: (entity)=>console.log(`Creating user: ${entity.email}`),},});export{User};

🔍 Pagination

The built-in pagination helper makes it easy to retrieve a page of rows using LIMIT/OFFSET semantics.

constusers=awaituserRepository.find().paginate(2,10).execute(dbClient);// users = [...] // Returns an array of rows for that page

Or, use the query builder:

constusers=awaituserRepository.find().where('isActive = ?',true).paginate(1,20).execute(dbClient);

🛡️ Advanced Validation

Models can define advanced validation rules for columns, including:

  • required
  • minLength / maxLength
  • pattern (RegExp)
  • customValidator (function)

Validation errors are thrown on create/update if data is invalid.

constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,required: true,unique: true,minLength: 6,pattern: /^[^@]+@[^@]+\.[^@]+$/,customValidator: (val)=>val.endsWith("@offbytesecure.com")||"Must use an @offbytesecure.com email",},password: {type: DataTypes.String,minLength: 8},},});

⏳ Versioning & Auditing

Enable automatic history tracking and time-travel queries by setting versioned: true in your model configuration.

  • Each change is recorded in a <table>_history table with version, operation, and audit columns.
  • Supports snapshot queries, rollbacks, audits, and time-travel.

Versioning Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},},});// --- Using versioning features:constuserRepository=orm.getRepository(User);// Rollback to a previous versionawaituserRepository.rollback(1,3);// roll back user with id=1 to version 3// Get a snapshot as of a specific dateconstuserAsOf=awaituserRepository.asOf(1,newDate("2025-01-01T00:00:00Z"));console.log(userAsOf);// View full version historyconsthistory=awaituserRepository.history(1);console.log(history);

🔄 Model Lifecycle Hooks

Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.

Hooks Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},hooks: {beforeCreate: (entity)=>{entity.createdAt=newDate();},beforeUpdate: (entity)=>{entity.updatedAt=newDate();},afterCreate: (entity)=>{console.log(`User created: ${entity.name}`);},},});// Add a hook as a class methodUser.prototype.afterUpdate=asyncfunction(){console.log(`Updated user: ${this.name}`);};export{User};

Supported hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeSave, afterSave.


💻 Command-Line Interface (CLI)

Stabilize includes a powerful CLI for managing your workflow. See: stabilize-cli on GitHub

Generating Files

  • Generate a model:

    stabilize-cli generate:model Product
  • Generate a migration from a model:

    stabilize-cli generate:migration User
  • Generate a seed file:

    stabilize-cli generate:seed InitialRoles
  • Generate a REST API scaffold:

    stabilize-cli generate:api User

Database & Migration Management

  • Run all pending migrations:

    stabilize-cli migrate
  • Roll back the last migration:

    stabilize-cli migrate:rollback
  • Fresh migration (drop + re-migrate):

    stabilize-cli migrate:fresh --force
  • Run all pending seeds (in dependency order):

    stabilize-cli seed
  • Check the status of migrations and seeds:

    stabilize-cli status
  • Reset the database (drop, migrate, seed):

    stabilize-cli db:reset

Backup & Restore

  • Backup the database:

    stabilize-cli db:backup
  • Restore from a backup:

    stabilize-cli db:restore backups/backup_20250101120000.db --force

Diagnostics

  • Database size statistics:

    stabilize-cli db:size
  • Health check:

    stabilize-cli health
  • CLI info:

    stabilize-cli info

🧑‍💻 Querying Data

Basic CRUD with Repositories

import{orm}from"./db";import{User}from"./models/User";constuserRepository=orm.getRepository(User);constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});constfoundUser=awaituserRepository.findOne(newUser.id);constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});awaituserRepository.delete(newUser.id);

Advanced Queries with the Query Builder

constactiveAdmins=awaitorm.getRepository(UserRole).find().join("users","user_roles.user_id = users.id").join("roles","user_roles.role_id = roles.id").select("users.id","users.email","roles.name as role_name").where("roles.name = ?","Admin").orderBy("users.email ASC").execute();console.log(activeAdmins);

Query Builder API

{select(...fields: string[]): QueryBuilder<User>;where(condition: string, ...params: any[]): QueryBuilder<User>;orWhere(condition: string, ...params: any[]): QueryBuilder<User>;whereIn(column: string,values: any[]): QueryBuilder<User>;whereNotIn(column: string,values: any[]): QueryBuilder<User>;whereNull(column: string): QueryBuilder<User>;whereNotNull(column: string): QueryBuilder<User>;whereBetween(column: string,start: any,end: any): QueryBuilder<User>;groupBy(clause: string): QueryBuilder<User>;having(condition: string, ...params: any[]): QueryBuilder<User>;join(table: string,condition: string): QueryBuilder<User>;orderBy(clause: string): QueryBuilder<User>;limit(limit: number): QueryBuilder<User>;offset(offset: number): QueryBuilder<User>;lock(mode?: "FOR UPDATE"|"FOR SHARE"): QueryBuilder<User>;withRelations(...relations: string[]): QueryBuilder<User>;scope(name: string, ...args: any[]): QueryBuilder<User>;paginate(page: number,pageSize: number): QueryBuilder<User>;build(): { query: string; params: any[]};execute(client?: DBClient,cache?: Cache,cacheKey?: string): Promise<User[]>;}

Custom Query Scopes

Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the scope method on Repository or QueryBuilder, allowing you to chain them with other query operations.

Scopes Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},isActive: {type: DataTypes.Boolean,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},scopes: {active: (qb)=>qb.where("isActive = ?",true),recent: (qb,days: number)=>qb.where("createdAt >= ?",newDate(Date.now()-days*24*60*60*1000),),},});constuserRepository=orm.getRepository(User);// Fetch active usersconstactiveUsers=awaituserRepository.scope("active").execute();// Fetch users created in the last 7 daysconstrecentUsers=awaituserRepository.scope("recent",7).execute();// Combine scopes with other query operationsconstrecentActiveUsers=awaituserRepository.scope("active").scope("recent",7).orderBy("createdAt DESC").limit(10).execute();console.log(recentActiveUsers);

Timestamps

Enable automatic management of createdAt and updatedAt columns by setting timestamps in your model configuration. The ORM automatically sets these fields during create, update, bulkCreate, bulkUpdate, and upsert operations in a TypeScript-safe manner, eliminating the need for manual hooks.

Timestamps Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},timestamps: {createdAt: "createdAt",updatedAt: "updatedAt",},});constuserRepository=orm.getRepository(User);// Create a user (createdAt and updatedAt set automatically)constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});console.log(newUser.createdAt,newUser.updatedAt);// Outputs current timestamp// Update a user (updatedAt updated automatically)constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});console.log(updatedUser.updatedAt);// Outputs new timestamp// Bulk create usersconstnewUsers=awaituserRepository.bulkCreate([{email: "user1@example.com"},{email: "user2@example.com"},]);console.log(newUsers.map((u)=>u.createdAt));// Outputs timestamps for each user

🗑️ Soft Deletes

Enable soft deletes by setting softDelete: true and marking a column (e.g., deletedAt) with softDelete: true in the model configuration.

  • Use repository.delete(id) to mark an entity as deleted.
  • Use repository.recover(id) to restore a soft-deleted entity.
  • Queries automatically exclude soft-deleted rows unless specified otherwise.

Soft Delete Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",softDelete: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},deletedAt: {type: DataTypes.DateTime,softDelete: true},},});constuserRepository=orm.getRepository(User);awaituserRepository.create({email: "lwazicd@icloud.com"});awaituserRepository.delete(1);// Soft deleteawaituserRepository.recover(1);// Recover

🌐 Express.js Integration

Stabilize ORM works seamlessly with web frameworks like Express.

importexpressfrom"express";import{orm}from"./db";import{User}from"./models/User";constapp=express();app.use(express.json());constuserRepository=orm.getRepository(User);app.get("/users",async(req,res)=>{try{constusers=awaituserRepository.find().execute();res.json(users);}catch(err){res.status(500).json({error: "Failed to fetch users."});}});app.post("/users",async(req,res)=>{try{constuser=awaituserRepository.create(req.body);res.status(201).json(user);}catch(err){res.status(500).json({error: "User creation failed."});}});app.listen(3000,()=>{console.log("Server listening on port 3000");});

🧑‍🔬 Testing & Time-Travel

  • Use time-travel queries to inspect historical entity states.
  • Assert audit trails and rollback operations in your tests.

🔒 Optimistic Locking

Enable optimistic locking to detect concurrent modifications. Add optimisticLock: true to a version column in your model.

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String},version: {type: DataTypes.Integer,optimisticLock: true},},});constuserRepository=orm.getRepository(User);// Create with initial versionconstuser=awaituserRepository.create({name: "Lwazi",version: 1});// Update - version is automatically incremented// If another transaction modified the record, a CONCURRENT_MODIFICATION error is throwntry{awaituserRepository.update(user.id,{name: "Updated",version: user.version,});}catch(err){if(err.code==="CONCURRENT_MODIFICATION"){console.log("Record was modified by another transaction");}}

⏱️ SQL Default Expressions

Use sqlDefault() to set database-side default values for columns (e.g., gen_random_uuid(), NOW()).

import{defineModel,DataTypes,sqlDefault}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.UUID,required: true,defaultExpression: sqlDefault("gen_random_uuid()"),},name: {type: DataTypes.String},createdAt: {type: DataTypes.DateTime,defaultExpression: sqlDefault("NOW()"),},},});

📊 Advanced Query Builder

The query builder now supports additional filter methods:

constresults=awaituserRepository.find().where("status = ?","active").orWhere("role = ?","admin").whereIn("age",[25,30,35]).whereBetween("createdAt",newDate("2025-01-01"),newDate("2025-12-31")).whereNull("deletedAt").groupBy("department").having("COUNT(*) > ?",5).orderBy("createdAt DESC").limit(10).execute();

🔗 Nested Relations

Load deeply nested relations using dot notation:

constuser=awaituserRepository.findOne(1,{relations: ["roles","roles.permissions"],});

📊 Aggregation Queries

Run aggregate queries directly on the repository or query builder.

// Repository-level aggregatesconsttotal=awaituserRepository.count();constexists=awaituserRepository.exists({email: "admin@offbytesecure.com",});conststats=awaituserRepository.aggregate({count: "*",sum: ["salary"],avg: ["salary"],min: ["salary"],max: ["salary"],});// stats = { count_: 100, sum_salary: 5000000, avg_salary: 50000, min_salary: 20000, max_salary: 150000 }

🔎 findOneBy / findBy

TypeORM-style conditional finders without writing raw SQL.

// Find one record by conditionconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"});// Find multiple recordsconstadmins=awaituserRepository.findBy({role: "admin"},{limit: 10,orderBy: "createdAt DESC"},);// Combined with relationsconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"},{relations: ["roles"]},);

🔢 findAndCount

Get paginated results with a total count in a single call.

const{ data, total }=awaituserRepository.findAndCount();console.log(`Showing ${data.length} of ${total} total records`);

🖱️ Cursor-Based Pagination

Efficient cursor-based pagination for large datasets.

// First pageconstpage1=awaituserRepository.findMany({take: 10,orderBy: {field: "id",direction: "ASC"},});// Next page using cursorconstlastId=page1[page1.length-1].id;constpage2=awaituserRepository.findMany({cursor: {field: "id",value: lastId,direction: "forward"},take: 10,orderBy: {field: "id",direction: "ASC"},});

🌱 Database Seeding

Define and run seeds for populating development/test databases.

import{defineSeed,runSeeds,resetDatabase}from"stabilize-orm";// Define a seeddefineSeed("create-default-roles",async(db)=>{constroleRepo=orm.getRepository(Role);awaitroleRepo.bulkCreate([{name: "Admin",permissions: "all"},{name: "User",permissions: "read"},]);});// Run all seedsawaitrunSeeds(orm.client);// Reset database (drop, migrate, seed)awaitresetDatabase(orm.client,[User,Role]);

🏥 Health Check

Monitor database and cache connectivity with latency.

consthealth=awaitorm.healthCheck();// { status: "healthy", database: "postgres", latencyMs: 12.5, cacheStatus: "connected" }// Per-table healthconstuserHealth=awaituserRepository.healthCheck();// { status: "healthy", table: "users", rows: 142, latencyMs: 8.3 }

📂 Bulk Upsert

Upsert multiple records in a single transaction.

constusers=awaituserRepository.bulkUpsert([{email: "lwazicd@icloud.com",name: "Lwazi"},{email: "ciniso@icloud.com",name: "Ciniso"},],["email"],// unique key(s));

🔧 Raw SQL

Execute raw SQL queries directly from the ORM instance.

constresults=awaitorm.rawQuery("SELECT * FROM users WHERE age > ?",[25]);const{ affectedRows }=awaitorm.rawExec("UPDATE users SET active = false WHERE last_login < ?",[oneYearAgo],);

🗑️ recoverAll / truncate

// Restore all soft-deleted recordsconstrecovered=awaituserRepository.recoverAll();console.log(`Recovered ${recovered} records`);// Clear the tableawaituserRepository.truncate();

⬆️⬇️ Increment / Decrement

Atomically update numeric fields without loading the record.

constupdated=awaituserRepository.increment(user.id,"loginCount",1);constupdated=awaituserRepository.decrement(user.id,"credits",5);

🏷️ Pluck / SelectColumns

// Get just the email column as an arrayconstemails=awaituserRepository.pluck("email");// ["lwazicd@icloud.com", "ciniso@icloud.com", ...]// Get specific columnsconstusers=awaituserRepository.selectColumns("id","email");// [{ id: 1, email: "lwazicd@icloud.com" }, ...]

🔄 Toggle

Toggle a boolean field.

consttoggled=awaituserRepository.toggle(user.id,"isActive");// isActive was true, now false (or vice versa)

✏️ updateBy / deleteBy

// Update all active users' role to "member"constupdated=awaituserRepository.updateBy({isActive: true},{role: "member"},);// Delete all users with null emailconstdeleted=awaituserRepository.deleteBy({email: null});

🕳️ findDeleted / withTrashed

// Get only soft-deleted recordsconstdeletedUsers=awaituserRepository.findDeleted().execute();// Get all records including soft-deletedconstallUsers=awaituserRepository.withTrashed().execute();// Restore matching soft-deleted recordsconstrestored=awaituserRepository.restoreBy({role: "admin"});

🔄 firstOrCreate / updateOrCreate

// Find or create in one callconstuser=awaituserRepository.firstOrCreate({email: "lwazicd@icloud.com"},{name: "Lwazi"},);// Find and update, or create if not foundconstuser=awaituserRepository.updateOrCreate({email: "lwazicd@icloud.com"},{name: "Updated Name"},);

🥇 first / last / random

constfirstUser=awaituserRepository.first();constadmin=awaituserRepository.first({role: "admin"});constlastUser=awaituserRepository.last();constrandomUser=awaituserRepository.random();

🔒 Pessimistic Locking (lockForUpdate)

constuser=awaituserRepository.lockForUpdate(user.id);// The row is now locked for the duration of the transaction

📡 Event Emitter

Subscribe to ORM lifecycle events.

constorm=newStabilize(dbConfig);orm.events.on("query",(entry)=>{console.log(`[${entry.durationMs}ms] ${entry.query}`);});orm.events.on("error",(err)=>{console.error("ORM error:",err);});orm.events.on("connection:open",(dbType)=>{console.log(`Connected to ${dbType}`);});

📦 Pool Stats

conststats=awaitorm.poolStats();// { active: 5, idle: 10, total: 15 }

🆔 generateUUID

import{generateUUID}from"stabilize-orm";constid=generateUUID();// "550e8400-e29b-41d4-a716-446655440000"

📑 License

Licensed under the MIT License. See LICENSE.md for details.


Created with ❤️ by ElectronSz
File last updated: 2026-04-02

About

A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Latest commit

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Stabilize ORM

A Modern, Type-Safe, and Expressive ORM for Bun

Stabilize ORM Logo

NPM VersionLicenseStabilize CLIPostgreSQLMySQLSQLiteBuild StatusMIT License

Stabilize is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for PostgreSQL, MySQL, and SQLite. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.


🚀 Features

  • Unified API: Write once, run on PostgreSQL, MySQL, or SQLite.
  • Programmatic Model Definitions: Define models and columns using the defineModel API with the DataTypes enum for database-agnostic schemas.
  • Full-Featured CLI: Generate models, manage migrations, seed data, and reset your database from the command line with stabilize-cli.
  • Automatic Migrations: Generate database-specific SQL schemas directly from your model definitions.
  • Versioned Models & Time-Travel: Enable versioning in your model configuration for automatic history tables and snapshot queries.
  • Retry Logic: Automatic exponential backoff for database queries to handle transient connection issues.
  • Connection Pooling: Efficient connection management for PostgreSQL and MySQL.
  • Transactional Integrity: Built-in support for atomic transactions with automatic rollback on failure.
  • Advanced Query Builder: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
  • Pagination Helper: Easily paginate any query with .paginate(page, pageSize) and get { data, total, page, pageSize }.
  • Advanced Model Validation: Enforce rules like required, minLength, maxLength, pattern, and custom validators—errors are thrown on invalid input.
  • Model Relationships: Define OneToOne, ManyToOne, OneToMany, and ManyToMany relationships in the model configuration.
  • Soft Deletes: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
  • Lifecycle Hooks: Define hooks in the model configuration or as class methods for lifecycle events like beforeCreate, afterUpdate, etc.
  • Pluggable Logging: Includes a robust StabilizeLogger with support for file-based, rotating logs.
  • Custom Errors: StabilizeError provides clear, consistent error handling.
  • Caching Layer: Optional Redis-backed caching with cache-aside and write-through strategies.
  • Custom Query Scopes: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
  • Timestamps: Automatically manage createdAt and updatedAt columns for tracking record creation and update times.
  • SQL Default Expressions: Support database-side default expressions (e.g., gen_random_uuid(), NOW()) for columns using the sqlDefault() helper.
  • Nested Relations (Eager Loading): Load deeply nested relations using dot notation like "roles.permissions".
  • AutoMigrate with Index Management: Automatically create, detect, and remove indexes and unique constraints during migration.
  • Advanced Query Builder Filters: Chainable .orWhere(), .whereIn(), .whereNotIn(), .whereNull(), .whereNotNull(), .whereBetween(), .groupBy(), .having(), .lock() methods.
  • Optimistic Locking: Add optimisticLock: true to a version column to automatically detect concurrent modification conflicts and throw CONCURRENT_MODIFICATION errors.
  • findAndCount: Get paginated results with a total count in one call.
  • findOneBy / findBy: TypeORM-style conditional finders without writing raw SQL.
  • Aggregate Queries: Run count(), sum(), avg(), min(), max() directly from the repository or query builder.
  • Cursor-Based Pagination: Efficient forward/backward cursor pagination for large datasets (Prisma-style findMany).
  • exists: Check if a record exists without loading it.
  • recoverAll: Bulk restore all soft-deleted records.
  • truncate: Clear all rows from a table.
  • seed / defineSeed: Laravel-style seeding framework with defineSeed and runSeeds.
  • resetDatabase: Drop tables, re-migrate, and optionally re-seed for development.
  • healthCheck: Get database and table health status with latency for monitoring endpoints.
  • rawQuery / rawExec: Execute raw SQL directly from the Stabilize instance.
  • bulkUpsert: Upsert multiple records in a single transaction.
  • findMany: Prisma-style query with where, cursor, take, skip, orderBy.
  • countDistinct: Count unique values in a column.
  • increment / decrement: Atomically increment or decrement a numeric field.
  • pluck: Get an array of a single column's values (Rails-style).
  • selectColumns: Get only specific columns from a query.
  • toggle: Toggle a boolean field (Rails-style).
  • updateBy / deleteBy: Conditional bulk updates and deletes without writing SQL.
  • restoreBy: Restore soft-deleted records matching conditions.
  • findDeleted / withTrashed: Query soft-deleted or all records.
  • upsertMany: Batch upsert in configurable batch sizes.
  • map / each / eachBatch: Transform, iterate, and batch-process query results.
  • lockForUpdate: Pessimistic row locking for read-modify-write.
  • firstOrCreate / updateOrCreate: Laravel-style find-or-create patterns.
  • first / last / random: Single-record shortcuts.
  • StabilizeEmitter: Event system for query, error, connection:open/close, transaction:start/complete/error.
  • TransactionIsolationLevel: Type for READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE.
  • generateUUID: Cross-runtime UUID generation helper.
  • poolStats: Get connection pool statistics.
  • Database Backup & Restore: db:backup and db:restore commands for database backup management.
  • API Generation: generate:api command scaffolds full CRUD REST API routes from models.
  • Fresh Migrations: migrate:fresh drops all tables and re-runs migrations without seeding.
  • Database Size Analysis: db:size command shows table sizes and row count statistics.

📦 Installation

Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).

# Using Bun
bun add stabilize-orm
# Using npm
npm install stabilize-orm

📃 Documentation & Community

Examples

  • Blog - Blog with users, posts, comments, and versioning
  • E-Commerce - Products, categories, orders with transactions
  • REST API - Express.js REST API with pagination and optimistic locking
  • SaaS - Multi-tenant SaaS with tenants, members, and scoped projects
  • CMS - Content management with authors, categories, articles, and versioning
  • Analytics - Event tracking with aggregations and metrics

⚙️ Configuration

Create a database configuration file.

// config/database.tsimport{DBType,typeDBConfig}from"stabilize-orm";constdbConfig: DBConfig={type: DBType.Postgres,connectionString:
process.env.DATABASE_URL||"postgres://user:password@localhost:5432/mydb",retryAttempts: 3,retryDelay: 1000,};exportdefaultdbConfig;

Next, create a central ORM instance for your application.

// db.tsimport{Stabilize,typeCacheConfig,typeLoggerConfig,LogLevel,}from"stabilize-orm";importdbConfigfrom"./database";constcacheConfig: CacheConfig={enabled: process.env.CACHE_ENABLED==="true",redisUrl: process.env.REDIS_URL,ttl: 60,};constloggerConfig: LoggerConfig={level: LogLevel.Info,filePath: "logs/stabilize.log",maxFileSize: 5*1024*1024,// 5MBmaxFiles: 3,};exportconstorm=newStabilize(dbConfig,cacheConfig,loggerConfig);

🏗️ Models & Relationships

Define your tables as classes using the defineModel function. The DataTypes enum ensures database-agnostic schemas.

Example: Users and Roles (Many-to-Many) with Versioning

// models/User.tsimport{defineModel,DataTypes,RelationType}from"stabilize-orm";import{UserRole}from"./UserRole";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true,unique: true,},},relations: [{type: RelationType.OneToMany,target: ()=>UserRole,property: "roles",foreignKey: "userId",},],hooks: {beforeCreate: (entity)=>console.log(`Creating user: ${entity.email}`),},});export{User};

🔍 Pagination

The built-in pagination helper makes it easy to retrieve a page of rows using LIMIT/OFFSET semantics.

constusers=awaituserRepository.find().paginate(2,10).execute(dbClient);// users = [...] // Returns an array of rows for that page

Or, use the query builder:

constusers=awaituserRepository.find().where('isActive = ?',true).paginate(1,20).execute(dbClient);

🛡️ Advanced Validation

Models can define advanced validation rules for columns, including:

  • required
  • minLength / maxLength
  • pattern (RegExp)
  • customValidator (function)

Validation errors are thrown on create/update if data is invalid.

constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,required: true,unique: true,minLength: 6,pattern: /^[^@]+@[^@]+\.[^@]+$/,customValidator: (val)=>val.endsWith("@offbytesecure.com")||"Must use an @offbytesecure.com email",},password: {type: DataTypes.String,minLength: 8},},});

⏳ Versioning & Auditing

Enable automatic history tracking and time-travel queries by setting versioned: true in your model configuration.

  • Each change is recorded in a <table>_history table with version, operation, and audit columns.
  • Supports snapshot queries, rollbacks, audits, and time-travel.

Versioning Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},},});// --- Using versioning features:constuserRepository=orm.getRepository(User);// Rollback to a previous versionawaituserRepository.rollback(1,3);// roll back user with id=1 to version 3// Get a snapshot as of a specific dateconstuserAsOf=awaituserRepository.asOf(1,newDate("2025-01-01T00:00:00Z"));console.log(userAsOf);// View full version historyconsthistory=awaituserRepository.history(1);console.log(history);

🔄 Model Lifecycle Hooks

Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.

Hooks Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},hooks: {beforeCreate: (entity)=>{entity.createdAt=newDate();},beforeUpdate: (entity)=>{entity.updatedAt=newDate();},afterCreate: (entity)=>{console.log(`User created: ${entity.name}`);},},});// Add a hook as a class methodUser.prototype.afterUpdate=asyncfunction(){console.log(`Updated user: ${this.name}`);};export{User};

Supported hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeSave, afterSave.


💻 Command-Line Interface (CLI)

Stabilize includes a powerful CLI for managing your workflow. See: stabilize-cli on GitHub

Generating Files

  • Generate a model:

    stabilize-cli generate:model Product
  • Generate a migration from a model:

    stabilize-cli generate:migration User
  • Generate a seed file:

    stabilize-cli generate:seed InitialRoles
  • Generate a REST API scaffold:

    stabilize-cli generate:api User

Database & Migration Management

  • Run all pending migrations:

    stabilize-cli migrate
  • Roll back the last migration:

    stabilize-cli migrate:rollback
  • Fresh migration (drop + re-migrate):

    stabilize-cli migrate:fresh --force
  • Run all pending seeds (in dependency order):

    stabilize-cli seed
  • Check the status of migrations and seeds:

    stabilize-cli status
  • Reset the database (drop, migrate, seed):

    stabilize-cli db:reset

Backup & Restore

  • Backup the database:

    stabilize-cli db:backup
  • Restore from a backup:

    stabilize-cli db:restore backups/backup_20250101120000.db --force

Diagnostics

  • Database size statistics:

    stabilize-cli db:size
  • Health check:

    stabilize-cli health
  • CLI info:

    stabilize-cli info

🧑‍💻 Querying Data

Basic CRUD with Repositories

import{orm}from"./db";import{User}from"./models/User";constuserRepository=orm.getRepository(User);constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});constfoundUser=awaituserRepository.findOne(newUser.id);constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});awaituserRepository.delete(newUser.id);

Advanced Queries with the Query Builder

constactiveAdmins=awaitorm.getRepository(UserRole).find().join("users","user_roles.user_id = users.id").join("roles","user_roles.role_id = roles.id").select("users.id","users.email","roles.name as role_name").where("roles.name = ?","Admin").orderBy("users.email ASC").execute();console.log(activeAdmins);

Query Builder API

{select(...fields: string[]): QueryBuilder<User>;where(condition: string, ...params: any[]): QueryBuilder<User>;orWhere(condition: string, ...params: any[]): QueryBuilder<User>;whereIn(column: string,values: any[]): QueryBuilder<User>;whereNotIn(column: string,values: any[]): QueryBuilder<User>;whereNull(column: string): QueryBuilder<User>;whereNotNull(column: string): QueryBuilder<User>;whereBetween(column: string,start: any,end: any): QueryBuilder<User>;groupBy(clause: string): QueryBuilder<User>;having(condition: string, ...params: any[]): QueryBuilder<User>;join(table: string,condition: string): QueryBuilder<User>;orderBy(clause: string): QueryBuilder<User>;limit(limit: number): QueryBuilder<User>;offset(offset: number): QueryBuilder<User>;lock(mode?: "FOR UPDATE"|"FOR SHARE"): QueryBuilder<User>;withRelations(...relations: string[]): QueryBuilder<User>;scope(name: string, ...args: any[]): QueryBuilder<User>;paginate(page: number,pageSize: number): QueryBuilder<User>;build(): { query: string; params: any[]};execute(client?: DBClient,cache?: Cache,cacheKey?: string): Promise<User[]>;}

Custom Query Scopes

Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the scope method on Repository or QueryBuilder, allowing you to chain them with other query operations.

Scopes Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},isActive: {type: DataTypes.Boolean,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},scopes: {active: (qb)=>qb.where("isActive = ?",true),recent: (qb,days: number)=>qb.where("createdAt >= ?",newDate(Date.now()-days*24*60*60*1000),),},});constuserRepository=orm.getRepository(User);// Fetch active usersconstactiveUsers=awaituserRepository.scope("active").execute();// Fetch users created in the last 7 daysconstrecentUsers=awaituserRepository.scope("recent",7).execute();// Combine scopes with other query operationsconstrecentActiveUsers=awaituserRepository.scope("active").scope("recent",7).orderBy("createdAt DESC").limit(10).execute();console.log(recentActiveUsers);

Timestamps

Enable automatic management of createdAt and updatedAt columns by setting timestamps in your model configuration. The ORM automatically sets these fields during create, update, bulkCreate, bulkUpdate, and upsert operations in a TypeScript-safe manner, eliminating the need for manual hooks.

Timestamps Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},timestamps: {createdAt: "createdAt",updatedAt: "updatedAt",},});constuserRepository=orm.getRepository(User);// Create a user (createdAt and updatedAt set automatically)constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});console.log(newUser.createdAt,newUser.updatedAt);// Outputs current timestamp// Update a user (updatedAt updated automatically)constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});console.log(updatedUser.updatedAt);// Outputs new timestamp// Bulk create usersconstnewUsers=awaituserRepository.bulkCreate([{email: "user1@example.com"},{email: "user2@example.com"},]);console.log(newUsers.map((u)=>u.createdAt));// Outputs timestamps for each user

🗑️ Soft Deletes

Enable soft deletes by setting softDelete: true and marking a column (e.g., deletedAt) with softDelete: true in the model configuration.

  • Use repository.delete(id) to mark an entity as deleted.
  • Use repository.recover(id) to restore a soft-deleted entity.
  • Queries automatically exclude soft-deleted rows unless specified otherwise.

Soft Delete Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",softDelete: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},deletedAt: {type: DataTypes.DateTime,softDelete: true},},});constuserRepository=orm.getRepository(User);awaituserRepository.create({email: "lwazicd@icloud.com"});awaituserRepository.delete(1);// Soft deleteawaituserRepository.recover(1);// Recover

🌐 Express.js Integration

Stabilize ORM works seamlessly with web frameworks like Express.

importexpressfrom"express";import{orm}from"./db";import{User}from"./models/User";constapp=express();app.use(express.json());constuserRepository=orm.getRepository(User);app.get("/users",async(req,res)=>{try{constusers=awaituserRepository.find().execute();res.json(users);}catch(err){res.status(500).json({error: "Failed to fetch users."});}});app.post("/users",async(req,res)=>{try{constuser=awaituserRepository.create(req.body);res.status(201).json(user);}catch(err){res.status(500).json({error: "User creation failed."});}});app.listen(3000,()=>{console.log("Server listening on port 3000");});

🧑‍🔬 Testing & Time-Travel

  • Use time-travel queries to inspect historical entity states.
  • Assert audit trails and rollback operations in your tests.

🔒 Optimistic Locking

Enable optimistic locking to detect concurrent modifications. Add optimisticLock: true to a version column in your model.

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String},version: {type: DataTypes.Integer,optimisticLock: true},},});constuserRepository=orm.getRepository(User);// Create with initial versionconstuser=awaituserRepository.create({name: "Lwazi",version: 1});// Update - version is automatically incremented// If another transaction modified the record, a CONCURRENT_MODIFICATION error is throwntry{awaituserRepository.update(user.id,{name: "Updated",version: user.version,});}catch(err){if(err.code==="CONCURRENT_MODIFICATION"){console.log("Record was modified by another transaction");}}

⏱️ SQL Default Expressions

Use sqlDefault() to set database-side default values for columns (e.g., gen_random_uuid(), NOW()).

import{defineModel,DataTypes,sqlDefault}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.UUID,required: true,defaultExpression: sqlDefault("gen_random_uuid()"),},name: {type: DataTypes.String},createdAt: {type: DataTypes.DateTime,defaultExpression: sqlDefault("NOW()"),},},});

📊 Advanced Query Builder

The query builder now supports additional filter methods:

constresults=awaituserRepository.find().where("status = ?","active").orWhere("role = ?","admin").whereIn("age",[25,30,35]).whereBetween("createdAt",newDate("2025-01-01"),newDate("2025-12-31")).whereNull("deletedAt").groupBy("department").having("COUNT(*) > ?",5).orderBy("createdAt DESC").limit(10).execute();

🔗 Nested Relations

Load deeply nested relations using dot notation:

constuser=awaituserRepository.findOne(1,{relations: ["roles","roles.permissions"],});

📊 Aggregation Queries

Run aggregate queries directly on the repository or query builder.

// Repository-level aggregatesconsttotal=awaituserRepository.count();constexists=awaituserRepository.exists({email: "admin@offbytesecure.com",});conststats=awaituserRepository.aggregate({count: "*",sum: ["salary"],avg: ["salary"],min: ["salary"],max: ["salary"],});// stats = { count_: 100, sum_salary: 5000000, avg_salary: 50000, min_salary: 20000, max_salary: 150000 }

🔎 findOneBy / findBy

TypeORM-style conditional finders without writing raw SQL.

// Find one record by conditionconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"});// Find multiple recordsconstadmins=awaituserRepository.findBy({role: "admin"},{limit: 10,orderBy: "createdAt DESC"},);// Combined with relationsconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"},{relations: ["roles"]},);

🔢 findAndCount

Get paginated results with a total count in a single call.

const{ data, total }=awaituserRepository.findAndCount();console.log(`Showing ${data.length} of ${total} total records`);

🖱️ Cursor-Based Pagination

Efficient cursor-based pagination for large datasets.

// First pageconstpage1=awaituserRepository.findMany({take: 10,orderBy: {field: "id",direction: "ASC"},});// Next page using cursorconstlastId=page1[page1.length-1].id;constpage2=awaituserRepository.findMany({cursor: {field: "id",value: lastId,direction: "forward"},take: 10,orderBy: {field: "id",direction: "ASC"},});

🌱 Database Seeding

Define and run seeds for populating development/test databases.

import{defineSeed,runSeeds,resetDatabase}from"stabilize-orm";// Define a seeddefineSeed("create-default-roles",async(db)=>{constroleRepo=orm.getRepository(Role);awaitroleRepo.bulkCreate([{name: "Admin",permissions: "all"},{name: "User",permissions: "read"},]);});// Run all seedsawaitrunSeeds(orm.client);// Reset database (drop, migrate, seed)awaitresetDatabase(orm.client,[User,Role]);

🏥 Health Check

Monitor database and cache connectivity with latency.

consthealth=awaitorm.healthCheck();// { status: "healthy", database: "postgres", latencyMs: 12.5, cacheStatus: "connected" }// Per-table healthconstuserHealth=awaituserRepository.healthCheck();// { status: "healthy", table: "users", rows: 142, latencyMs: 8.3 }

📂 Bulk Upsert

Upsert multiple records in a single transaction.

constusers=awaituserRepository.bulkUpsert([{email: "lwazicd@icloud.com",name: "Lwazi"},{email: "ciniso@icloud.com",name: "Ciniso"},],["email"],// unique key(s));

🔧 Raw SQL

Execute raw SQL queries directly from the ORM instance.

constresults=awaitorm.rawQuery("SELECT * FROM users WHERE age > ?",[25]);const{ affectedRows }=awaitorm.rawExec("UPDATE users SET active = false WHERE last_login < ?",[oneYearAgo],);

🗑️ recoverAll / truncate

// Restore all soft-deleted recordsconstrecovered=awaituserRepository.recoverAll();console.log(`Recovered ${recovered} records`);// Clear the tableawaituserRepository.truncate();

⬆️⬇️ Increment / Decrement

Atomically update numeric fields without loading the record.

constupdated=awaituserRepository.increment(user.id,"loginCount",1);constupdated=awaituserRepository.decrement(user.id,"credits",5);

🏷️ Pluck / SelectColumns

// Get just the email column as an arrayconstemails=awaituserRepository.pluck("email");// ["lwazicd@icloud.com", "ciniso@icloud.com", ...]// Get specific columnsconstusers=awaituserRepository.selectColumns("id","email");// [{ id: 1, email: "lwazicd@icloud.com" }, ...]

🔄 Toggle

Toggle a boolean field.

consttoggled=awaituserRepository.toggle(user.id,"isActive");// isActive was true, now false (or vice versa)

✏️ updateBy / deleteBy

// Update all active users' role to "member"constupdated=awaituserRepository.updateBy({isActive: true},{role: "member"},);// Delete all users with null emailconstdeleted=awaituserRepository.deleteBy({email: null});

🕳️ findDeleted / withTrashed

// Get only soft-deleted recordsconstdeletedUsers=awaituserRepository.findDeleted().execute();// Get all records including soft-deletedconstallUsers=awaituserRepository.withTrashed().execute();// Restore matching soft-deleted recordsconstrestored=awaituserRepository.restoreBy({role: "admin"});

🔄 firstOrCreate / updateOrCreate

// Find or create in one callconstuser=awaituserRepository.firstOrCreate({email: "lwazicd@icloud.com"},{name: "Lwazi"},);// Find and update, or create if not foundconstuser=awaituserRepository.updateOrCreate({email: "lwazicd@icloud.com"},{name: "Updated Name"},);

🥇 first / last / random

constfirstUser=awaituserRepository.first();constadmin=awaituserRepository.first({role: "admin"});constlastUser=awaituserRepository.last();constrandomUser=awaituserRepository.random();

🔒 Pessimistic Locking (lockForUpdate)

constuser=awaituserRepository.lockForUpdate(user.id);// The row is now locked for the duration of the transaction

📡 Event Emitter

Subscribe to ORM lifecycle events.

constorm=newStabilize(dbConfig);orm.events.on("query",(entry)=>{console.log(`[${entry.durationMs}ms] ${entry.query}`);});orm.events.on("error",(err)=>{console.error("ORM error:",err);});orm.events.on("connection:open",(dbType)=>{console.log(`Connected to ${dbType}`);});

📦 Pool Stats

conststats=awaitorm.poolStats();// { active: 5, idle: 10, total: 15 }

🆔 generateUUID

import{generateUUID}from"stabilize-orm";constid=generateUUID();// "550e8400-e29b-41d4-a716-446655440000"

📑 License

Licensed under the MIT License. See LICENSE.md for details.


Created with ❤️ by ElectronSz
File last updated: 2026-04-02

About

A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Stabilize ORM

A Modern, Type-Safe, and Expressive ORM for Bun

Stabilize ORM Logo

NPM VersionLicenseStabilize CLIPostgreSQLMySQLSQLiteBuild StatusMIT License

Stabilize is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for PostgreSQL, MySQL, and SQLite. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.


🚀 Features

  • Unified API: Write once, run on PostgreSQL, MySQL, or SQLite.
  • Programmatic Model Definitions: Define models and columns using the defineModel API with the DataTypes enum for database-agnostic schemas.
  • Full-Featured CLI: Generate models, manage migrations, seed data, and reset your database from the command line with stabilize-cli.
  • Automatic Migrations: Generate database-specific SQL schemas directly from your model definitions.
  • Versioned Models & Time-Travel: Enable versioning in your model configuration for automatic history tables and snapshot queries.
  • Retry Logic: Automatic exponential backoff for database queries to handle transient connection issues.
  • Connection Pooling: Efficient connection management for PostgreSQL and MySQL.
  • Transactional Integrity: Built-in support for atomic transactions with automatic rollback on failure.
  • Advanced Query Builder: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
  • Pagination Helper: Easily paginate any query with .paginate(page, pageSize) and get { data, total, page, pageSize }.
  • Advanced Model Validation: Enforce rules like required, minLength, maxLength, pattern, and custom validators—errors are thrown on invalid input.
  • Model Relationships: Define OneToOne, ManyToOne, OneToMany, and ManyToMany relationships in the model configuration.
  • Soft Deletes: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
  • Lifecycle Hooks: Define hooks in the model configuration or as class methods for lifecycle events like beforeCreate, afterUpdate, etc.
  • Pluggable Logging: Includes a robust StabilizeLogger with support for file-based, rotating logs.
  • Custom Errors: StabilizeError provides clear, consistent error handling.
  • Caching Layer: Optional Redis-backed caching with cache-aside and write-through strategies.
  • Custom Query Scopes: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
  • Timestamps: Automatically manage createdAt and updatedAt columns for tracking record creation and update times.
  • SQL Default Expressions: Support database-side default expressions (e.g., gen_random_uuid(), NOW()) for columns using the sqlDefault() helper.
  • Nested Relations (Eager Loading): Load deeply nested relations using dot notation like "roles.permissions".
  • AutoMigrate with Index Management: Automatically create, detect, and remove indexes and unique constraints during migration.
  • Advanced Query Builder Filters: Chainable .orWhere(), .whereIn(), .whereNotIn(), .whereNull(), .whereNotNull(), .whereBetween(), .groupBy(), .having(), .lock() methods.
  • Optimistic Locking: Add optimisticLock: true to a version column to automatically detect concurrent modification conflicts and throw CONCURRENT_MODIFICATION errors.
  • findAndCount: Get paginated results with a total count in one call.
  • findOneBy / findBy: TypeORM-style conditional finders without writing raw SQL.
  • Aggregate Queries: Run count(), sum(), avg(), min(), max() directly from the repository or query builder.
  • Cursor-Based Pagination: Efficient forward/backward cursor pagination for large datasets (Prisma-style findMany).
  • exists: Check if a record exists without loading it.
  • recoverAll: Bulk restore all soft-deleted records.
  • truncate: Clear all rows from a table.
  • seed / defineSeed: Laravel-style seeding framework with defineSeed and runSeeds.
  • resetDatabase: Drop tables, re-migrate, and optionally re-seed for development.
  • healthCheck: Get database and table health status with latency for monitoring endpoints.
  • rawQuery / rawExec: Execute raw SQL directly from the Stabilize instance.
  • bulkUpsert: Upsert multiple records in a single transaction.
  • findMany: Prisma-style query with where, cursor, take, skip, orderBy.
  • countDistinct: Count unique values in a column.
  • increment / decrement: Atomically increment or decrement a numeric field.
  • pluck: Get an array of a single column's values (Rails-style).
  • selectColumns: Get only specific columns from a query.
  • toggle: Toggle a boolean field (Rails-style).
  • updateBy / deleteBy: Conditional bulk updates and deletes without writing SQL.
  • restoreBy: Restore soft-deleted records matching conditions.
  • findDeleted / withTrashed: Query soft-deleted or all records.
  • upsertMany: Batch upsert in configurable batch sizes.
  • map / each / eachBatch: Transform, iterate, and batch-process query results.
  • lockForUpdate: Pessimistic row locking for read-modify-write.
  • firstOrCreate / updateOrCreate: Laravel-style find-or-create patterns.
  • first / last / random: Single-record shortcuts.
  • StabilizeEmitter: Event system for query, error, connection:open/close, transaction:start/complete/error.
  • TransactionIsolationLevel: Type for READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE.
  • generateUUID: Cross-runtime UUID generation helper.
  • poolStats: Get connection pool statistics.
  • Database Backup & Restore: db:backup and db:restore commands for database backup management.
  • API Generation: generate:api command scaffolds full CRUD REST API routes from models.
  • Fresh Migrations: migrate:fresh drops all tables and re-runs migrations without seeding.
  • Database Size Analysis: db:size command shows table sizes and row count statistics.

📦 Installation

Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).

# Using Bun
bun add stabilize-orm
# Using npm
npm install stabilize-orm

📃 Documentation & Community

Examples

  • Blog - Blog with users, posts, comments, and versioning
  • E-Commerce - Products, categories, orders with transactions
  • REST API - Express.js REST API with pagination and optimistic locking
  • SaaS - Multi-tenant SaaS with tenants, members, and scoped projects
  • CMS - Content management with authors, categories, articles, and versioning
  • Analytics - Event tracking with aggregations and metrics

⚙️ Configuration

Create a database configuration file.

// config/database.tsimport{DBType,typeDBConfig}from"stabilize-orm";constdbConfig: DBConfig={type: DBType.Postgres,connectionString:
process.env.DATABASE_URL||"postgres://user:password@localhost:5432/mydb",retryAttempts: 3,retryDelay: 1000,};exportdefaultdbConfig;

Next, create a central ORM instance for your application.

// db.tsimport{Stabilize,typeCacheConfig,typeLoggerConfig,LogLevel,}from"stabilize-orm";importdbConfigfrom"./database";constcacheConfig: CacheConfig={enabled: process.env.CACHE_ENABLED==="true",redisUrl: process.env.REDIS_URL,ttl: 60,};constloggerConfig: LoggerConfig={level: LogLevel.Info,filePath: "logs/stabilize.log",maxFileSize: 5*1024*1024,// 5MBmaxFiles: 3,};exportconstorm=newStabilize(dbConfig,cacheConfig,loggerConfig);

🏗️ Models & Relationships

Define your tables as classes using the defineModel function. The DataTypes enum ensures database-agnostic schemas.

Example: Users and Roles (Many-to-Many) with Versioning

// models/User.tsimport{defineModel,DataTypes,RelationType}from"stabilize-orm";import{UserRole}from"./UserRole";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true,unique: true,},},relations: [{type: RelationType.OneToMany,target: ()=>UserRole,property: "roles",foreignKey: "userId",},],hooks: {beforeCreate: (entity)=>console.log(`Creating user: ${entity.email}`),},});export{User};

🔍 Pagination

The built-in pagination helper makes it easy to retrieve a page of rows using LIMIT/OFFSET semantics.

constusers=awaituserRepository.find().paginate(2,10).execute(dbClient);// users = [...] // Returns an array of rows for that page

Or, use the query builder:

constusers=awaituserRepository.find().where('isActive = ?',true).paginate(1,20).execute(dbClient);

🛡️ Advanced Validation

Models can define advanced validation rules for columns, including:

  • required
  • minLength / maxLength
  • pattern (RegExp)
  • customValidator (function)

Validation errors are thrown on create/update if data is invalid.

constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,required: true,unique: true,minLength: 6,pattern: /^[^@]+@[^@]+\.[^@]+$/,customValidator: (val)=>val.endsWith("@offbytesecure.com")||"Must use an @offbytesecure.com email",},password: {type: DataTypes.String,minLength: 8},},});

⏳ Versioning & Auditing

Enable automatic history tracking and time-travel queries by setting versioned: true in your model configuration.

  • Each change is recorded in a <table>_history table with version, operation, and audit columns.
  • Supports snapshot queries, rollbacks, audits, and time-travel.

Versioning Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},},});// --- Using versioning features:constuserRepository=orm.getRepository(User);// Rollback to a previous versionawaituserRepository.rollback(1,3);// roll back user with id=1 to version 3// Get a snapshot as of a specific dateconstuserAsOf=awaituserRepository.asOf(1,newDate("2025-01-01T00:00:00Z"));console.log(userAsOf);// View full version historyconsthistory=awaituserRepository.history(1);console.log(history);

🔄 Model Lifecycle Hooks

Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.

Hooks Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},hooks: {beforeCreate: (entity)=>{entity.createdAt=newDate();},beforeUpdate: (entity)=>{entity.updatedAt=newDate();},afterCreate: (entity)=>{console.log(`User created: ${entity.name}`);},},});// Add a hook as a class methodUser.prototype.afterUpdate=asyncfunction(){console.log(`Updated user: ${this.name}`);};export{User};

Supported hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeSave, afterSave.


💻 Command-Line Interface (CLI)

Stabilize includes a powerful CLI for managing your workflow. See: stabilize-cli on GitHub

Generating Files

  • Generate a model:

    stabilize-cli generate:model Product
  • Generate a migration from a model:

    stabilize-cli generate:migration User
  • Generate a seed file:

    stabilize-cli generate:seed InitialRoles
  • Generate a REST API scaffold:

    stabilize-cli generate:api User

Database & Migration Management

  • Run all pending migrations:

    stabilize-cli migrate
  • Roll back the last migration:

    stabilize-cli migrate:rollback
  • Fresh migration (drop + re-migrate):

    stabilize-cli migrate:fresh --force
  • Run all pending seeds (in dependency order):

    stabilize-cli seed
  • Check the status of migrations and seeds:

    stabilize-cli status
  • Reset the database (drop, migrate, seed):

    stabilize-cli db:reset

Backup & Restore

  • Backup the database:

    stabilize-cli db:backup
  • Restore from a backup:

    stabilize-cli db:restore backups/backup_20250101120000.db --force

Diagnostics

  • Database size statistics:

    stabilize-cli db:size
  • Health check:

    stabilize-cli health
  • CLI info:

    stabilize-cli info

🧑‍💻 Querying Data

Basic CRUD with Repositories

import{orm}from"./db";import{User}from"./models/User";constuserRepository=orm.getRepository(User);constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});constfoundUser=awaituserRepository.findOne(newUser.id);constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});awaituserRepository.delete(newUser.id);

Advanced Queries with the Query Builder

constactiveAdmins=awaitorm.getRepository(UserRole).find().join("users","user_roles.user_id = users.id").join("roles","user_roles.role_id = roles.id").select("users.id","users.email","roles.name as role_name").where("roles.name = ?","Admin").orderBy("users.email ASC").execute();console.log(activeAdmins);

Query Builder API

{select(...fields: string[]): QueryBuilder<User>;where(condition: string, ...params: any[]): QueryBuilder<User>;orWhere(condition: string, ...params: any[]): QueryBuilder<User>;whereIn(column: string,values: any[]): QueryBuilder<User>;whereNotIn(column: string,values: any[]): QueryBuilder<User>;whereNull(column: string): QueryBuilder<User>;whereNotNull(column: string): QueryBuilder<User>;whereBetween(column: string,start: any,end: any): QueryBuilder<User>;groupBy(clause: string): QueryBuilder<User>;having(condition: string, ...params: any[]): QueryBuilder<User>;join(table: string,condition: string): QueryBuilder<User>;orderBy(clause: string): QueryBuilder<User>;limit(limit: number): QueryBuilder<User>;offset(offset: number): QueryBuilder<User>;lock(mode?: "FOR UPDATE"|"FOR SHARE"): QueryBuilder<User>;withRelations(...relations: string[]): QueryBuilder<User>;scope(name: string, ...args: any[]): QueryBuilder<User>;paginate(page: number,pageSize: number): QueryBuilder<User>;build(): { query: string; params: any[]};execute(client?: DBClient,cache?: Cache,cacheKey?: string): Promise<User[]>;}

Custom Query Scopes

Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the scope method on Repository or QueryBuilder, allowing you to chain them with other query operations.

Scopes Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},isActive: {type: DataTypes.Boolean,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},scopes: {active: (qb)=>qb.where("isActive = ?",true),recent: (qb,days: number)=>qb.where("createdAt >= ?",newDate(Date.now()-days*24*60*60*1000),),},});constuserRepository=orm.getRepository(User);// Fetch active usersconstactiveUsers=awaituserRepository.scope("active").execute();// Fetch users created in the last 7 daysconstrecentUsers=awaituserRepository.scope("recent",7).execute();// Combine scopes with other query operationsconstrecentActiveUsers=awaituserRepository.scope("active").scope("recent",7).orderBy("createdAt DESC").limit(10).execute();console.log(recentActiveUsers);

Timestamps

Enable automatic management of createdAt and updatedAt columns by setting timestamps in your model configuration. The ORM automatically sets these fields during create, update, bulkCreate, bulkUpdate, and upsert operations in a TypeScript-safe manner, eliminating the need for manual hooks.

Timestamps Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},timestamps: {createdAt: "createdAt",updatedAt: "updatedAt",},});constuserRepository=orm.getRepository(User);// Create a user (createdAt and updatedAt set automatically)constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});console.log(newUser.createdAt,newUser.updatedAt);// Outputs current timestamp// Update a user (updatedAt updated automatically)constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});console.log(updatedUser.updatedAt);// Outputs new timestamp// Bulk create usersconstnewUsers=awaituserRepository.bulkCreate([{email: "user1@example.com"},{email: "user2@example.com"},]);console.log(newUsers.map((u)=>u.createdAt));// Outputs timestamps for each user

🗑️ Soft Deletes

Enable soft deletes by setting softDelete: true and marking a column (e.g., deletedAt) with softDelete: true in the model configuration.

  • Use repository.delete(id) to mark an entity as deleted.
  • Use repository.recover(id) to restore a soft-deleted entity.
  • Queries automatically exclude soft-deleted rows unless specified otherwise.

Soft Delete Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",softDelete: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},deletedAt: {type: DataTypes.DateTime,softDelete: true},},});constuserRepository=orm.getRepository(User);awaituserRepository.create({email: "lwazicd@icloud.com"});awaituserRepository.delete(1);// Soft deleteawaituserRepository.recover(1);// Recover

🌐 Express.js Integration

Stabilize ORM works seamlessly with web frameworks like Express.

importexpressfrom"express";import{orm}from"./db";import{User}from"./models/User";constapp=express();app.use(express.json());constuserRepository=orm.getRepository(User);app.get("/users",async(req,res)=>{try{constusers=awaituserRepository.find().execute();res.json(users);}catch(err){res.status(500).json({error: "Failed to fetch users."});}});app.post("/users",async(req,res)=>{try{constuser=awaituserRepository.create(req.body);res.status(201).json(user);}catch(err){res.status(500).json({error: "User creation failed."});}});app.listen(3000,()=>{console.log("Server listening on port 3000");});

🧑‍🔬 Testing & Time-Travel

  • Use time-travel queries to inspect historical entity states.
  • Assert audit trails and rollback operations in your tests.

🔒 Optimistic Locking

Enable optimistic locking to detect concurrent modifications. Add optimisticLock: true to a version column in your model.

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String},version: {type: DataTypes.Integer,optimisticLock: true},},});constuserRepository=orm.getRepository(User);// Create with initial versionconstuser=awaituserRepository.create({name: "Lwazi",version: 1});// Update - version is automatically incremented// If another transaction modified the record, a CONCURRENT_MODIFICATION error is throwntry{awaituserRepository.update(user.id,{name: "Updated",version: user.version,});}catch(err){if(err.code==="CONCURRENT_MODIFICATION"){console.log("Record was modified by another transaction");}}

⏱️ SQL Default Expressions

Use sqlDefault() to set database-side default values for columns (e.g., gen_random_uuid(), NOW()).

import{defineModel,DataTypes,sqlDefault}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.UUID,required: true,defaultExpression: sqlDefault("gen_random_uuid()"),},name: {type: DataTypes.String},createdAt: {type: DataTypes.DateTime,defaultExpression: sqlDefault("NOW()"),},},});

📊 Advanced Query Builder

The query builder now supports additional filter methods:

constresults=awaituserRepository.find().where("status = ?","active").orWhere("role = ?","admin").whereIn("age",[25,30,35]).whereBetween("createdAt",newDate("2025-01-01"),newDate("2025-12-31")).whereNull("deletedAt").groupBy("department").having("COUNT(*) > ?",5).orderBy("createdAt DESC").limit(10).execute();

🔗 Nested Relations

Load deeply nested relations using dot notation:

constuser=awaituserRepository.findOne(1,{relations: ["roles","roles.permissions"],});

📊 Aggregation Queries

Run aggregate queries directly on the repository or query builder.

// Repository-level aggregatesconsttotal=awaituserRepository.count();constexists=awaituserRepository.exists({email: "admin@offbytesecure.com",});conststats=awaituserRepository.aggregate({count: "*",sum: ["salary"],avg: ["salary"],min: ["salary"],max: ["salary"],});// stats = { count_: 100, sum_salary: 5000000, avg_salary: 50000, min_salary: 20000, max_salary: 150000 }

🔎 findOneBy / findBy

TypeORM-style conditional finders without writing raw SQL.

// Find one record by conditionconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"});// Find multiple recordsconstadmins=awaituserRepository.findBy({role: "admin"},{limit: 10,orderBy: "createdAt DESC"},);// Combined with relationsconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"},{relations: ["roles"]},);

🔢 findAndCount

Get paginated results with a total count in a single call.

const{ data, total }=awaituserRepository.findAndCount();console.log(`Showing ${data.length} of ${total} total records`);

🖱️ Cursor-Based Pagination

Efficient cursor-based pagination for large datasets.

// First pageconstpage1=awaituserRepository.findMany({take: 10,orderBy: {field: "id",direction: "ASC"},});// Next page using cursorconstlastId=page1[page1.length-1].id;constpage2=awaituserRepository.findMany({cursor: {field: "id",value: lastId,direction: "forward"},take: 10,orderBy: {field: "id",direction: "ASC"},});

🌱 Database Seeding

Define and run seeds for populating development/test databases.

import{defineSeed,runSeeds,resetDatabase}from"stabilize-orm";// Define a seeddefineSeed("create-default-roles",async(db)=>{constroleRepo=orm.getRepository(Role);awaitroleRepo.bulkCreate([{name: "Admin",permissions: "all"},{name: "User",permissions: "read"},]);});// Run all seedsawaitrunSeeds(orm.client);// Reset database (drop, migrate, seed)awaitresetDatabase(orm.client,[User,Role]);

🏥 Health Check

Monitor database and cache connectivity with latency.

consthealth=awaitorm.healthCheck();// { status: "healthy", database: "postgres", latencyMs: 12.5, cacheStatus: "connected" }// Per-table healthconstuserHealth=awaituserRepository.healthCheck();// { status: "healthy", table: "users", rows: 142, latencyMs: 8.3 }

📂 Bulk Upsert

Upsert multiple records in a single transaction.

constusers=awaituserRepository.bulkUpsert([{email: "lwazicd@icloud.com",name: "Lwazi"},{email: "ciniso@icloud.com",name: "Ciniso"},],["email"],// unique key(s));

🔧 Raw SQL

Execute raw SQL queries directly from the ORM instance.

constresults=awaitorm.rawQuery("SELECT * FROM users WHERE age > ?",[25]);const{ affectedRows }=awaitorm.rawExec("UPDATE users SET active = false WHERE last_login < ?",[oneYearAgo],);

🗑️ recoverAll / truncate

// Restore all soft-deleted recordsconstrecovered=awaituserRepository.recoverAll();console.log(`Recovered ${recovered} records`);// Clear the tableawaituserRepository.truncate();

⬆️⬇️ Increment / Decrement

Atomically update numeric fields without loading the record.

constupdated=awaituserRepository.increment(user.id,"loginCount",1);constupdated=awaituserRepository.decrement(user.id,"credits",5);

🏷️ Pluck / SelectColumns

// Get just the email column as an arrayconstemails=awaituserRepository.pluck("email");// ["lwazicd@icloud.com", "ciniso@icloud.com", ...]// Get specific columnsconstusers=awaituserRepository.selectColumns("id","email");// [{ id: 1, email: "lwazicd@icloud.com" }, ...]

🔄 Toggle

Toggle a boolean field.

consttoggled=awaituserRepository.toggle(user.id,"isActive");// isActive was true, now false (or vice versa)

✏️ updateBy / deleteBy

// Update all active users' role to "member"constupdated=awaituserRepository.updateBy({isActive: true},{role: "member"},);// Delete all users with null emailconstdeleted=awaituserRepository.deleteBy({email: null});

🕳️ findDeleted / withTrashed

// Get only soft-deleted recordsconstdeletedUsers=awaituserRepository.findDeleted().execute();// Get all records including soft-deletedconstallUsers=awaituserRepository.withTrashed().execute();// Restore matching soft-deleted recordsconstrestored=awaituserRepository.restoreBy({role: "admin"});

🔄 firstOrCreate / updateOrCreate

// Find or create in one callconstuser=awaituserRepository.firstOrCreate({email: "lwazicd@icloud.com"},{name: "Lwazi"},);// Find and update, or create if not foundconstuser=awaituserRepository.updateOrCreate({email: "lwazicd@icloud.com"},{name: "Updated Name"},);

🥇 first / last / random

constfirstUser=awaituserRepository.first();constadmin=awaituserRepository.first({role: "admin"});constlastUser=awaituserRepository.last();constrandomUser=awaituserRepository.random();

🔒 Pessimistic Locking (lockForUpdate)

constuser=awaituserRepository.lockForUpdate(user.id);// The row is now locked for the duration of the transaction

📡 Event Emitter

Subscribe to ORM lifecycle events.

constorm=newStabilize(dbConfig);orm.events.on("query",(entry)=>{console.log(`[${entry.durationMs}ms] ${entry.query}`);});orm.events.on("error",(err)=>{console.error("ORM error:",err);});orm.events.on("connection:open",(dbType)=>{console.log(`Connected to ${dbType}`);});

📦 Pool Stats

conststats=awaitorm.poolStats();// { active: 5, idle: 10, total: 15 }

🆔 generateUUID

import{generateUUID}from"stabilize-orm";constid=generateUUID();// "550e8400-e29b-41d4-a716-446655440000"

📑 License

Licensed under the MIT License. See LICENSE.md for details.


Created with ❤️ by ElectronSz
File last updated: 2026-04-02

About

A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Stabilize ORM

A Modern, Type-Safe, and Expressive ORM for Bun

Stabilize ORM Logo

NPM VersionLicenseStabilize CLIPostgreSQLMySQLSQLiteBuild StatusMIT License

Stabilize is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for PostgreSQL, MySQL, and SQLite. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.


🚀 Features

  • Unified API: Write once, run on PostgreSQL, MySQL, or SQLite.
  • Programmatic Model Definitions: Define models and columns using the defineModel API with the DataTypes enum for database-agnostic schemas.
  • Full-Featured CLI: Generate models, manage migrations, seed data, and reset your database from the command line with stabilize-cli.
  • Automatic Migrations: Generate database-specific SQL schemas directly from your model definitions.
  • Versioned Models & Time-Travel: Enable versioning in your model configuration for automatic history tables and snapshot queries.
  • Retry Logic: Automatic exponential backoff for database queries to handle transient connection issues.
  • Connection Pooling: Efficient connection management for PostgreSQL and MySQL.
  • Transactional Integrity: Built-in support for atomic transactions with automatic rollback on failure.
  • Advanced Query Builder: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
  • Pagination Helper: Easily paginate any query with .paginate(page, pageSize) and get { data, total, page, pageSize }.
  • Advanced Model Validation: Enforce rules like required, minLength, maxLength, pattern, and custom validators—errors are thrown on invalid input.
  • Model Relationships: Define OneToOne, ManyToOne, OneToMany, and ManyToMany relationships in the model configuration.
  • Soft Deletes: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
  • Lifecycle Hooks: Define hooks in the model configuration or as class methods for lifecycle events like beforeCreate, afterUpdate, etc.
  • Pluggable Logging: Includes a robust StabilizeLogger with support for file-based, rotating logs.
  • Custom Errors: StabilizeError provides clear, consistent error handling.
  • Caching Layer: Optional Redis-backed caching with cache-aside and write-through strategies.
  • Custom Query Scopes: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
  • Timestamps: Automatically manage createdAt and updatedAt columns for tracking record creation and update times.
  • SQL Default Expressions: Support database-side default expressions (e.g., gen_random_uuid(), NOW()) for columns using the sqlDefault() helper.
  • Nested Relations (Eager Loading): Load deeply nested relations using dot notation like "roles.permissions".
  • AutoMigrate with Index Management: Automatically create, detect, and remove indexes and unique constraints during migration.
  • Advanced Query Builder Filters: Chainable .orWhere(), .whereIn(), .whereNotIn(), .whereNull(), .whereNotNull(), .whereBetween(), .groupBy(), .having(), .lock() methods.
  • Optimistic Locking: Add optimisticLock: true to a version column to automatically detect concurrent modification conflicts and throw CONCURRENT_MODIFICATION errors.
  • findAndCount: Get paginated results with a total count in one call.
  • findOneBy / findBy: TypeORM-style conditional finders without writing raw SQL.
  • Aggregate Queries: Run count(), sum(), avg(), min(), max() directly from the repository or query builder.
  • Cursor-Based Pagination: Efficient forward/backward cursor pagination for large datasets (Prisma-style findMany).
  • exists: Check if a record exists without loading it.
  • recoverAll: Bulk restore all soft-deleted records.
  • truncate: Clear all rows from a table.
  • seed / defineSeed: Laravel-style seeding framework with defineSeed and runSeeds.
  • resetDatabase: Drop tables, re-migrate, and optionally re-seed for development.
  • healthCheck: Get database and table health status with latency for monitoring endpoints.
  • rawQuery / rawExec: Execute raw SQL directly from the Stabilize instance.
  • bulkUpsert: Upsert multiple records in a single transaction.
  • findMany: Prisma-style query with where, cursor, take, skip, orderBy.
  • countDistinct: Count unique values in a column.
  • increment / decrement: Atomically increment or decrement a numeric field.
  • pluck: Get an array of a single column's values (Rails-style).
  • selectColumns: Get only specific columns from a query.
  • toggle: Toggle a boolean field (Rails-style).
  • updateBy / deleteBy: Conditional bulk updates and deletes without writing SQL.
  • restoreBy: Restore soft-deleted records matching conditions.
  • findDeleted / withTrashed: Query soft-deleted or all records.
  • upsertMany: Batch upsert in configurable batch sizes.
  • map / each / eachBatch: Transform, iterate, and batch-process query results.
  • lockForUpdate: Pessimistic row locking for read-modify-write.
  • firstOrCreate / updateOrCreate: Laravel-style find-or-create patterns.
  • first / last / random: Single-record shortcuts.
  • StabilizeEmitter: Event system for query, error, connection:open/close, transaction:start/complete/error.
  • TransactionIsolationLevel: Type for READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE.
  • generateUUID: Cross-runtime UUID generation helper.
  • poolStats: Get connection pool statistics.
  • Database Backup & Restore: db:backup and db:restore commands for database backup management.
  • API Generation: generate:api command scaffolds full CRUD REST API routes from models.
  • Fresh Migrations: migrate:fresh drops all tables and re-runs migrations without seeding.
  • Database Size Analysis: db:size command shows table sizes and row count statistics.

📦 Installation

Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).

# Using Bun
bun add stabilize-orm
# Using npm
npm install stabilize-orm

📃 Documentation & Community

Examples

  • Blog - Blog with users, posts, comments, and versioning
  • E-Commerce - Products, categories, orders with transactions
  • REST API - Express.js REST API with pagination and optimistic locking
  • SaaS - Multi-tenant SaaS with tenants, members, and scoped projects
  • CMS - Content management with authors, categories, articles, and versioning
  • Analytics - Event tracking with aggregations and metrics

⚙️ Configuration

Create a database configuration file.

// config/database.tsimport{DBType,typeDBConfig}from"stabilize-orm";constdbConfig: DBConfig={type: DBType.Postgres,connectionString:
process.env.DATABASE_URL||"postgres://user:password@localhost:5432/mydb",retryAttempts: 3,retryDelay: 1000,};exportdefaultdbConfig;

Next, create a central ORM instance for your application.

// db.tsimport{Stabilize,typeCacheConfig,typeLoggerConfig,LogLevel,}from"stabilize-orm";importdbConfigfrom"./database";constcacheConfig: CacheConfig={enabled: process.env.CACHE_ENABLED==="true",redisUrl: process.env.REDIS_URL,ttl: 60,};constloggerConfig: LoggerConfig={level: LogLevel.Info,filePath: "logs/stabilize.log",maxFileSize: 5*1024*1024,// 5MBmaxFiles: 3,};exportconstorm=newStabilize(dbConfig,cacheConfig,loggerConfig);

🏗️ Models & Relationships

Define your tables as classes using the defineModel function. The DataTypes enum ensures database-agnostic schemas.

Example: Users and Roles (Many-to-Many) with Versioning

// models/User.tsimport{defineModel,DataTypes,RelationType}from"stabilize-orm";import{UserRole}from"./UserRole";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true,unique: true,},},relations: [{type: RelationType.OneToMany,target: ()=>UserRole,property: "roles",foreignKey: "userId",},],hooks: {beforeCreate: (entity)=>console.log(`Creating user: ${entity.email}`),},});export{User};

🔍 Pagination

The built-in pagination helper makes it easy to retrieve a page of rows using LIMIT/OFFSET semantics.

constusers=awaituserRepository.find().paginate(2,10).execute(dbClient);// users = [...] // Returns an array of rows for that page

Or, use the query builder:

constusers=awaituserRepository.find().where('isActive = ?',true).paginate(1,20).execute(dbClient);

🛡️ Advanced Validation

Models can define advanced validation rules for columns, including:

  • required
  • minLength / maxLength
  • pattern (RegExp)
  • customValidator (function)

Validation errors are thrown on create/update if data is invalid.

constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,required: true,unique: true,minLength: 6,pattern: /^[^@]+@[^@]+\.[^@]+$/,customValidator: (val)=>val.endsWith("@offbytesecure.com")||"Must use an @offbytesecure.com email",},password: {type: DataTypes.String,minLength: 8},},});

⏳ Versioning & Auditing

Enable automatic history tracking and time-travel queries by setting versioned: true in your model configuration.

  • Each change is recorded in a <table>_history table with version, operation, and audit columns.
  • Supports snapshot queries, rollbacks, audits, and time-travel.

Versioning Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},},});// --- Using versioning features:constuserRepository=orm.getRepository(User);// Rollback to a previous versionawaituserRepository.rollback(1,3);// roll back user with id=1 to version 3// Get a snapshot as of a specific dateconstuserAsOf=awaituserRepository.asOf(1,newDate("2025-01-01T00:00:00Z"));console.log(userAsOf);// View full version historyconsthistory=awaituserRepository.history(1);console.log(history);

🔄 Model Lifecycle Hooks

Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.

Hooks Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},hooks: {beforeCreate: (entity)=>{entity.createdAt=newDate();},beforeUpdate: (entity)=>{entity.updatedAt=newDate();},afterCreate: (entity)=>{console.log(`User created: ${entity.name}`);},},});// Add a hook as a class methodUser.prototype.afterUpdate=asyncfunction(){console.log(`Updated user: ${this.name}`);};export{User};

Supported hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeSave, afterSave.


💻 Command-Line Interface (CLI)

Stabilize includes a powerful CLI for managing your workflow. See: stabilize-cli on GitHub

Generating Files

  • Generate a model:

    stabilize-cli generate:model Product
  • Generate a migration from a model:

    stabilize-cli generate:migration User
  • Generate a seed file:

    stabilize-cli generate:seed InitialRoles
  • Generate a REST API scaffold:

    stabilize-cli generate:api User

Database & Migration Management

  • Run all pending migrations:

    stabilize-cli migrate
  • Roll back the last migration:

    stabilize-cli migrate:rollback
  • Fresh migration (drop + re-migrate):

    stabilize-cli migrate:fresh --force
  • Run all pending seeds (in dependency order):

    stabilize-cli seed
  • Check the status of migrations and seeds:

    stabilize-cli status
  • Reset the database (drop, migrate, seed):

    stabilize-cli db:reset

Backup & Restore

  • Backup the database:

    stabilize-cli db:backup
  • Restore from a backup:

    stabilize-cli db:restore backups/backup_20250101120000.db --force

Diagnostics

  • Database size statistics:

    stabilize-cli db:size
  • Health check:

    stabilize-cli health
  • CLI info:

    stabilize-cli info

🧑‍💻 Querying Data

Basic CRUD with Repositories

import{orm}from"./db";import{User}from"./models/User";constuserRepository=orm.getRepository(User);constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});constfoundUser=awaituserRepository.findOne(newUser.id);constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});awaituserRepository.delete(newUser.id);

Advanced Queries with the Query Builder

constactiveAdmins=awaitorm.getRepository(UserRole).find().join("users","user_roles.user_id = users.id").join("roles","user_roles.role_id = roles.id").select("users.id","users.email","roles.name as role_name").where("roles.name = ?","Admin").orderBy("users.email ASC").execute();console.log(activeAdmins);

Query Builder API

{select(...fields: string[]): QueryBuilder<User>;where(condition: string, ...params: any[]): QueryBuilder<User>;orWhere(condition: string, ...params: any[]): QueryBuilder<User>;whereIn(column: string,values: any[]): QueryBuilder<User>;whereNotIn(column: string,values: any[]): QueryBuilder<User>;whereNull(column: string): QueryBuilder<User>;whereNotNull(column: string): QueryBuilder<User>;whereBetween(column: string,start: any,end: any): QueryBuilder<User>;groupBy(clause: string): QueryBuilder<User>;having(condition: string, ...params: any[]): QueryBuilder<User>;join(table: string,condition: string): QueryBuilder<User>;orderBy(clause: string): QueryBuilder<User>;limit(limit: number): QueryBuilder<User>;offset(offset: number): QueryBuilder<User>;lock(mode?: "FOR UPDATE"|"FOR SHARE"): QueryBuilder<User>;withRelations(...relations: string[]): QueryBuilder<User>;scope(name: string, ...args: any[]): QueryBuilder<User>;paginate(page: number,pageSize: number): QueryBuilder<User>;build(): { query: string; params: any[]};execute(client?: DBClient,cache?: Cache,cacheKey?: string): Promise<User[]>;}

Custom Query Scopes

Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the scope method on Repository or QueryBuilder, allowing you to chain them with other query operations.

Scopes Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},isActive: {type: DataTypes.Boolean,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},scopes: {active: (qb)=>qb.where("isActive = ?",true),recent: (qb,days: number)=>qb.where("createdAt >= ?",newDate(Date.now()-days*24*60*60*1000),),},});constuserRepository=orm.getRepository(User);// Fetch active usersconstactiveUsers=awaituserRepository.scope("active").execute();// Fetch users created in the last 7 daysconstrecentUsers=awaituserRepository.scope("recent",7).execute();// Combine scopes with other query operationsconstrecentActiveUsers=awaituserRepository.scope("active").scope("recent",7).orderBy("createdAt DESC").limit(10).execute();console.log(recentActiveUsers);

Timestamps

Enable automatic management of createdAt and updatedAt columns by setting timestamps in your model configuration. The ORM automatically sets these fields during create, update, bulkCreate, bulkUpdate, and upsert operations in a TypeScript-safe manner, eliminating the need for manual hooks.

Timestamps Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},timestamps: {createdAt: "createdAt",updatedAt: "updatedAt",},});constuserRepository=orm.getRepository(User);// Create a user (createdAt and updatedAt set automatically)constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});console.log(newUser.createdAt,newUser.updatedAt);// Outputs current timestamp// Update a user (updatedAt updated automatically)constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});console.log(updatedUser.updatedAt);// Outputs new timestamp// Bulk create usersconstnewUsers=awaituserRepository.bulkCreate([{email: "user1@example.com"},{email: "user2@example.com"},]);console.log(newUsers.map((u)=>u.createdAt));// Outputs timestamps for each user

🗑️ Soft Deletes

Enable soft deletes by setting softDelete: true and marking a column (e.g., deletedAt) with softDelete: true in the model configuration.

  • Use repository.delete(id) to mark an entity as deleted.
  • Use repository.recover(id) to restore a soft-deleted entity.
  • Queries automatically exclude soft-deleted rows unless specified otherwise.

Soft Delete Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",softDelete: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},deletedAt: {type: DataTypes.DateTime,softDelete: true},},});constuserRepository=orm.getRepository(User);awaituserRepository.create({email: "lwazicd@icloud.com"});awaituserRepository.delete(1);// Soft deleteawaituserRepository.recover(1);// Recover

🌐 Express.js Integration

Stabilize ORM works seamlessly with web frameworks like Express.

importexpressfrom"express";import{orm}from"./db";import{User}from"./models/User";constapp=express();app.use(express.json());constuserRepository=orm.getRepository(User);app.get("/users",async(req,res)=>{try{constusers=awaituserRepository.find().execute();res.json(users);}catch(err){res.status(500).json({error: "Failed to fetch users."});}});app.post("/users",async(req,res)=>{try{constuser=awaituserRepository.create(req.body);res.status(201).json(user);}catch(err){res.status(500).json({error: "User creation failed."});}});app.listen(3000,()=>{console.log("Server listening on port 3000");});

🧑‍🔬 Testing & Time-Travel

  • Use time-travel queries to inspect historical entity states.
  • Assert audit trails and rollback operations in your tests.

🔒 Optimistic Locking

Enable optimistic locking to detect concurrent modifications. Add optimisticLock: true to a version column in your model.

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String},version: {type: DataTypes.Integer,optimisticLock: true},},});constuserRepository=orm.getRepository(User);// Create with initial versionconstuser=awaituserRepository.create({name: "Lwazi",version: 1});// Update - version is automatically incremented// If another transaction modified the record, a CONCURRENT_MODIFICATION error is throwntry{awaituserRepository.update(user.id,{name: "Updated",version: user.version,});}catch(err){if(err.code==="CONCURRENT_MODIFICATION"){console.log("Record was modified by another transaction");}}

⏱️ SQL Default Expressions

Use sqlDefault() to set database-side default values for columns (e.g., gen_random_uuid(), NOW()).

import{defineModel,DataTypes,sqlDefault}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.UUID,required: true,defaultExpression: sqlDefault("gen_random_uuid()"),},name: {type: DataTypes.String},createdAt: {type: DataTypes.DateTime,defaultExpression: sqlDefault("NOW()"),},},});

📊 Advanced Query Builder

The query builder now supports additional filter methods:

constresults=awaituserRepository.find().where("status = ?","active").orWhere("role = ?","admin").whereIn("age",[25,30,35]).whereBetween("createdAt",newDate("2025-01-01"),newDate("2025-12-31")).whereNull("deletedAt").groupBy("department").having("COUNT(*) > ?",5).orderBy("createdAt DESC").limit(10).execute();

🔗 Nested Relations

Load deeply nested relations using dot notation:

constuser=awaituserRepository.findOne(1,{relations: ["roles","roles.permissions"],});

📊 Aggregation Queries

Run aggregate queries directly on the repository or query builder.

// Repository-level aggregatesconsttotal=awaituserRepository.count();constexists=awaituserRepository.exists({email: "admin@offbytesecure.com",});conststats=awaituserRepository.aggregate({count: "*",sum: ["salary"],avg: ["salary"],min: ["salary"],max: ["salary"],});// stats = { count_: 100, sum_salary: 5000000, avg_salary: 50000, min_salary: 20000, max_salary: 150000 }

🔎 findOneBy / findBy

TypeORM-style conditional finders without writing raw SQL.

// Find one record by conditionconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"});// Find multiple recordsconstadmins=awaituserRepository.findBy({role: "admin"},{limit: 10,orderBy: "createdAt DESC"},);// Combined with relationsconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"},{relations: ["roles"]},);

🔢 findAndCount

Get paginated results with a total count in a single call.

const{ data, total }=awaituserRepository.findAndCount();console.log(`Showing ${data.length} of ${total} total records`);

🖱️ Cursor-Based Pagination

Efficient cursor-based pagination for large datasets.

// First pageconstpage1=awaituserRepository.findMany({take: 10,orderBy: {field: "id",direction: "ASC"},});// Next page using cursorconstlastId=page1[page1.length-1].id;constpage2=awaituserRepository.findMany({cursor: {field: "id",value: lastId,direction: "forward"},take: 10,orderBy: {field: "id",direction: "ASC"},});

🌱 Database Seeding

Define and run seeds for populating development/test databases.

import{defineSeed,runSeeds,resetDatabase}from"stabilize-orm";// Define a seeddefineSeed("create-default-roles",async(db)=>{constroleRepo=orm.getRepository(Role);awaitroleRepo.bulkCreate([{name: "Admin",permissions: "all"},{name: "User",permissions: "read"},]);});// Run all seedsawaitrunSeeds(orm.client);// Reset database (drop, migrate, seed)awaitresetDatabase(orm.client,[User,Role]);

🏥 Health Check

Monitor database and cache connectivity with latency.

consthealth=awaitorm.healthCheck();// { status: "healthy", database: "postgres", latencyMs: 12.5, cacheStatus: "connected" }// Per-table healthconstuserHealth=awaituserRepository.healthCheck();// { status: "healthy", table: "users", rows: 142, latencyMs: 8.3 }

📂 Bulk Upsert

Upsert multiple records in a single transaction.

constusers=awaituserRepository.bulkUpsert([{email: "lwazicd@icloud.com",name: "Lwazi"},{email: "ciniso@icloud.com",name: "Ciniso"},],["email"],// unique key(s));

🔧 Raw SQL

Execute raw SQL queries directly from the ORM instance.

constresults=awaitorm.rawQuery("SELECT * FROM users WHERE age > ?",[25]);const{ affectedRows }=awaitorm.rawExec("UPDATE users SET active = false WHERE last_login < ?",[oneYearAgo],);

🗑️ recoverAll / truncate

// Restore all soft-deleted recordsconstrecovered=awaituserRepository.recoverAll();console.log(`Recovered ${recovered} records`);// Clear the tableawaituserRepository.truncate();

⬆️⬇️ Increment / Decrement

Atomically update numeric fields without loading the record.

constupdated=awaituserRepository.increment(user.id,"loginCount",1);constupdated=awaituserRepository.decrement(user.id,"credits",5);

🏷️ Pluck / SelectColumns

// Get just the email column as an arrayconstemails=awaituserRepository.pluck("email");// ["lwazicd@icloud.com", "ciniso@icloud.com", ...]// Get specific columnsconstusers=awaituserRepository.selectColumns("id","email");// [{ id: 1, email: "lwazicd@icloud.com" }, ...]

🔄 Toggle

Toggle a boolean field.

consttoggled=awaituserRepository.toggle(user.id,"isActive");// isActive was true, now false (or vice versa)

✏️ updateBy / deleteBy

// Update all active users' role to "member"constupdated=awaituserRepository.updateBy({isActive: true},{role: "member"},);// Delete all users with null emailconstdeleted=awaituserRepository.deleteBy({email: null});

🕳️ findDeleted / withTrashed

// Get only soft-deleted recordsconstdeletedUsers=awaituserRepository.findDeleted().execute();// Get all records including soft-deletedconstallUsers=awaituserRepository.withTrashed().execute();// Restore matching soft-deleted recordsconstrestored=awaituserRepository.restoreBy({role: "admin"});

🔄 firstOrCreate / updateOrCreate

// Find or create in one callconstuser=awaituserRepository.firstOrCreate({email: "lwazicd@icloud.com"},{name: "Lwazi"},);// Find and update, or create if not foundconstuser=awaituserRepository.updateOrCreate({email: "lwazicd@icloud.com"},{name: "Updated Name"},);

🥇 first / last / random

constfirstUser=awaituserRepository.first();constadmin=awaituserRepository.first({role: "admin"});constlastUser=awaituserRepository.last();constrandomUser=awaituserRepository.random();

🔒 Pessimistic Locking (lockForUpdate)

constuser=awaituserRepository.lockForUpdate(user.id);// The row is now locked for the duration of the transaction

📡 Event Emitter

Subscribe to ORM lifecycle events.

constorm=newStabilize(dbConfig);orm.events.on("query",(entry)=>{console.log(`[${entry.durationMs}ms] ${entry.query}`);});orm.events.on("error",(err)=>{console.error("ORM error:",err);});orm.events.on("connection:open",(dbType)=>{console.log(`Connected to ${dbType}`);});

📦 Pool Stats

conststats=awaitorm.poolStats();// { active: 5, idle: 10, total: 15 }

🆔 generateUUID

import{generateUUID}from"stabilize-orm";constid=generateUUID();// "550e8400-e29b-41d4-a716-446655440000"

📑 License

Licensed under the MIT License. See LICENSE.md for details.


Created with ❤️ by ElectronSz
File last updated: 2026-04-02

About

A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Latest commit

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Stabilize ORM

A Modern, Type-Safe, and Expressive ORM for Bun

Stabilize ORM Logo

NPM VersionLicenseStabilize CLIPostgreSQLMySQLSQLiteBuild StatusMIT License

Stabilize is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for PostgreSQL, MySQL, and SQLite. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.


🚀 Features

  • Unified API: Write once, run on PostgreSQL, MySQL, or SQLite.
  • Programmatic Model Definitions: Define models and columns using the defineModel API with the DataTypes enum for database-agnostic schemas.
  • Full-Featured CLI: Generate models, manage migrations, seed data, and reset your database from the command line with stabilize-cli.
  • Automatic Migrations: Generate database-specific SQL schemas directly from your model definitions.
  • Versioned Models & Time-Travel: Enable versioning in your model configuration for automatic history tables and snapshot queries.
  • Retry Logic: Automatic exponential backoff for database queries to handle transient connection issues.
  • Connection Pooling: Efficient connection management for PostgreSQL and MySQL.
  • Transactional Integrity: Built-in support for atomic transactions with automatic rollback on failure.
  • Advanced Query Builder: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
  • Pagination Helper: Easily paginate any query with .paginate(page, pageSize) and get { data, total, page, pageSize }.
  • Advanced Model Validation: Enforce rules like required, minLength, maxLength, pattern, and custom validators—errors are thrown on invalid input.
  • Model Relationships: Define OneToOne, ManyToOne, OneToMany, and ManyToMany relationships in the model configuration.
  • Soft Deletes: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
  • Lifecycle Hooks: Define hooks in the model configuration or as class methods for lifecycle events like beforeCreate, afterUpdate, etc.
  • Pluggable Logging: Includes a robust StabilizeLogger with support for file-based, rotating logs.
  • Custom Errors: StabilizeError provides clear, consistent error handling.
  • Caching Layer: Optional Redis-backed caching with cache-aside and write-through strategies.
  • Custom Query Scopes: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
  • Timestamps: Automatically manage createdAt and updatedAt columns for tracking record creation and update times.
  • SQL Default Expressions: Support database-side default expressions (e.g., gen_random_uuid(), NOW()) for columns using the sqlDefault() helper.
  • Nested Relations (Eager Loading): Load deeply nested relations using dot notation like "roles.permissions".
  • AutoMigrate with Index Management: Automatically create, detect, and remove indexes and unique constraints during migration.
  • Advanced Query Builder Filters: Chainable .orWhere(), .whereIn(), .whereNotIn(), .whereNull(), .whereNotNull(), .whereBetween(), .groupBy(), .having(), .lock() methods.
  • Optimistic Locking: Add optimisticLock: true to a version column to automatically detect concurrent modification conflicts and throw CONCURRENT_MODIFICATION errors.
  • findAndCount: Get paginated results with a total count in one call.
  • findOneBy / findBy: TypeORM-style conditional finders without writing raw SQL.
  • Aggregate Queries: Run count(), sum(), avg(), min(), max() directly from the repository or query builder.
  • Cursor-Based Pagination: Efficient forward/backward cursor pagination for large datasets (Prisma-style findMany).
  • exists: Check if a record exists without loading it.
  • recoverAll: Bulk restore all soft-deleted records.
  • truncate: Clear all rows from a table.
  • seed / defineSeed: Laravel-style seeding framework with defineSeed and runSeeds.
  • resetDatabase: Drop tables, re-migrate, and optionally re-seed for development.
  • healthCheck: Get database and table health status with latency for monitoring endpoints.
  • rawQuery / rawExec: Execute raw SQL directly from the Stabilize instance.
  • bulkUpsert: Upsert multiple records in a single transaction.
  • findMany: Prisma-style query with where, cursor, take, skip, orderBy.
  • countDistinct: Count unique values in a column.
  • increment / decrement: Atomically increment or decrement a numeric field.
  • pluck: Get an array of a single column's values (Rails-style).
  • selectColumns: Get only specific columns from a query.
  • toggle: Toggle a boolean field (Rails-style).
  • updateBy / deleteBy: Conditional bulk updates and deletes without writing SQL.
  • restoreBy: Restore soft-deleted records matching conditions.
  • findDeleted / withTrashed: Query soft-deleted or all records.
  • upsertMany: Batch upsert in configurable batch sizes.
  • map / each / eachBatch: Transform, iterate, and batch-process query results.
  • lockForUpdate: Pessimistic row locking for read-modify-write.
  • firstOrCreate / updateOrCreate: Laravel-style find-or-create patterns.
  • first / last / random: Single-record shortcuts.
  • StabilizeEmitter: Event system for query, error, connection:open/close, transaction:start/complete/error.
  • TransactionIsolationLevel: Type for READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE.
  • generateUUID: Cross-runtime UUID generation helper.
  • poolStats: Get connection pool statistics.
  • Database Backup & Restore: db:backup and db:restore commands for database backup management.
  • API Generation: generate:api command scaffolds full CRUD REST API routes from models.
  • Fresh Migrations: migrate:fresh drops all tables and re-runs migrations without seeding.
  • Database Size Analysis: db:size command shows table sizes and row count statistics.

📦 Installation

Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).

# Using Bun
bun add stabilize-orm
# Using npm
npm install stabilize-orm

📃 Documentation & Community

Examples

  • Blog - Blog with users, posts, comments, and versioning
  • E-Commerce - Products, categories, orders with transactions
  • REST API - Express.js REST API with pagination and optimistic locking
  • SaaS - Multi-tenant SaaS with tenants, members, and scoped projects
  • CMS - Content management with authors, categories, articles, and versioning
  • Analytics - Event tracking with aggregations and metrics

⚙️ Configuration

Create a database configuration file.

// config/database.tsimport{DBType,typeDBConfig}from"stabilize-orm";constdbConfig: DBConfig={type: DBType.Postgres,connectionString:
process.env.DATABASE_URL||"postgres://user:password@localhost:5432/mydb",retryAttempts: 3,retryDelay: 1000,};exportdefaultdbConfig;

Next, create a central ORM instance for your application.

// db.tsimport{Stabilize,typeCacheConfig,typeLoggerConfig,LogLevel,}from"stabilize-orm";importdbConfigfrom"./database";constcacheConfig: CacheConfig={enabled: process.env.CACHE_ENABLED==="true",redisUrl: process.env.REDIS_URL,ttl: 60,};constloggerConfig: LoggerConfig={level: LogLevel.Info,filePath: "logs/stabilize.log",maxFileSize: 5*1024*1024,// 5MBmaxFiles: 3,};exportconstorm=newStabilize(dbConfig,cacheConfig,loggerConfig);

🏗️ Models & Relationships

Define your tables as classes using the defineModel function. The DataTypes enum ensures database-agnostic schemas.

Example: Users and Roles (Many-to-Many) with Versioning

// models/User.tsimport{defineModel,DataTypes,RelationType}from"stabilize-orm";import{UserRole}from"./UserRole";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true,unique: true,},},relations: [{type: RelationType.OneToMany,target: ()=>UserRole,property: "roles",foreignKey: "userId",},],hooks: {beforeCreate: (entity)=>console.log(`Creating user: ${entity.email}`),},});export{User};

🔍 Pagination

The built-in pagination helper makes it easy to retrieve a page of rows using LIMIT/OFFSET semantics.

constusers=awaituserRepository.find().paginate(2,10).execute(dbClient);// users = [...] // Returns an array of rows for that page

Or, use the query builder:

constusers=awaituserRepository.find().where('isActive = ?',true).paginate(1,20).execute(dbClient);

🛡️ Advanced Validation

Models can define advanced validation rules for columns, including:

  • required
  • minLength / maxLength
  • pattern (RegExp)
  • customValidator (function)

Validation errors are thrown on create/update if data is invalid.

constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,required: true,unique: true,minLength: 6,pattern: /^[^@]+@[^@]+\.[^@]+$/,customValidator: (val)=>val.endsWith("@offbytesecure.com")||"Must use an @offbytesecure.com email",},password: {type: DataTypes.String,minLength: 8},},});

⏳ Versioning & Auditing

Enable automatic history tracking and time-travel queries by setting versioned: true in your model configuration.

  • Each change is recorded in a <table>_history table with version, operation, and audit columns.
  • Supports snapshot queries, rollbacks, audits, and time-travel.

Versioning Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},},});// --- Using versioning features:constuserRepository=orm.getRepository(User);// Rollback to a previous versionawaituserRepository.rollback(1,3);// roll back user with id=1 to version 3// Get a snapshot as of a specific dateconstuserAsOf=awaituserRepository.asOf(1,newDate("2025-01-01T00:00:00Z"));console.log(userAsOf);// View full version historyconsthistory=awaituserRepository.history(1);console.log(history);

🔄 Model Lifecycle Hooks

Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.

Hooks Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},hooks: {beforeCreate: (entity)=>{entity.createdAt=newDate();},beforeUpdate: (entity)=>{entity.updatedAt=newDate();},afterCreate: (entity)=>{console.log(`User created: ${entity.name}`);},},});// Add a hook as a class methodUser.prototype.afterUpdate=asyncfunction(){console.log(`Updated user: ${this.name}`);};export{User};

Supported hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeSave, afterSave.


💻 Command-Line Interface (CLI)

Stabilize includes a powerful CLI for managing your workflow. See: stabilize-cli on GitHub

Generating Files

  • Generate a model:

    stabilize-cli generate:model Product
  • Generate a migration from a model:

    stabilize-cli generate:migration User
  • Generate a seed file:

    stabilize-cli generate:seed InitialRoles
  • Generate a REST API scaffold:

    stabilize-cli generate:api User

Database & Migration Management

  • Run all pending migrations:

    stabilize-cli migrate
  • Roll back the last migration:

    stabilize-cli migrate:rollback
  • Fresh migration (drop + re-migrate):

    stabilize-cli migrate:fresh --force
  • Run all pending seeds (in dependency order):

    stabilize-cli seed
  • Check the status of migrations and seeds:

    stabilize-cli status
  • Reset the database (drop, migrate, seed):

    stabilize-cli db:reset

Backup & Restore

  • Backup the database:

    stabilize-cli db:backup
  • Restore from a backup:

    stabilize-cli db:restore backups/backup_20250101120000.db --force

Diagnostics

  • Database size statistics:

    stabilize-cli db:size
  • Health check:

    stabilize-cli health
  • CLI info:

    stabilize-cli info

🧑‍💻 Querying Data

Basic CRUD with Repositories

import{orm}from"./db";import{User}from"./models/User";constuserRepository=orm.getRepository(User);constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});constfoundUser=awaituserRepository.findOne(newUser.id);constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});awaituserRepository.delete(newUser.id);

Advanced Queries with the Query Builder

constactiveAdmins=awaitorm.getRepository(UserRole).find().join("users","user_roles.user_id = users.id").join("roles","user_roles.role_id = roles.id").select("users.id","users.email","roles.name as role_name").where("roles.name = ?","Admin").orderBy("users.email ASC").execute();console.log(activeAdmins);

Query Builder API

{select(...fields: string[]): QueryBuilder<User>;where(condition: string, ...params: any[]): QueryBuilder<User>;orWhere(condition: string, ...params: any[]): QueryBuilder<User>;whereIn(column: string,values: any[]): QueryBuilder<User>;whereNotIn(column: string,values: any[]): QueryBuilder<User>;whereNull(column: string): QueryBuilder<User>;whereNotNull(column: string): QueryBuilder<User>;whereBetween(column: string,start: any,end: any): QueryBuilder<User>;groupBy(clause: string): QueryBuilder<User>;having(condition: string, ...params: any[]): QueryBuilder<User>;join(table: string,condition: string): QueryBuilder<User>;orderBy(clause: string): QueryBuilder<User>;limit(limit: number): QueryBuilder<User>;offset(offset: number): QueryBuilder<User>;lock(mode?: "FOR UPDATE"|"FOR SHARE"): QueryBuilder<User>;withRelations(...relations: string[]): QueryBuilder<User>;scope(name: string, ...args: any[]): QueryBuilder<User>;paginate(page: number,pageSize: number): QueryBuilder<User>;build(): { query: string; params: any[]};execute(client?: DBClient,cache?: Cache,cacheKey?: string): Promise<User[]>;}

Custom Query Scopes

Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the scope method on Repository or QueryBuilder, allowing you to chain them with other query operations.

Scopes Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},isActive: {type: DataTypes.Boolean,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},scopes: {active: (qb)=>qb.where("isActive = ?",true),recent: (qb,days: number)=>qb.where("createdAt >= ?",newDate(Date.now()-days*24*60*60*1000),),},});constuserRepository=orm.getRepository(User);// Fetch active usersconstactiveUsers=awaituserRepository.scope("active").execute();// Fetch users created in the last 7 daysconstrecentUsers=awaituserRepository.scope("recent",7).execute();// Combine scopes with other query operationsconstrecentActiveUsers=awaituserRepository.scope("active").scope("recent",7).orderBy("createdAt DESC").limit(10).execute();console.log(recentActiveUsers);

Timestamps

Enable automatic management of createdAt and updatedAt columns by setting timestamps in your model configuration. The ORM automatically sets these fields during create, update, bulkCreate, bulkUpdate, and upsert operations in a TypeScript-safe manner, eliminating the need for manual hooks.

Timestamps Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},timestamps: {createdAt: "createdAt",updatedAt: "updatedAt",},});constuserRepository=orm.getRepository(User);// Create a user (createdAt and updatedAt set automatically)constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});console.log(newUser.createdAt,newUser.updatedAt);// Outputs current timestamp// Update a user (updatedAt updated automatically)constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});console.log(updatedUser.updatedAt);// Outputs new timestamp// Bulk create usersconstnewUsers=awaituserRepository.bulkCreate([{email: "user1@example.com"},{email: "user2@example.com"},]);console.log(newUsers.map((u)=>u.createdAt));// Outputs timestamps for each user

🗑️ Soft Deletes

Enable soft deletes by setting softDelete: true and marking a column (e.g., deletedAt) with softDelete: true in the model configuration.

  • Use repository.delete(id) to mark an entity as deleted.
  • Use repository.recover(id) to restore a soft-deleted entity.
  • Queries automatically exclude soft-deleted rows unless specified otherwise.

Soft Delete Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",softDelete: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},deletedAt: {type: DataTypes.DateTime,softDelete: true},},});constuserRepository=orm.getRepository(User);awaituserRepository.create({email: "lwazicd@icloud.com"});awaituserRepository.delete(1);// Soft deleteawaituserRepository.recover(1);// Recover

🌐 Express.js Integration

Stabilize ORM works seamlessly with web frameworks like Express.

importexpressfrom"express";import{orm}from"./db";import{User}from"./models/User";constapp=express();app.use(express.json());constuserRepository=orm.getRepository(User);app.get("/users",async(req,res)=>{try{constusers=awaituserRepository.find().execute();res.json(users);}catch(err){res.status(500).json({error: "Failed to fetch users."});}});app.post("/users",async(req,res)=>{try{constuser=awaituserRepository.create(req.body);res.status(201).json(user);}catch(err){res.status(500).json({error: "User creation failed."});}});app.listen(3000,()=>{console.log("Server listening on port 3000");});

🧑‍🔬 Testing & Time-Travel

  • Use time-travel queries to inspect historical entity states.
  • Assert audit trails and rollback operations in your tests.

🔒 Optimistic Locking

Enable optimistic locking to detect concurrent modifications. Add optimisticLock: true to a version column in your model.

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String},version: {type: DataTypes.Integer,optimisticLock: true},},});constuserRepository=orm.getRepository(User);// Create with initial versionconstuser=awaituserRepository.create({name: "Lwazi",version: 1});// Update - version is automatically incremented// If another transaction modified the record, a CONCURRENT_MODIFICATION error is throwntry{awaituserRepository.update(user.id,{name: "Updated",version: user.version,});}catch(err){if(err.code==="CONCURRENT_MODIFICATION"){console.log("Record was modified by another transaction");}}

⏱️ SQL Default Expressions

Use sqlDefault() to set database-side default values for columns (e.g., gen_random_uuid(), NOW()).

import{defineModel,DataTypes,sqlDefault}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.UUID,required: true,defaultExpression: sqlDefault("gen_random_uuid()"),},name: {type: DataTypes.String},createdAt: {type: DataTypes.DateTime,defaultExpression: sqlDefault("NOW()"),},},});

📊 Advanced Query Builder

The query builder now supports additional filter methods:

constresults=awaituserRepository.find().where("status = ?","active").orWhere("role = ?","admin").whereIn("age",[25,30,35]).whereBetween("createdAt",newDate("2025-01-01"),newDate("2025-12-31")).whereNull("deletedAt").groupBy("department").having("COUNT(*) > ?",5).orderBy("createdAt DESC").limit(10).execute();

🔗 Nested Relations

Load deeply nested relations using dot notation:

constuser=awaituserRepository.findOne(1,{relations: ["roles","roles.permissions"],});

📊 Aggregation Queries

Run aggregate queries directly on the repository or query builder.

// Repository-level aggregatesconsttotal=awaituserRepository.count();constexists=awaituserRepository.exists({email: "admin@offbytesecure.com",});conststats=awaituserRepository.aggregate({count: "*",sum: ["salary"],avg: ["salary"],min: ["salary"],max: ["salary"],});// stats = { count_: 100, sum_salary: 5000000, avg_salary: 50000, min_salary: 20000, max_salary: 150000 }

🔎 findOneBy / findBy

TypeORM-style conditional finders without writing raw SQL.

// Find one record by conditionconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"});// Find multiple recordsconstadmins=awaituserRepository.findBy({role: "admin"},{limit: 10,orderBy: "createdAt DESC"},);// Combined with relationsconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"},{relations: ["roles"]},);

🔢 findAndCount

Get paginated results with a total count in a single call.

const{ data, total }=awaituserRepository.findAndCount();console.log(`Showing ${data.length} of ${total} total records`);

🖱️ Cursor-Based Pagination

Efficient cursor-based pagination for large datasets.

// First pageconstpage1=awaituserRepository.findMany({take: 10,orderBy: {field: "id",direction: "ASC"},});// Next page using cursorconstlastId=page1[page1.length-1].id;constpage2=awaituserRepository.findMany({cursor: {field: "id",value: lastId,direction: "forward"},take: 10,orderBy: {field: "id",direction: "ASC"},});

🌱 Database Seeding

Define and run seeds for populating development/test databases.

import{defineSeed,runSeeds,resetDatabase}from"stabilize-orm";// Define a seeddefineSeed("create-default-roles",async(db)=>{constroleRepo=orm.getRepository(Role);awaitroleRepo.bulkCreate([{name: "Admin",permissions: "all"},{name: "User",permissions: "read"},]);});// Run all seedsawaitrunSeeds(orm.client);// Reset database (drop, migrate, seed)awaitresetDatabase(orm.client,[User,Role]);

🏥 Health Check

Monitor database and cache connectivity with latency.

consthealth=awaitorm.healthCheck();// { status: "healthy", database: "postgres", latencyMs: 12.5, cacheStatus: "connected" }// Per-table healthconstuserHealth=awaituserRepository.healthCheck();// { status: "healthy", table: "users", rows: 142, latencyMs: 8.3 }

📂 Bulk Upsert

Upsert multiple records in a single transaction.

constusers=awaituserRepository.bulkUpsert([{email: "lwazicd@icloud.com",name: "Lwazi"},{email: "ciniso@icloud.com",name: "Ciniso"},],["email"],// unique key(s));

🔧 Raw SQL

Execute raw SQL queries directly from the ORM instance.

constresults=awaitorm.rawQuery("SELECT * FROM users WHERE age > ?",[25]);const{ affectedRows }=awaitorm.rawExec("UPDATE users SET active = false WHERE last_login < ?",[oneYearAgo],);

🗑️ recoverAll / truncate

// Restore all soft-deleted recordsconstrecovered=awaituserRepository.recoverAll();console.log(`Recovered ${recovered} records`);// Clear the tableawaituserRepository.truncate();

⬆️⬇️ Increment / Decrement

Atomically update numeric fields without loading the record.

constupdated=awaituserRepository.increment(user.id,"loginCount",1);constupdated=awaituserRepository.decrement(user.id,"credits",5);

🏷️ Pluck / SelectColumns

// Get just the email column as an arrayconstemails=awaituserRepository.pluck("email");// ["lwazicd@icloud.com", "ciniso@icloud.com", ...]// Get specific columnsconstusers=awaituserRepository.selectColumns("id","email");// [{ id: 1, email: "lwazicd@icloud.com" }, ...]

🔄 Toggle

Toggle a boolean field.

consttoggled=awaituserRepository.toggle(user.id,"isActive");// isActive was true, now false (or vice versa)

✏️ updateBy / deleteBy

// Update all active users' role to "member"constupdated=awaituserRepository.updateBy({isActive: true},{role: "member"},);// Delete all users with null emailconstdeleted=awaituserRepository.deleteBy({email: null});

🕳️ findDeleted / withTrashed

// Get only soft-deleted recordsconstdeletedUsers=awaituserRepository.findDeleted().execute();// Get all records including soft-deletedconstallUsers=awaituserRepository.withTrashed().execute();// Restore matching soft-deleted recordsconstrestored=awaituserRepository.restoreBy({role: "admin"});

🔄 firstOrCreate / updateOrCreate

// Find or create in one callconstuser=awaituserRepository.firstOrCreate({email: "lwazicd@icloud.com"},{name: "Lwazi"},);// Find and update, or create if not foundconstuser=awaituserRepository.updateOrCreate({email: "lwazicd@icloud.com"},{name: "Updated Name"},);

🥇 first / last / random

constfirstUser=awaituserRepository.first();constadmin=awaituserRepository.first({role: "admin"});constlastUser=awaituserRepository.last();constrandomUser=awaituserRepository.random();

🔒 Pessimistic Locking (lockForUpdate)

constuser=awaituserRepository.lockForUpdate(user.id);// The row is now locked for the duration of the transaction

📡 Event Emitter

Subscribe to ORM lifecycle events.

constorm=newStabilize(dbConfig);orm.events.on("query",(entry)=>{console.log(`[${entry.durationMs}ms] ${entry.query}`);});orm.events.on("error",(err)=>{console.error("ORM error:",err);});orm.events.on("connection:open",(dbType)=>{console.log(`Connected to ${dbType}`);});

📦 Pool Stats

conststats=awaitorm.poolStats();// { active: 5, idle: 10, total: 15 }

🆔 generateUUID

import{generateUUID}from"stabilize-orm";constid=generateUUID();// "550e8400-e29b-41d4-a716-446655440000"

📑 License

Licensed under the MIT License. See LICENSE.md for details.


Created with ❤️ by ElectronSz
File last updated: 2026-04-02

About

A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Stabilize ORM

A Modern, Type-Safe, and Expressive ORM for Bun

Stabilize ORM Logo

NPM VersionLicenseStabilize CLIPostgreSQLMySQLSQLiteBuild StatusMIT License

Stabilize is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for PostgreSQL, MySQL, and SQLite. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.


🚀 Features

  • Unified API: Write once, run on PostgreSQL, MySQL, or SQLite.
  • Programmatic Model Definitions: Define models and columns using the defineModel API with the DataTypes enum for database-agnostic schemas.
  • Full-Featured CLI: Generate models, manage migrations, seed data, and reset your database from the command line with stabilize-cli.
  • Automatic Migrations: Generate database-specific SQL schemas directly from your model definitions.
  • Versioned Models & Time-Travel: Enable versioning in your model configuration for automatic history tables and snapshot queries.
  • Retry Logic: Automatic exponential backoff for database queries to handle transient connection issues.
  • Connection Pooling: Efficient connection management for PostgreSQL and MySQL.
  • Transactional Integrity: Built-in support for atomic transactions with automatic rollback on failure.
  • Advanced Query Builder: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
  • Pagination Helper: Easily paginate any query with .paginate(page, pageSize) and get { data, total, page, pageSize }.
  • Advanced Model Validation: Enforce rules like required, minLength, maxLength, pattern, and custom validators—errors are thrown on invalid input.
  • Model Relationships: Define OneToOne, ManyToOne, OneToMany, and ManyToMany relationships in the model configuration.
  • Soft Deletes: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
  • Lifecycle Hooks: Define hooks in the model configuration or as class methods for lifecycle events like beforeCreate, afterUpdate, etc.
  • Pluggable Logging: Includes a robust StabilizeLogger with support for file-based, rotating logs.
  • Custom Errors: StabilizeError provides clear, consistent error handling.
  • Caching Layer: Optional Redis-backed caching with cache-aside and write-through strategies.
  • Custom Query Scopes: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
  • Timestamps: Automatically manage createdAt and updatedAt columns for tracking record creation and update times.
  • SQL Default Expressions: Support database-side default expressions (e.g., gen_random_uuid(), NOW()) for columns using the sqlDefault() helper.
  • Nested Relations (Eager Loading): Load deeply nested relations using dot notation like "roles.permissions".
  • AutoMigrate with Index Management: Automatically create, detect, and remove indexes and unique constraints during migration.
  • Advanced Query Builder Filters: Chainable .orWhere(), .whereIn(), .whereNotIn(), .whereNull(), .whereNotNull(), .whereBetween(), .groupBy(), .having(), .lock() methods.
  • Optimistic Locking: Add optimisticLock: true to a version column to automatically detect concurrent modification conflicts and throw CONCURRENT_MODIFICATION errors.
  • findAndCount: Get paginated results with a total count in one call.
  • findOneBy / findBy: TypeORM-style conditional finders without writing raw SQL.
  • Aggregate Queries: Run count(), sum(), avg(), min(), max() directly from the repository or query builder.
  • Cursor-Based Pagination: Efficient forward/backward cursor pagination for large datasets (Prisma-style findMany).
  • exists: Check if a record exists without loading it.
  • recoverAll: Bulk restore all soft-deleted records.
  • truncate: Clear all rows from a table.
  • seed / defineSeed: Laravel-style seeding framework with defineSeed and runSeeds.
  • resetDatabase: Drop tables, re-migrate, and optionally re-seed for development.
  • healthCheck: Get database and table health status with latency for monitoring endpoints.
  • rawQuery / rawExec: Execute raw SQL directly from the Stabilize instance.
  • bulkUpsert: Upsert multiple records in a single transaction.
  • findMany: Prisma-style query with where, cursor, take, skip, orderBy.
  • countDistinct: Count unique values in a column.
  • increment / decrement: Atomically increment or decrement a numeric field.
  • pluck: Get an array of a single column's values (Rails-style).
  • selectColumns: Get only specific columns from a query.
  • toggle: Toggle a boolean field (Rails-style).
  • updateBy / deleteBy: Conditional bulk updates and deletes without writing SQL.
  • restoreBy: Restore soft-deleted records matching conditions.
  • findDeleted / withTrashed: Query soft-deleted or all records.
  • upsertMany: Batch upsert in configurable batch sizes.
  • map / each / eachBatch: Transform, iterate, and batch-process query results.
  • lockForUpdate: Pessimistic row locking for read-modify-write.
  • firstOrCreate / updateOrCreate: Laravel-style find-or-create patterns.
  • first / last / random: Single-record shortcuts.
  • StabilizeEmitter: Event system for query, error, connection:open/close, transaction:start/complete/error.
  • TransactionIsolationLevel: Type for READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE.
  • generateUUID: Cross-runtime UUID generation helper.
  • poolStats: Get connection pool statistics.
  • Database Backup & Restore: db:backup and db:restore commands for database backup management.
  • API Generation: generate:api command scaffolds full CRUD REST API routes from models.
  • Fresh Migrations: migrate:fresh drops all tables and re-runs migrations without seeding.
  • Database Size Analysis: db:size command shows table sizes and row count statistics.

📦 Installation

Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).

# Using Bun
bun add stabilize-orm
# Using npm
npm install stabilize-orm

📃 Documentation & Community

Examples

  • Blog - Blog with users, posts, comments, and versioning
  • E-Commerce - Products, categories, orders with transactions
  • REST API - Express.js REST API with pagination and optimistic locking
  • SaaS - Multi-tenant SaaS with tenants, members, and scoped projects
  • CMS - Content management with authors, categories, articles, and versioning
  • Analytics - Event tracking with aggregations and metrics

⚙️ Configuration

Create a database configuration file.

// config/database.tsimport{DBType,typeDBConfig}from"stabilize-orm";constdbConfig: DBConfig={type: DBType.Postgres,connectionString:
process.env.DATABASE_URL||"postgres://user:password@localhost:5432/mydb",retryAttempts: 3,retryDelay: 1000,};exportdefaultdbConfig;

Next, create a central ORM instance for your application.

// db.tsimport{Stabilize,typeCacheConfig,typeLoggerConfig,LogLevel,}from"stabilize-orm";importdbConfigfrom"./database";constcacheConfig: CacheConfig={enabled: process.env.CACHE_ENABLED==="true",redisUrl: process.env.REDIS_URL,ttl: 60,};constloggerConfig: LoggerConfig={level: LogLevel.Info,filePath: "logs/stabilize.log",maxFileSize: 5*1024*1024,// 5MBmaxFiles: 3,};exportconstorm=newStabilize(dbConfig,cacheConfig,loggerConfig);

🏗️ Models & Relationships

Define your tables as classes using the defineModel function. The DataTypes enum ensures database-agnostic schemas.

Example: Users and Roles (Many-to-Many) with Versioning

// models/User.tsimport{defineModel,DataTypes,RelationType}from"stabilize-orm";import{UserRole}from"./UserRole";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true,unique: true,},},relations: [{type: RelationType.OneToMany,target: ()=>UserRole,property: "roles",foreignKey: "userId",},],hooks: {beforeCreate: (entity)=>console.log(`Creating user: ${entity.email}`),},});export{User};

🔍 Pagination

The built-in pagination helper makes it easy to retrieve a page of rows using LIMIT/OFFSET semantics.

constusers=awaituserRepository.find().paginate(2,10).execute(dbClient);// users = [...] // Returns an array of rows for that page

Or, use the query builder:

constusers=awaituserRepository.find().where('isActive = ?',true).paginate(1,20).execute(dbClient);

🛡️ Advanced Validation

Models can define advanced validation rules for columns, including:

  • required
  • minLength / maxLength
  • pattern (RegExp)
  • customValidator (function)

Validation errors are thrown on create/update if data is invalid.

constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,required: true,unique: true,minLength: 6,pattern: /^[^@]+@[^@]+\.[^@]+$/,customValidator: (val)=>val.endsWith("@offbytesecure.com")||"Must use an @offbytesecure.com email",},password: {type: DataTypes.String,minLength: 8},},});

⏳ Versioning & Auditing

Enable automatic history tracking and time-travel queries by setting versioned: true in your model configuration.

  • Each change is recorded in a <table>_history table with version, operation, and audit columns.
  • Supports snapshot queries, rollbacks, audits, and time-travel.

Versioning Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},},});// --- Using versioning features:constuserRepository=orm.getRepository(User);// Rollback to a previous versionawaituserRepository.rollback(1,3);// roll back user with id=1 to version 3// Get a snapshot as of a specific dateconstuserAsOf=awaituserRepository.asOf(1,newDate("2025-01-01T00:00:00Z"));console.log(userAsOf);// View full version historyconsthistory=awaituserRepository.history(1);console.log(history);

🔄 Model Lifecycle Hooks

Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.

Hooks Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},hooks: {beforeCreate: (entity)=>{entity.createdAt=newDate();},beforeUpdate: (entity)=>{entity.updatedAt=newDate();},afterCreate: (entity)=>{console.log(`User created: ${entity.name}`);},},});// Add a hook as a class methodUser.prototype.afterUpdate=asyncfunction(){console.log(`Updated user: ${this.name}`);};export{User};

Supported hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeSave, afterSave.


💻 Command-Line Interface (CLI)

Stabilize includes a powerful CLI for managing your workflow. See: stabilize-cli on GitHub

Generating Files

  • Generate a model:

    stabilize-cli generate:model Product
  • Generate a migration from a model:

    stabilize-cli generate:migration User
  • Generate a seed file:

    stabilize-cli generate:seed InitialRoles
  • Generate a REST API scaffold:

    stabilize-cli generate:api User

Database & Migration Management

  • Run all pending migrations:

    stabilize-cli migrate
  • Roll back the last migration:

    stabilize-cli migrate:rollback
  • Fresh migration (drop + re-migrate):

    stabilize-cli migrate:fresh --force
  • Run all pending seeds (in dependency order):

    stabilize-cli seed
  • Check the status of migrations and seeds:

    stabilize-cli status
  • Reset the database (drop, migrate, seed):

    stabilize-cli db:reset

Backup & Restore

  • Backup the database:

    stabilize-cli db:backup
  • Restore from a backup:

    stabilize-cli db:restore backups/backup_20250101120000.db --force

Diagnostics

  • Database size statistics:

    stabilize-cli db:size
  • Health check:

    stabilize-cli health
  • CLI info:

    stabilize-cli info

🧑‍💻 Querying Data

Basic CRUD with Repositories

import{orm}from"./db";import{User}from"./models/User";constuserRepository=orm.getRepository(User);constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});constfoundUser=awaituserRepository.findOne(newUser.id);constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});awaituserRepository.delete(newUser.id);

Advanced Queries with the Query Builder

constactiveAdmins=awaitorm.getRepository(UserRole).find().join("users","user_roles.user_id = users.id").join("roles","user_roles.role_id = roles.id").select("users.id","users.email","roles.name as role_name").where("roles.name = ?","Admin").orderBy("users.email ASC").execute();console.log(activeAdmins);

Query Builder API

{select(...fields: string[]): QueryBuilder<User>;where(condition: string, ...params: any[]): QueryBuilder<User>;orWhere(condition: string, ...params: any[]): QueryBuilder<User>;whereIn(column: string,values: any[]): QueryBuilder<User>;whereNotIn(column: string,values: any[]): QueryBuilder<User>;whereNull(column: string): QueryBuilder<User>;whereNotNull(column: string): QueryBuilder<User>;whereBetween(column: string,start: any,end: any): QueryBuilder<User>;groupBy(clause: string): QueryBuilder<User>;having(condition: string, ...params: any[]): QueryBuilder<User>;join(table: string,condition: string): QueryBuilder<User>;orderBy(clause: string): QueryBuilder<User>;limit(limit: number): QueryBuilder<User>;offset(offset: number): QueryBuilder<User>;lock(mode?: "FOR UPDATE"|"FOR SHARE"): QueryBuilder<User>;withRelations(...relations: string[]): QueryBuilder<User>;scope(name: string, ...args: any[]): QueryBuilder<User>;paginate(page: number,pageSize: number): QueryBuilder<User>;build(): { query: string; params: any[]};execute(client?: DBClient,cache?: Cache,cacheKey?: string): Promise<User[]>;}

Custom Query Scopes

Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the scope method on Repository or QueryBuilder, allowing you to chain them with other query operations.

Scopes Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},isActive: {type: DataTypes.Boolean,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},scopes: {active: (qb)=>qb.where("isActive = ?",true),recent: (qb,days: number)=>qb.where("createdAt >= ?",newDate(Date.now()-days*24*60*60*1000),),},});constuserRepository=orm.getRepository(User);// Fetch active usersconstactiveUsers=awaituserRepository.scope("active").execute();// Fetch users created in the last 7 daysconstrecentUsers=awaituserRepository.scope("recent",7).execute();// Combine scopes with other query operationsconstrecentActiveUsers=awaituserRepository.scope("active").scope("recent",7).orderBy("createdAt DESC").limit(10).execute();console.log(recentActiveUsers);

Timestamps

Enable automatic management of createdAt and updatedAt columns by setting timestamps in your model configuration. The ORM automatically sets these fields during create, update, bulkCreate, bulkUpdate, and upsert operations in a TypeScript-safe manner, eliminating the need for manual hooks.

Timestamps Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},timestamps: {createdAt: "createdAt",updatedAt: "updatedAt",},});constuserRepository=orm.getRepository(User);// Create a user (createdAt and updatedAt set automatically)constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});console.log(newUser.createdAt,newUser.updatedAt);// Outputs current timestamp// Update a user (updatedAt updated automatically)constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});console.log(updatedUser.updatedAt);// Outputs new timestamp// Bulk create usersconstnewUsers=awaituserRepository.bulkCreate([{email: "user1@example.com"},{email: "user2@example.com"},]);console.log(newUsers.map((u)=>u.createdAt));// Outputs timestamps for each user

🗑️ Soft Deletes

Enable soft deletes by setting softDelete: true and marking a column (e.g., deletedAt) with softDelete: true in the model configuration.

  • Use repository.delete(id) to mark an entity as deleted.
  • Use repository.recover(id) to restore a soft-deleted entity.
  • Queries automatically exclude soft-deleted rows unless specified otherwise.

Soft Delete Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",softDelete: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},deletedAt: {type: DataTypes.DateTime,softDelete: true},},});constuserRepository=orm.getRepository(User);awaituserRepository.create({email: "lwazicd@icloud.com"});awaituserRepository.delete(1);// Soft deleteawaituserRepository.recover(1);// Recover

🌐 Express.js Integration

Stabilize ORM works seamlessly with web frameworks like Express.

importexpressfrom"express";import{orm}from"./db";import{User}from"./models/User";constapp=express();app.use(express.json());constuserRepository=orm.getRepository(User);app.get("/users",async(req,res)=>{try{constusers=awaituserRepository.find().execute();res.json(users);}catch(err){res.status(500).json({error: "Failed to fetch users."});}});app.post("/users",async(req,res)=>{try{constuser=awaituserRepository.create(req.body);res.status(201).json(user);}catch(err){res.status(500).json({error: "User creation failed."});}});app.listen(3000,()=>{console.log("Server listening on port 3000");});

🧑‍🔬 Testing & Time-Travel

  • Use time-travel queries to inspect historical entity states.
  • Assert audit trails and rollback operations in your tests.

🔒 Optimistic Locking

Enable optimistic locking to detect concurrent modifications. Add optimisticLock: true to a version column in your model.

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String},version: {type: DataTypes.Integer,optimisticLock: true},},});constuserRepository=orm.getRepository(User);// Create with initial versionconstuser=awaituserRepository.create({name: "Lwazi",version: 1});// Update - version is automatically incremented// If another transaction modified the record, a CONCURRENT_MODIFICATION error is throwntry{awaituserRepository.update(user.id,{name: "Updated",version: user.version,});}catch(err){if(err.code==="CONCURRENT_MODIFICATION"){console.log("Record was modified by another transaction");}}

⏱️ SQL Default Expressions

Use sqlDefault() to set database-side default values for columns (e.g., gen_random_uuid(), NOW()).

import{defineModel,DataTypes,sqlDefault}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.UUID,required: true,defaultExpression: sqlDefault("gen_random_uuid()"),},name: {type: DataTypes.String},createdAt: {type: DataTypes.DateTime,defaultExpression: sqlDefault("NOW()"),},},});

📊 Advanced Query Builder

The query builder now supports additional filter methods:

constresults=awaituserRepository.find().where("status = ?","active").orWhere("role = ?","admin").whereIn("age",[25,30,35]).whereBetween("createdAt",newDate("2025-01-01"),newDate("2025-12-31")).whereNull("deletedAt").groupBy("department").having("COUNT(*) > ?",5).orderBy("createdAt DESC").limit(10).execute();

🔗 Nested Relations

Load deeply nested relations using dot notation:

constuser=awaituserRepository.findOne(1,{relations: ["roles","roles.permissions"],});

📊 Aggregation Queries

Run aggregate queries directly on the repository or query builder.

// Repository-level aggregatesconsttotal=awaituserRepository.count();constexists=awaituserRepository.exists({email: "admin@offbytesecure.com",});conststats=awaituserRepository.aggregate({count: "*",sum: ["salary"],avg: ["salary"],min: ["salary"],max: ["salary"],});// stats = { count_: 100, sum_salary: 5000000, avg_salary: 50000, min_salary: 20000, max_salary: 150000 }

🔎 findOneBy / findBy

TypeORM-style conditional finders without writing raw SQL.

// Find one record by conditionconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"});// Find multiple recordsconstadmins=awaituserRepository.findBy({role: "admin"},{limit: 10,orderBy: "createdAt DESC"},);// Combined with relationsconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"},{relations: ["roles"]},);

🔢 findAndCount

Get paginated results with a total count in a single call.

const{ data, total }=awaituserRepository.findAndCount();console.log(`Showing ${data.length} of ${total} total records`);

🖱️ Cursor-Based Pagination

Efficient cursor-based pagination for large datasets.

// First pageconstpage1=awaituserRepository.findMany({take: 10,orderBy: {field: "id",direction: "ASC"},});// Next page using cursorconstlastId=page1[page1.length-1].id;constpage2=awaituserRepository.findMany({cursor: {field: "id",value: lastId,direction: "forward"},take: 10,orderBy: {field: "id",direction: "ASC"},});

🌱 Database Seeding

Define and run seeds for populating development/test databases.

import{defineSeed,runSeeds,resetDatabase}from"stabilize-orm";// Define a seeddefineSeed("create-default-roles",async(db)=>{constroleRepo=orm.getRepository(Role);awaitroleRepo.bulkCreate([{name: "Admin",permissions: "all"},{name: "User",permissions: "read"},]);});// Run all seedsawaitrunSeeds(orm.client);// Reset database (drop, migrate, seed)awaitresetDatabase(orm.client,[User,Role]);

🏥 Health Check

Monitor database and cache connectivity with latency.

consthealth=awaitorm.healthCheck();// { status: "healthy", database: "postgres", latencyMs: 12.5, cacheStatus: "connected" }// Per-table healthconstuserHealth=awaituserRepository.healthCheck();// { status: "healthy", table: "users", rows: 142, latencyMs: 8.3 }

📂 Bulk Upsert

Upsert multiple records in a single transaction.

constusers=awaituserRepository.bulkUpsert([{email: "lwazicd@icloud.com",name: "Lwazi"},{email: "ciniso@icloud.com",name: "Ciniso"},],["email"],// unique key(s));

🔧 Raw SQL

Execute raw SQL queries directly from the ORM instance.

constresults=awaitorm.rawQuery("SELECT * FROM users WHERE age > ?",[25]);const{ affectedRows }=awaitorm.rawExec("UPDATE users SET active = false WHERE last_login < ?",[oneYearAgo],);

🗑️ recoverAll / truncate

// Restore all soft-deleted recordsconstrecovered=awaituserRepository.recoverAll();console.log(`Recovered ${recovered} records`);// Clear the tableawaituserRepository.truncate();

⬆️⬇️ Increment / Decrement

Atomically update numeric fields without loading the record.

constupdated=awaituserRepository.increment(user.id,"loginCount",1);constupdated=awaituserRepository.decrement(user.id,"credits",5);

🏷️ Pluck / SelectColumns

// Get just the email column as an arrayconstemails=awaituserRepository.pluck("email");// ["lwazicd@icloud.com", "ciniso@icloud.com", ...]// Get specific columnsconstusers=awaituserRepository.selectColumns("id","email");// [{ id: 1, email: "lwazicd@icloud.com" }, ...]

🔄 Toggle

Toggle a boolean field.

consttoggled=awaituserRepository.toggle(user.id,"isActive");// isActive was true, now false (or vice versa)

✏️ updateBy / deleteBy

// Update all active users' role to "member"constupdated=awaituserRepository.updateBy({isActive: true},{role: "member"},);// Delete all users with null emailconstdeleted=awaituserRepository.deleteBy({email: null});

🕳️ findDeleted / withTrashed

// Get only soft-deleted recordsconstdeletedUsers=awaituserRepository.findDeleted().execute();// Get all records including soft-deletedconstallUsers=awaituserRepository.withTrashed().execute();// Restore matching soft-deleted recordsconstrestored=awaituserRepository.restoreBy({role: "admin"});

🔄 firstOrCreate / updateOrCreate

// Find or create in one callconstuser=awaituserRepository.firstOrCreate({email: "lwazicd@icloud.com"},{name: "Lwazi"},);// Find and update, or create if not foundconstuser=awaituserRepository.updateOrCreate({email: "lwazicd@icloud.com"},{name: "Updated Name"},);

🥇 first / last / random

constfirstUser=awaituserRepository.first();constadmin=awaituserRepository.first({role: "admin"});constlastUser=awaituserRepository.last();constrandomUser=awaituserRepository.random();

🔒 Pessimistic Locking (lockForUpdate)

constuser=awaituserRepository.lockForUpdate(user.id);// The row is now locked for the duration of the transaction

📡 Event Emitter

Subscribe to ORM lifecycle events.

constorm=newStabilize(dbConfig);orm.events.on("query",(entry)=>{console.log(`[${entry.durationMs}ms] ${entry.query}`);});orm.events.on("error",(err)=>{console.error("ORM error:",err);});orm.events.on("connection:open",(dbType)=>{console.log(`Connected to ${dbType}`);});

📦 Pool Stats

conststats=awaitorm.poolStats();// { active: 5, idle: 10, total: 15 }

🆔 generateUUID

import{generateUUID}from"stabilize-orm";constid=generateUUID();// "550e8400-e29b-41d4-a716-446655440000"

📑 License

Licensed under the MIT License. See LICENSE.md for details.


Created with ❤️ by ElectronSz
File last updated: 2026-04-02

About

A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Latest commit

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Stabilize ORM

A Modern, Type-Safe, and Expressive ORM for Bun

Stabilize ORM Logo

NPM VersionLicenseStabilize CLIPostgreSQLMySQLSQLiteBuild StatusMIT License

Stabilize is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for PostgreSQL, MySQL, and SQLite. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.


🚀 Features

  • Unified API: Write once, run on PostgreSQL, MySQL, or SQLite.
  • Programmatic Model Definitions: Define models and columns using the defineModel API with the DataTypes enum for database-agnostic schemas.
  • Full-Featured CLI: Generate models, manage migrations, seed data, and reset your database from the command line with stabilize-cli.
  • Automatic Migrations: Generate database-specific SQL schemas directly from your model definitions.
  • Versioned Models & Time-Travel: Enable versioning in your model configuration for automatic history tables and snapshot queries.
  • Retry Logic: Automatic exponential backoff for database queries to handle transient connection issues.
  • Connection Pooling: Efficient connection management for PostgreSQL and MySQL.
  • Transactional Integrity: Built-in support for atomic transactions with automatic rollback on failure.
  • Advanced Query Builder: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
  • Pagination Helper: Easily paginate any query with .paginate(page, pageSize) and get { data, total, page, pageSize }.
  • Advanced Model Validation: Enforce rules like required, minLength, maxLength, pattern, and custom validators—errors are thrown on invalid input.
  • Model Relationships: Define OneToOne, ManyToOne, OneToMany, and ManyToMany relationships in the model configuration.
  • Soft Deletes: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
  • Lifecycle Hooks: Define hooks in the model configuration or as class methods for lifecycle events like beforeCreate, afterUpdate, etc.
  • Pluggable Logging: Includes a robust StabilizeLogger with support for file-based, rotating logs.
  • Custom Errors: StabilizeError provides clear, consistent error handling.
  • Caching Layer: Optional Redis-backed caching with cache-aside and write-through strategies.
  • Custom Query Scopes: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
  • Timestamps: Automatically manage createdAt and updatedAt columns for tracking record creation and update times.
  • SQL Default Expressions: Support database-side default expressions (e.g., gen_random_uuid(), NOW()) for columns using the sqlDefault() helper.
  • Nested Relations (Eager Loading): Load deeply nested relations using dot notation like "roles.permissions".
  • AutoMigrate with Index Management: Automatically create, detect, and remove indexes and unique constraints during migration.
  • Advanced Query Builder Filters: Chainable .orWhere(), .whereIn(), .whereNotIn(), .whereNull(), .whereNotNull(), .whereBetween(), .groupBy(), .having(), .lock() methods.
  • Optimistic Locking: Add optimisticLock: true to a version column to automatically detect concurrent modification conflicts and throw CONCURRENT_MODIFICATION errors.
  • findAndCount: Get paginated results with a total count in one call.
  • findOneBy / findBy: TypeORM-style conditional finders without writing raw SQL.
  • Aggregate Queries: Run count(), sum(), avg(), min(), max() directly from the repository or query builder.
  • Cursor-Based Pagination: Efficient forward/backward cursor pagination for large datasets (Prisma-style findMany).
  • exists: Check if a record exists without loading it.
  • recoverAll: Bulk restore all soft-deleted records.
  • truncate: Clear all rows from a table.
  • seed / defineSeed: Laravel-style seeding framework with defineSeed and runSeeds.
  • resetDatabase: Drop tables, re-migrate, and optionally re-seed for development.
  • healthCheck: Get database and table health status with latency for monitoring endpoints.
  • rawQuery / rawExec: Execute raw SQL directly from the Stabilize instance.
  • bulkUpsert: Upsert multiple records in a single transaction.
  • findMany: Prisma-style query with where, cursor, take, skip, orderBy.
  • countDistinct: Count unique values in a column.
  • increment / decrement: Atomically increment or decrement a numeric field.
  • pluck: Get an array of a single column's values (Rails-style).
  • selectColumns: Get only specific columns from a query.
  • toggle: Toggle a boolean field (Rails-style).
  • updateBy / deleteBy: Conditional bulk updates and deletes without writing SQL.
  • restoreBy: Restore soft-deleted records matching conditions.
  • findDeleted / withTrashed: Query soft-deleted or all records.
  • upsertMany: Batch upsert in configurable batch sizes.
  • map / each / eachBatch: Transform, iterate, and batch-process query results.
  • lockForUpdate: Pessimistic row locking for read-modify-write.
  • firstOrCreate / updateOrCreate: Laravel-style find-or-create patterns.
  • first / last / random: Single-record shortcuts.
  • StabilizeEmitter: Event system for query, error, connection:open/close, transaction:start/complete/error.
  • TransactionIsolationLevel: Type for READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE.
  • generateUUID: Cross-runtime UUID generation helper.
  • poolStats: Get connection pool statistics.
  • Database Backup & Restore: db:backup and db:restore commands for database backup management.
  • API Generation: generate:api command scaffolds full CRUD REST API routes from models.
  • Fresh Migrations: migrate:fresh drops all tables and re-runs migrations without seeding.
  • Database Size Analysis: db:size command shows table sizes and row count statistics.

📦 Installation

Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).

# Using Bun
bun add stabilize-orm
# Using npm
npm install stabilize-orm

📃 Documentation & Community

Examples

  • Blog - Blog with users, posts, comments, and versioning
  • E-Commerce - Products, categories, orders with transactions
  • REST API - Express.js REST API with pagination and optimistic locking
  • SaaS - Multi-tenant SaaS with tenants, members, and scoped projects
  • CMS - Content management with authors, categories, articles, and versioning
  • Analytics - Event tracking with aggregations and metrics

⚙️ Configuration

Create a database configuration file.

// config/database.tsimport{DBType,typeDBConfig}from"stabilize-orm";constdbConfig: DBConfig={type: DBType.Postgres,connectionString:
process.env.DATABASE_URL||"postgres://user:password@localhost:5432/mydb",retryAttempts: 3,retryDelay: 1000,};exportdefaultdbConfig;

Next, create a central ORM instance for your application.

// db.tsimport{Stabilize,typeCacheConfig,typeLoggerConfig,LogLevel,}from"stabilize-orm";importdbConfigfrom"./database";constcacheConfig: CacheConfig={enabled: process.env.CACHE_ENABLED==="true",redisUrl: process.env.REDIS_URL,ttl: 60,};constloggerConfig: LoggerConfig={level: LogLevel.Info,filePath: "logs/stabilize.log",maxFileSize: 5*1024*1024,// 5MBmaxFiles: 3,};exportconstorm=newStabilize(dbConfig,cacheConfig,loggerConfig);

🏗️ Models & Relationships

Define your tables as classes using the defineModel function. The DataTypes enum ensures database-agnostic schemas.

Example: Users and Roles (Many-to-Many) with Versioning

// models/User.tsimport{defineModel,DataTypes,RelationType}from"stabilize-orm";import{UserRole}from"./UserRole";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true,unique: true,},},relations: [{type: RelationType.OneToMany,target: ()=>UserRole,property: "roles",foreignKey: "userId",},],hooks: {beforeCreate: (entity)=>console.log(`Creating user: ${entity.email}`),},});export{User};

🔍 Pagination

The built-in pagination helper makes it easy to retrieve a page of rows using LIMIT/OFFSET semantics.

constusers=awaituserRepository.find().paginate(2,10).execute(dbClient);// users = [...] // Returns an array of rows for that page

Or, use the query builder:

constusers=awaituserRepository.find().where('isActive = ?',true).paginate(1,20).execute(dbClient);

🛡️ Advanced Validation

Models can define advanced validation rules for columns, including:

  • required
  • minLength / maxLength
  • pattern (RegExp)
  • customValidator (function)

Validation errors are thrown on create/update if data is invalid.

constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,required: true,unique: true,minLength: 6,pattern: /^[^@]+@[^@]+\.[^@]+$/,customValidator: (val)=>val.endsWith("@offbytesecure.com")||"Must use an @offbytesecure.com email",},password: {type: DataTypes.String,minLength: 8},},});

⏳ Versioning & Auditing

Enable automatic history tracking and time-travel queries by setting versioned: true in your model configuration.

  • Each change is recorded in a <table>_history table with version, operation, and audit columns.
  • Supports snapshot queries, rollbacks, audits, and time-travel.

Versioning Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},},});// --- Using versioning features:constuserRepository=orm.getRepository(User);// Rollback to a previous versionawaituserRepository.rollback(1,3);// roll back user with id=1 to version 3// Get a snapshot as of a specific dateconstuserAsOf=awaituserRepository.asOf(1,newDate("2025-01-01T00:00:00Z"));console.log(userAsOf);// View full version historyconsthistory=awaituserRepository.history(1);console.log(history);

🔄 Model Lifecycle Hooks

Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.

Hooks Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},hooks: {beforeCreate: (entity)=>{entity.createdAt=newDate();},beforeUpdate: (entity)=>{entity.updatedAt=newDate();},afterCreate: (entity)=>{console.log(`User created: ${entity.name}`);},},});// Add a hook as a class methodUser.prototype.afterUpdate=asyncfunction(){console.log(`Updated user: ${this.name}`);};export{User};

Supported hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeSave, afterSave.


💻 Command-Line Interface (CLI)

Stabilize includes a powerful CLI for managing your workflow. See: stabilize-cli on GitHub

Generating Files

  • Generate a model:

    stabilize-cli generate:model Product
  • Generate a migration from a model:

    stabilize-cli generate:migration User
  • Generate a seed file:

    stabilize-cli generate:seed InitialRoles
  • Generate a REST API scaffold:

    stabilize-cli generate:api User

Database & Migration Management

  • Run all pending migrations:

    stabilize-cli migrate
  • Roll back the last migration:

    stabilize-cli migrate:rollback
  • Fresh migration (drop + re-migrate):

    stabilize-cli migrate:fresh --force
  • Run all pending seeds (in dependency order):

    stabilize-cli seed
  • Check the status of migrations and seeds:

    stabilize-cli status
  • Reset the database (drop, migrate, seed):

    stabilize-cli db:reset

Backup & Restore

  • Backup the database:

    stabilize-cli db:backup
  • Restore from a backup:

    stabilize-cli db:restore backups/backup_20250101120000.db --force

Diagnostics

  • Database size statistics:

    stabilize-cli db:size
  • Health check:

    stabilize-cli health
  • CLI info:

    stabilize-cli info

🧑‍💻 Querying Data

Basic CRUD with Repositories

import{orm}from"./db";import{User}from"./models/User";constuserRepository=orm.getRepository(User);constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});constfoundUser=awaituserRepository.findOne(newUser.id);constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});awaituserRepository.delete(newUser.id);

Advanced Queries with the Query Builder

constactiveAdmins=awaitorm.getRepository(UserRole).find().join("users","user_roles.user_id = users.id").join("roles","user_roles.role_id = roles.id").select("users.id","users.email","roles.name as role_name").where("roles.name = ?","Admin").orderBy("users.email ASC").execute();console.log(activeAdmins);

Query Builder API

{select(...fields: string[]): QueryBuilder<User>;where(condition: string, ...params: any[]): QueryBuilder<User>;orWhere(condition: string, ...params: any[]): QueryBuilder<User>;whereIn(column: string,values: any[]): QueryBuilder<User>;whereNotIn(column: string,values: any[]): QueryBuilder<User>;whereNull(column: string): QueryBuilder<User>;whereNotNull(column: string): QueryBuilder<User>;whereBetween(column: string,start: any,end: any): QueryBuilder<User>;groupBy(clause: string): QueryBuilder<User>;having(condition: string, ...params: any[]): QueryBuilder<User>;join(table: string,condition: string): QueryBuilder<User>;orderBy(clause: string): QueryBuilder<User>;limit(limit: number): QueryBuilder<User>;offset(offset: number): QueryBuilder<User>;lock(mode?: "FOR UPDATE"|"FOR SHARE"): QueryBuilder<User>;withRelations(...relations: string[]): QueryBuilder<User>;scope(name: string, ...args: any[]): QueryBuilder<User>;paginate(page: number,pageSize: number): QueryBuilder<User>;build(): { query: string; params: any[]};execute(client?: DBClient,cache?: Cache,cacheKey?: string): Promise<User[]>;}

Custom Query Scopes

Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the scope method on Repository or QueryBuilder, allowing you to chain them with other query operations.

Scopes Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},isActive: {type: DataTypes.Boolean,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},scopes: {active: (qb)=>qb.where("isActive = ?",true),recent: (qb,days: number)=>qb.where("createdAt >= ?",newDate(Date.now()-days*24*60*60*1000),),},});constuserRepository=orm.getRepository(User);// Fetch active usersconstactiveUsers=awaituserRepository.scope("active").execute();// Fetch users created in the last 7 daysconstrecentUsers=awaituserRepository.scope("recent",7).execute();// Combine scopes with other query operationsconstrecentActiveUsers=awaituserRepository.scope("active").scope("recent",7).orderBy("createdAt DESC").limit(10).execute();console.log(recentActiveUsers);

Timestamps

Enable automatic management of createdAt and updatedAt columns by setting timestamps in your model configuration. The ORM automatically sets these fields during create, update, bulkCreate, bulkUpdate, and upsert operations in a TypeScript-safe manner, eliminating the need for manual hooks.

Timestamps Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},timestamps: {createdAt: "createdAt",updatedAt: "updatedAt",},});constuserRepository=orm.getRepository(User);// Create a user (createdAt and updatedAt set automatically)constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});console.log(newUser.createdAt,newUser.updatedAt);// Outputs current timestamp// Update a user (updatedAt updated automatically)constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});console.log(updatedUser.updatedAt);// Outputs new timestamp// Bulk create usersconstnewUsers=awaituserRepository.bulkCreate([{email: "user1@example.com"},{email: "user2@example.com"},]);console.log(newUsers.map((u)=>u.createdAt));// Outputs timestamps for each user

🗑️ Soft Deletes

Enable soft deletes by setting softDelete: true and marking a column (e.g., deletedAt) with softDelete: true in the model configuration.

  • Use repository.delete(id) to mark an entity as deleted.
  • Use repository.recover(id) to restore a soft-deleted entity.
  • Queries automatically exclude soft-deleted rows unless specified otherwise.

Soft Delete Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",softDelete: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},deletedAt: {type: DataTypes.DateTime,softDelete: true},},});constuserRepository=orm.getRepository(User);awaituserRepository.create({email: "lwazicd@icloud.com"});awaituserRepository.delete(1);// Soft deleteawaituserRepository.recover(1);// Recover

🌐 Express.js Integration

Stabilize ORM works seamlessly with web frameworks like Express.

importexpressfrom"express";import{orm}from"./db";import{User}from"./models/User";constapp=express();app.use(express.json());constuserRepository=orm.getRepository(User);app.get("/users",async(req,res)=>{try{constusers=awaituserRepository.find().execute();res.json(users);}catch(err){res.status(500).json({error: "Failed to fetch users."});}});app.post("/users",async(req,res)=>{try{constuser=awaituserRepository.create(req.body);res.status(201).json(user);}catch(err){res.status(500).json({error: "User creation failed."});}});app.listen(3000,()=>{console.log("Server listening on port 3000");});

🧑‍🔬 Testing & Time-Travel

  • Use time-travel queries to inspect historical entity states.
  • Assert audit trails and rollback operations in your tests.

🔒 Optimistic Locking

Enable optimistic locking to detect concurrent modifications. Add optimisticLock: true to a version column in your model.

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String},version: {type: DataTypes.Integer,optimisticLock: true},},});constuserRepository=orm.getRepository(User);// Create with initial versionconstuser=awaituserRepository.create({name: "Lwazi",version: 1});// Update - version is automatically incremented// If another transaction modified the record, a CONCURRENT_MODIFICATION error is throwntry{awaituserRepository.update(user.id,{name: "Updated",version: user.version,});}catch(err){if(err.code==="CONCURRENT_MODIFICATION"){console.log("Record was modified by another transaction");}}

⏱️ SQL Default Expressions

Use sqlDefault() to set database-side default values for columns (e.g., gen_random_uuid(), NOW()).

import{defineModel,DataTypes,sqlDefault}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.UUID,required: true,defaultExpression: sqlDefault("gen_random_uuid()"),},name: {type: DataTypes.String},createdAt: {type: DataTypes.DateTime,defaultExpression: sqlDefault("NOW()"),},},});

📊 Advanced Query Builder

The query builder now supports additional filter methods:

constresults=awaituserRepository.find().where("status = ?","active").orWhere("role = ?","admin").whereIn("age",[25,30,35]).whereBetween("createdAt",newDate("2025-01-01"),newDate("2025-12-31")).whereNull("deletedAt").groupBy("department").having("COUNT(*) > ?",5).orderBy("createdAt DESC").limit(10).execute();

🔗 Nested Relations

Load deeply nested relations using dot notation:

constuser=awaituserRepository.findOne(1,{relations: ["roles","roles.permissions"],});

📊 Aggregation Queries

Run aggregate queries directly on the repository or query builder.

// Repository-level aggregatesconsttotal=awaituserRepository.count();constexists=awaituserRepository.exists({email: "admin@offbytesecure.com",});conststats=awaituserRepository.aggregate({count: "*",sum: ["salary"],avg: ["salary"],min: ["salary"],max: ["salary"],});// stats = { count_: 100, sum_salary: 5000000, avg_salary: 50000, min_salary: 20000, max_salary: 150000 }

🔎 findOneBy / findBy

TypeORM-style conditional finders without writing raw SQL.

// Find one record by conditionconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"});// Find multiple recordsconstadmins=awaituserRepository.findBy({role: "admin"},{limit: 10,orderBy: "createdAt DESC"},);// Combined with relationsconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"},{relations: ["roles"]},);

🔢 findAndCount

Get paginated results with a total count in a single call.

const{ data, total }=awaituserRepository.findAndCount();console.log(`Showing ${data.length} of ${total} total records`);

🖱️ Cursor-Based Pagination

Efficient cursor-based pagination for large datasets.

// First pageconstpage1=awaituserRepository.findMany({take: 10,orderBy: {field: "id",direction: "ASC"},});// Next page using cursorconstlastId=page1[page1.length-1].id;constpage2=awaituserRepository.findMany({cursor: {field: "id",value: lastId,direction: "forward"},take: 10,orderBy: {field: "id",direction: "ASC"},});

🌱 Database Seeding

Define and run seeds for populating development/test databases.

import{defineSeed,runSeeds,resetDatabase}from"stabilize-orm";// Define a seeddefineSeed("create-default-roles",async(db)=>{constroleRepo=orm.getRepository(Role);awaitroleRepo.bulkCreate([{name: "Admin",permissions: "all"},{name: "User",permissions: "read"},]);});// Run all seedsawaitrunSeeds(orm.client);// Reset database (drop, migrate, seed)awaitresetDatabase(orm.client,[User,Role]);

🏥 Health Check

Monitor database and cache connectivity with latency.

consthealth=awaitorm.healthCheck();// { status: "healthy", database: "postgres", latencyMs: 12.5, cacheStatus: "connected" }// Per-table healthconstuserHealth=awaituserRepository.healthCheck();// { status: "healthy", table: "users", rows: 142, latencyMs: 8.3 }

📂 Bulk Upsert

Upsert multiple records in a single transaction.

constusers=awaituserRepository.bulkUpsert([{email: "lwazicd@icloud.com",name: "Lwazi"},{email: "ciniso@icloud.com",name: "Ciniso"},],["email"],// unique key(s));

🔧 Raw SQL

Execute raw SQL queries directly from the ORM instance.

constresults=awaitorm.rawQuery("SELECT * FROM users WHERE age > ?",[25]);const{ affectedRows }=awaitorm.rawExec("UPDATE users SET active = false WHERE last_login < ?",[oneYearAgo],);

🗑️ recoverAll / truncate

// Restore all soft-deleted recordsconstrecovered=awaituserRepository.recoverAll();console.log(`Recovered ${recovered} records`);// Clear the tableawaituserRepository.truncate();

⬆️⬇️ Increment / Decrement

Atomically update numeric fields without loading the record.

constupdated=awaituserRepository.increment(user.id,"loginCount",1);constupdated=awaituserRepository.decrement(user.id,"credits",5);

🏷️ Pluck / SelectColumns

// Get just the email column as an arrayconstemails=awaituserRepository.pluck("email");// ["lwazicd@icloud.com", "ciniso@icloud.com", ...]// Get specific columnsconstusers=awaituserRepository.selectColumns("id","email");// [{ id: 1, email: "lwazicd@icloud.com" }, ...]

🔄 Toggle

Toggle a boolean field.

consttoggled=awaituserRepository.toggle(user.id,"isActive");// isActive was true, now false (or vice versa)

✏️ updateBy / deleteBy

// Update all active users' role to "member"constupdated=awaituserRepository.updateBy({isActive: true},{role: "member"},);// Delete all users with null emailconstdeleted=awaituserRepository.deleteBy({email: null});

🕳️ findDeleted / withTrashed

// Get only soft-deleted recordsconstdeletedUsers=awaituserRepository.findDeleted().execute();// Get all records including soft-deletedconstallUsers=awaituserRepository.withTrashed().execute();// Restore matching soft-deleted recordsconstrestored=awaituserRepository.restoreBy({role: "admin"});

🔄 firstOrCreate / updateOrCreate

// Find or create in one callconstuser=awaituserRepository.firstOrCreate({email: "lwazicd@icloud.com"},{name: "Lwazi"},);// Find and update, or create if not foundconstuser=awaituserRepository.updateOrCreate({email: "lwazicd@icloud.com"},{name: "Updated Name"},);

🥇 first / last / random

constfirstUser=awaituserRepository.first();constadmin=awaituserRepository.first({role: "admin"});constlastUser=awaituserRepository.last();constrandomUser=awaituserRepository.random();

🔒 Pessimistic Locking (lockForUpdate)

constuser=awaituserRepository.lockForUpdate(user.id);// The row is now locked for the duration of the transaction

📡 Event Emitter

Subscribe to ORM lifecycle events.

constorm=newStabilize(dbConfig);orm.events.on("query",(entry)=>{console.log(`[${entry.durationMs}ms] ${entry.query}`);});orm.events.on("error",(err)=>{console.error("ORM error:",err);});orm.events.on("connection:open",(dbType)=>{console.log(`Connected to ${dbType}`);});

📦 Pool Stats

conststats=awaitorm.poolStats();// { active: 5, idle: 10, total: 15 }

🆔 generateUUID

import{generateUUID}from"stabilize-orm";constid=generateUUID();// "550e8400-e29b-41d4-a716-446655440000"

📑 License

Licensed under the MIT License. See LICENSE.md for details.


Created with ❤️ by ElectronSz
File last updated: 2026-04-02

About

A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Latest commit

History

41 Commits

Folders and files

NameName
Last commit message
Last commit date

Repository files navigation

Stabilize ORM

A Modern, Type-Safe, and Expressive ORM for Bun

Stabilize ORM Logo

NPM VersionLicenseStabilize CLIPostgreSQLMySQLSQLiteBuild StatusMIT License

Stabilize is a lightweight, feature-rich ORM designed for performance and developer experience. It provides a unified, database-agnostic API for PostgreSQL, MySQL, and SQLite. Powered by a robust query builder, programmatic model definitions, automatic versioning, and a full-featured command-line interface, Stabilize is built to scale with your app.


🚀 Features

  • Unified API: Write once, run on PostgreSQL, MySQL, or SQLite.
  • Programmatic Model Definitions: Define models and columns using the defineModel API with the DataTypes enum for database-agnostic schemas.
  • Full-Featured CLI: Generate models, manage migrations, seed data, and reset your database from the command line with stabilize-cli.
  • Automatic Migrations: Generate database-specific SQL schemas directly from your model definitions.
  • Versioned Models & Time-Travel: Enable versioning in your model configuration for automatic history tables and snapshot queries.
  • Retry Logic: Automatic exponential backoff for database queries to handle transient connection issues.
  • Connection Pooling: Efficient connection management for PostgreSQL and MySQL.
  • Transactional Integrity: Built-in support for atomic transactions with automatic rollback on failure.
  • Advanced Query Builder: Fluent, chainable API for building complex queries, including joins, filters, ordering, and pagination.
  • Pagination Helper: Easily paginate any query with .paginate(page, pageSize) and get { data, total, page, pageSize }.
  • Advanced Model Validation: Enforce rules like required, minLength, maxLength, pattern, and custom validators—errors are thrown on invalid input.
  • Model Relationships: Define OneToOne, ManyToOne, OneToMany, and ManyToMany relationships in the model configuration.
  • Soft Deletes: Enable soft deletes in the model configuration for transparent "deleted" flags and safe row removal.
  • Lifecycle Hooks: Define hooks in the model configuration or as class methods for lifecycle events like beforeCreate, afterUpdate, etc.
  • Pluggable Logging: Includes a robust StabilizeLogger with support for file-based, rotating logs.
  • Custom Errors: StabilizeError provides clear, consistent error handling.
  • Caching Layer: Optional Redis-backed caching with cache-aside and write-through strategies.
  • Custom Query Scopes: Define reusable query conditions (scopes) in models for simplified, reusable filtering logic.
  • Timestamps: Automatically manage createdAt and updatedAt columns for tracking record creation and update times.
  • SQL Default Expressions: Support database-side default expressions (e.g., gen_random_uuid(), NOW()) for columns using the sqlDefault() helper.
  • Nested Relations (Eager Loading): Load deeply nested relations using dot notation like "roles.permissions".
  • AutoMigrate with Index Management: Automatically create, detect, and remove indexes and unique constraints during migration.
  • Advanced Query Builder Filters: Chainable .orWhere(), .whereIn(), .whereNotIn(), .whereNull(), .whereNotNull(), .whereBetween(), .groupBy(), .having(), .lock() methods.
  • Optimistic Locking: Add optimisticLock: true to a version column to automatically detect concurrent modification conflicts and throw CONCURRENT_MODIFICATION errors.
  • findAndCount: Get paginated results with a total count in one call.
  • findOneBy / findBy: TypeORM-style conditional finders without writing raw SQL.
  • Aggregate Queries: Run count(), sum(), avg(), min(), max() directly from the repository or query builder.
  • Cursor-Based Pagination: Efficient forward/backward cursor pagination for large datasets (Prisma-style findMany).
  • exists: Check if a record exists without loading it.
  • recoverAll: Bulk restore all soft-deleted records.
  • truncate: Clear all rows from a table.
  • seed / defineSeed: Laravel-style seeding framework with defineSeed and runSeeds.
  • resetDatabase: Drop tables, re-migrate, and optionally re-seed for development.
  • healthCheck: Get database and table health status with latency for monitoring endpoints.
  • rawQuery / rawExec: Execute raw SQL directly from the Stabilize instance.
  • bulkUpsert: Upsert multiple records in a single transaction.
  • findMany: Prisma-style query with where, cursor, take, skip, orderBy.
  • countDistinct: Count unique values in a column.
  • increment / decrement: Atomically increment or decrement a numeric field.
  • pluck: Get an array of a single column's values (Rails-style).
  • selectColumns: Get only specific columns from a query.
  • toggle: Toggle a boolean field (Rails-style).
  • updateBy / deleteBy: Conditional bulk updates and deletes without writing SQL.
  • restoreBy: Restore soft-deleted records matching conditions.
  • findDeleted / withTrashed: Query soft-deleted or all records.
  • upsertMany: Batch upsert in configurable batch sizes.
  • map / each / eachBatch: Transform, iterate, and batch-process query results.
  • lockForUpdate: Pessimistic row locking for read-modify-write.
  • firstOrCreate / updateOrCreate: Laravel-style find-or-create patterns.
  • first / last / random: Single-record shortcuts.
  • StabilizeEmitter: Event system for query, error, connection:open/close, transaction:start/complete/error.
  • TransactionIsolationLevel: Type for READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE.
  • generateUUID: Cross-runtime UUID generation helper.
  • poolStats: Get connection pool statistics.
  • Database Backup & Restore: db:backup and db:restore commands for database backup management.
  • API Generation: generate:api command scaffolds full CRUD REST API routes from models.
  • Fresh Migrations: migrate:fresh drops all tables and re-runs migrations without seeding.
  • Database Size Analysis: db:size command shows table sizes and row count statistics.

📦 Installation

Stabilize ORM requires a modern JavaScript runtime (Bun v1.3+).

# Using Bun
bun add stabilize-orm
# Using npm
npm install stabilize-orm

📃 Documentation & Community

Examples

  • Blog - Blog with users, posts, comments, and versioning
  • E-Commerce - Products, categories, orders with transactions
  • REST API - Express.js REST API with pagination and optimistic locking
  • SaaS - Multi-tenant SaaS with tenants, members, and scoped projects
  • CMS - Content management with authors, categories, articles, and versioning
  • Analytics - Event tracking with aggregations and metrics

⚙️ Configuration

Create a database configuration file.

// config/database.tsimport{DBType,typeDBConfig}from"stabilize-orm";constdbConfig: DBConfig={type: DBType.Postgres,connectionString:
process.env.DATABASE_URL||"postgres://user:password@localhost:5432/mydb",retryAttempts: 3,retryDelay: 1000,};exportdefaultdbConfig;

Next, create a central ORM instance for your application.

// db.tsimport{Stabilize,typeCacheConfig,typeLoggerConfig,LogLevel,}from"stabilize-orm";importdbConfigfrom"./database";constcacheConfig: CacheConfig={enabled: process.env.CACHE_ENABLED==="true",redisUrl: process.env.REDIS_URL,ttl: 60,};constloggerConfig: LoggerConfig={level: LogLevel.Info,filePath: "logs/stabilize.log",maxFileSize: 5*1024*1024,// 5MBmaxFiles: 3,};exportconstorm=newStabilize(dbConfig,cacheConfig,loggerConfig);

🏗️ Models & Relationships

Define your tables as classes using the defineModel function. The DataTypes enum ensures database-agnostic schemas.

Example: Users and Roles (Many-to-Many) with Versioning

// models/User.tsimport{defineModel,DataTypes,RelationType}from"stabilize-orm";import{UserRole}from"./UserRole";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true,unique: true,},},relations: [{type: RelationType.OneToMany,target: ()=>UserRole,property: "roles",foreignKey: "userId",},],hooks: {beforeCreate: (entity)=>console.log(`Creating user: ${entity.email}`),},});export{User};

🔍 Pagination

The built-in pagination helper makes it easy to retrieve a page of rows using LIMIT/OFFSET semantics.

constusers=awaituserRepository.find().paginate(2,10).execute(dbClient);// users = [...] // Returns an array of rows for that page

Or, use the query builder:

constusers=awaituserRepository.find().where('isActive = ?',true).paginate(1,20).execute(dbClient);

🛡️ Advanced Validation

Models can define advanced validation rules for columns, including:

  • required
  • minLength / maxLength
  • pattern (RegExp)
  • customValidator (function)

Validation errors are thrown on create/update if data is invalid.

constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,required: true,unique: true,minLength: 6,pattern: /^[^@]+@[^@]+\.[^@]+$/,customValidator: (val)=>val.endsWith("@offbytesecure.com")||"Must use an @offbytesecure.com email",},password: {type: DataTypes.String,minLength: 8},},});

⏳ Versioning & Auditing

Enable automatic history tracking and time-travel queries by setting versioned: true in your model configuration.

  • Each change is recorded in a <table>_history table with version, operation, and audit columns.
  • Supports snapshot queries, rollbacks, audits, and time-travel.

Versioning Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",versioned: true,columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},},});// --- Using versioning features:constuserRepository=orm.getRepository(User);// Rollback to a previous versionawaituserRepository.rollback(1,3);// roll back user with id=1 to version 3// Get a snapshot as of a specific dateconstuserAsOf=awaituserRepository.asOf(1,newDate("2025-01-01T00:00:00Z"));console.log(userAsOf);// View full version historyconsthistory=awaituserRepository.history(1);console.log(history);

🔄 Model Lifecycle Hooks

Stabilize ORM supports lifecycle hooks defined in the model configuration or as class methods. You can run logic before/after create, update, delete, or save.

Hooks Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String,length: 100},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},hooks: {beforeCreate: (entity)=>{entity.createdAt=newDate();},beforeUpdate: (entity)=>{entity.updatedAt=newDate();},afterCreate: (entity)=>{console.log(`User created: ${entity.name}`);},},});// Add a hook as a class methodUser.prototype.afterUpdate=asyncfunction(){console.log(`Updated user: ${this.name}`);};export{User};

Supported hooks: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, beforeSave, afterSave.


💻 Command-Line Interface (CLI)

Stabilize includes a powerful CLI for managing your workflow. See: stabilize-cli on GitHub

Generating Files

  • Generate a model:

    stabilize-cli generate:model Product
  • Generate a migration from a model:

    stabilize-cli generate:migration User
  • Generate a seed file:

    stabilize-cli generate:seed InitialRoles
  • Generate a REST API scaffold:

    stabilize-cli generate:api User

Database & Migration Management

  • Run all pending migrations:

    stabilize-cli migrate
  • Roll back the last migration:

    stabilize-cli migrate:rollback
  • Fresh migration (drop + re-migrate):

    stabilize-cli migrate:fresh --force
  • Run all pending seeds (in dependency order):

    stabilize-cli seed
  • Check the status of migrations and seeds:

    stabilize-cli status
  • Reset the database (drop, migrate, seed):

    stabilize-cli db:reset

Backup & Restore

  • Backup the database:

    stabilize-cli db:backup
  • Restore from a backup:

    stabilize-cli db:restore backups/backup_20250101120000.db --force

Diagnostics

  • Database size statistics:

    stabilize-cli db:size
  • Health check:

    stabilize-cli health
  • CLI info:

    stabilize-cli info

🧑‍💻 Querying Data

Basic CRUD with Repositories

import{orm}from"./db";import{User}from"./models/User";constuserRepository=orm.getRepository(User);constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});constfoundUser=awaituserRepository.findOne(newUser.id);constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});awaituserRepository.delete(newUser.id);

Advanced Queries with the Query Builder

constactiveAdmins=awaitorm.getRepository(UserRole).find().join("users","user_roles.user_id = users.id").join("roles","user_roles.role_id = roles.id").select("users.id","users.email","roles.name as role_name").where("roles.name = ?","Admin").orderBy("users.email ASC").execute();console.log(activeAdmins);

Query Builder API

{select(...fields: string[]): QueryBuilder<User>;where(condition: string, ...params: any[]): QueryBuilder<User>;orWhere(condition: string, ...params: any[]): QueryBuilder<User>;whereIn(column: string,values: any[]): QueryBuilder<User>;whereNotIn(column: string,values: any[]): QueryBuilder<User>;whereNull(column: string): QueryBuilder<User>;whereNotNull(column: string): QueryBuilder<User>;whereBetween(column: string,start: any,end: any): QueryBuilder<User>;groupBy(clause: string): QueryBuilder<User>;having(condition: string, ...params: any[]): QueryBuilder<User>;join(table: string,condition: string): QueryBuilder<User>;orderBy(clause: string): QueryBuilder<User>;limit(limit: number): QueryBuilder<User>;offset(offset: number): QueryBuilder<User>;lock(mode?: "FOR UPDATE"|"FOR SHARE"): QueryBuilder<User>;withRelations(...relations: string[]): QueryBuilder<User>;scope(name: string, ...args: any[]): QueryBuilder<User>;paginate(page: number,pageSize: number): QueryBuilder<User>;build(): { query: string; params: any[]};execute(client?: DBClient,cache?: Cache,cacheKey?: string): Promise<User[]>;}

Custom Query Scopes

Define reusable query conditions (scopes) in your model configuration to simplify and reuse common filtering logic. Scopes are applied via the scope method on Repository or QueryBuilder, allowing you to chain them with other query operations.

Scopes Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},isActive: {type: DataTypes.Boolean,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},scopes: {active: (qb)=>qb.where("isActive = ?",true),recent: (qb,days: number)=>qb.where("createdAt >= ?",newDate(Date.now()-days*24*60*60*1000),),},});constuserRepository=orm.getRepository(User);// Fetch active usersconstactiveUsers=awaituserRepository.scope("active").execute();// Fetch users created in the last 7 daysconstrecentUsers=awaituserRepository.scope("recent",7).execute();// Combine scopes with other query operationsconstrecentActiveUsers=awaituserRepository.scope("active").scope("recent",7).orderBy("createdAt DESC").limit(10).execute();console.log(recentActiveUsers);

Timestamps

Enable automatic management of createdAt and updatedAt columns by setting timestamps in your model configuration. The ORM automatically sets these fields during create, update, bulkCreate, bulkUpdate, and upsert operations in a TypeScript-safe manner, eliminating the need for manual hooks.

Timestamps Example

import{defineModel,DataTypes}from"stabilize-orm";import{orm}from"./db";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},createdAt: {type: DataTypes.DateTime},updatedAt: {type: DataTypes.DateTime},},timestamps: {createdAt: "createdAt",updatedAt: "updatedAt",},});constuserRepository=orm.getRepository(User);// Create a user (createdAt and updatedAt set automatically)constnewUser=awaituserRepository.create({email: "lwazicd@icloud.com"});console.log(newUser.createdAt,newUser.updatedAt);// Outputs current timestamp// Update a user (updatedAt updated automatically)constupdatedUser=awaituserRepository.update(newUser.id,{email: "admin@offbytesecure.com",});console.log(updatedUser.updatedAt);// Outputs new timestamp// Bulk create usersconstnewUsers=awaituserRepository.bulkCreate([{email: "user1@example.com"},{email: "user2@example.com"},]);console.log(newUsers.map((u)=>u.createdAt));// Outputs timestamps for each user

🗑️ Soft Deletes

Enable soft deletes by setting softDelete: true and marking a column (e.g., deletedAt) with softDelete: true in the model configuration.

  • Use repository.delete(id) to mark an entity as deleted.
  • Use repository.recover(id) to restore a soft-deleted entity.
  • Queries automatically exclude soft-deleted rows unless specified otherwise.

Soft Delete Example

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",softDelete: true,columns: {id: {type: DataTypes.Integer,required: true},email: {type: DataTypes.String,length: 100,required: true},deletedAt: {type: DataTypes.DateTime,softDelete: true},},});constuserRepository=orm.getRepository(User);awaituserRepository.create({email: "lwazicd@icloud.com"});awaituserRepository.delete(1);// Soft deleteawaituserRepository.recover(1);// Recover

🌐 Express.js Integration

Stabilize ORM works seamlessly with web frameworks like Express.

importexpressfrom"express";import{orm}from"./db";import{User}from"./models/User";constapp=express();app.use(express.json());constuserRepository=orm.getRepository(User);app.get("/users",async(req,res)=>{try{constusers=awaituserRepository.find().execute();res.json(users);}catch(err){res.status(500).json({error: "Failed to fetch users."});}});app.post("/users",async(req,res)=>{try{constuser=awaituserRepository.create(req.body);res.status(201).json(user);}catch(err){res.status(500).json({error: "User creation failed."});}});app.listen(3000,()=>{console.log("Server listening on port 3000");});

🧑‍🔬 Testing & Time-Travel

  • Use time-travel queries to inspect historical entity states.
  • Assert audit trails and rollback operations in your tests.

🔒 Optimistic Locking

Enable optimistic locking to detect concurrent modifications. Add optimisticLock: true to a version column in your model.

import{defineModel,DataTypes}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.Integer,required: true},name: {type: DataTypes.String},version: {type: DataTypes.Integer,optimisticLock: true},},});constuserRepository=orm.getRepository(User);// Create with initial versionconstuser=awaituserRepository.create({name: "Lwazi",version: 1});// Update - version is automatically incremented// If another transaction modified the record, a CONCURRENT_MODIFICATION error is throwntry{awaituserRepository.update(user.id,{name: "Updated",version: user.version,});}catch(err){if(err.code==="CONCURRENT_MODIFICATION"){console.log("Record was modified by another transaction");}}

⏱️ SQL Default Expressions

Use sqlDefault() to set database-side default values for columns (e.g., gen_random_uuid(), NOW()).

import{defineModel,DataTypes,sqlDefault}from"stabilize-orm";constUser=defineModel({tableName: "users",columns: {id: {type: DataTypes.UUID,required: true,defaultExpression: sqlDefault("gen_random_uuid()"),},name: {type: DataTypes.String},createdAt: {type: DataTypes.DateTime,defaultExpression: sqlDefault("NOW()"),},},});

📊 Advanced Query Builder

The query builder now supports additional filter methods:

constresults=awaituserRepository.find().where("status = ?","active").orWhere("role = ?","admin").whereIn("age",[25,30,35]).whereBetween("createdAt",newDate("2025-01-01"),newDate("2025-12-31")).whereNull("deletedAt").groupBy("department").having("COUNT(*) > ?",5).orderBy("createdAt DESC").limit(10).execute();

🔗 Nested Relations

Load deeply nested relations using dot notation:

constuser=awaituserRepository.findOne(1,{relations: ["roles","roles.permissions"],});

📊 Aggregation Queries

Run aggregate queries directly on the repository or query builder.

// Repository-level aggregatesconsttotal=awaituserRepository.count();constexists=awaituserRepository.exists({email: "admin@offbytesecure.com",});conststats=awaituserRepository.aggregate({count: "*",sum: ["salary"],avg: ["salary"],min: ["salary"],max: ["salary"],});// stats = { count_: 100, sum_salary: 5000000, avg_salary: 50000, min_salary: 20000, max_salary: 150000 }

🔎 findOneBy / findBy

TypeORM-style conditional finders without writing raw SQL.

// Find one record by conditionconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"});// Find multiple recordsconstadmins=awaituserRepository.findBy({role: "admin"},{limit: 10,orderBy: "createdAt DESC"},);// Combined with relationsconstuser=awaituserRepository.findOneBy({email: "lwazicd@icloud.com"},{relations: ["roles"]},);

🔢 findAndCount

Get paginated results with a total count in a single call.

const{ data, total }=awaituserRepository.findAndCount();console.log(`Showing ${data.length} of ${total} total records`);

🖱️ Cursor-Based Pagination

Efficient cursor-based pagination for large datasets.

// First pageconstpage1=awaituserRepository.findMany({take: 10,orderBy: {field: "id",direction: "ASC"},});// Next page using cursorconstlastId=page1[page1.length-1].id;constpage2=awaituserRepository.findMany({cursor: {field: "id",value: lastId,direction: "forward"},take: 10,orderBy: {field: "id",direction: "ASC"},});

🌱 Database Seeding

Define and run seeds for populating development/test databases.

import{defineSeed,runSeeds,resetDatabase}from"stabilize-orm";// Define a seeddefineSeed("create-default-roles",async(db)=>{constroleRepo=orm.getRepository(Role);awaitroleRepo.bulkCreate([{name: "Admin",permissions: "all"},{name: "User",permissions: "read"},]);});// Run all seedsawaitrunSeeds(orm.client);// Reset database (drop, migrate, seed)awaitresetDatabase(orm.client,[User,Role]);

🏥 Health Check

Monitor database and cache connectivity with latency.

consthealth=awaitorm.healthCheck();// { status: "healthy", database: "postgres", latencyMs: 12.5, cacheStatus: "connected" }// Per-table healthconstuserHealth=awaituserRepository.healthCheck();// { status: "healthy", table: "users", rows: 142, latencyMs: 8.3 }

📂 Bulk Upsert

Upsert multiple records in a single transaction.

constusers=awaituserRepository.bulkUpsert([{email: "lwazicd@icloud.com",name: "Lwazi"},{email: "ciniso@icloud.com",name: "Ciniso"},],["email"],// unique key(s));

🔧 Raw SQL

Execute raw SQL queries directly from the ORM instance.

constresults=awaitorm.rawQuery("SELECT * FROM users WHERE age > ?",[25]);const{ affectedRows }=awaitorm.rawExec("UPDATE users SET active = false WHERE last_login < ?",[oneYearAgo],);

🗑️ recoverAll / truncate

// Restore all soft-deleted recordsconstrecovered=awaituserRepository.recoverAll();console.log(`Recovered ${recovered} records`);// Clear the tableawaituserRepository.truncate();

⬆️⬇️ Increment / Decrement

Atomically update numeric fields without loading the record.

constupdated=awaituserRepository.increment(user.id,"loginCount",1);constupdated=awaituserRepository.decrement(user.id,"credits",5);

🏷️ Pluck / SelectColumns

// Get just the email column as an arrayconstemails=awaituserRepository.pluck("email");// ["lwazicd@icloud.com", "ciniso@icloud.com", ...]// Get specific columnsconstusers=awaituserRepository.selectColumns("id","email");// [{ id: 1, email: "lwazicd@icloud.com" }, ...]

🔄 Toggle

Toggle a boolean field.

consttoggled=awaituserRepository.toggle(user.id,"isActive");// isActive was true, now false (or vice versa)

✏️ updateBy / deleteBy

// Update all active users' role to "member"constupdated=awaituserRepository.updateBy({isActive: true},{role: "member"},);// Delete all users with null emailconstdeleted=awaituserRepository.deleteBy({email: null});

🕳️ findDeleted / withTrashed

// Get only soft-deleted recordsconstdeletedUsers=awaituserRepository.findDeleted().execute();// Get all records including soft-deletedconstallUsers=awaituserRepository.withTrashed().execute();// Restore matching soft-deleted recordsconstrestored=awaituserRepository.restoreBy({role: "admin"});

🔄 firstOrCreate / updateOrCreate

// Find or create in one callconstuser=awaituserRepository.firstOrCreate({email: "lwazicd@icloud.com"},{name: "Lwazi"},);// Find and update, or create if not foundconstuser=awaituserRepository.updateOrCreate({email: "lwazicd@icloud.com"},{name: "Updated Name"},);

🥇 first / last / random

constfirstUser=awaituserRepository.first();constadmin=awaituserRepository.first({role: "admin"});constlastUser=awaituserRepository.last();constrandomUser=awaituserRepository.random();

🔒 Pessimistic Locking (lockForUpdate)

constuser=awaituserRepository.lockForUpdate(user.id);// The row is now locked for the duration of the transaction

📡 Event Emitter

Subscribe to ORM lifecycle events.

constorm=newStabilize(dbConfig);orm.events.on("query",(entry)=>{console.log(`[${entry.durationMs}ms] ${entry.query}`);});orm.events.on("error",(err)=>{console.error("ORM error:",err);});orm.events.on("connection:open",(dbType)=>{console.log(`Connected to ${dbType}`);});

📦 Pool Stats

conststats=awaitorm.poolStats();// { active: 5, idle: 10, total: 15 }

🆔 generateUUID

import{generateUUID}from"stabilize-orm";constid=generateUUID();// "550e8400-e29b-41d4-a716-446655440000"

📑 License

Licensed under the MIT License. See LICENSE.md for details.


Created with ❤️ by ElectronSz
File last updated: 2026-04-02

About

A lightweight, type-safe ORM for Bun.js with support for SQLite, MySQL, PostgreSQL, and Redis caching

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages