๐ŸŒ AIๆœ็ดข & ไปฃ็† ไธป้กต
Skip to content

ElectronSz/stabilize-orm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

35 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Stabilize ORM

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

Stabilize ORM Logo

NPM Version License Stabilize CLI PostgreSQL MySQL SQLite Build Status MIT 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.

๐Ÿ“ฆ 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


โš™๏ธ Configuration

Create a database configuration file.

// config/database.ts
import { DBType, type DBConfig } from "stabilize-orm";

const dbConfig: DBConfig = {
  type: DBType.Postgres,
  connectionString: process.env.DATABASE_URL || "postgres://user:password@localhost:5432/mydb",
  retryAttempts: 3,
  retryDelay: 1000,
};

export default dbConfig;

Next, create a central ORM instance for your application.

// db.ts
import { Stabilize, type CacheConfig, type LoggerConfig, LogLevel } from "stabilize-orm";
import dbConfig from "./database";

const cacheConfig: CacheConfig = {
  enabled: process.env.CACHE_ENABLED === "true",
  redisUrl: process.env.REDIS_URL,
  ttl: 60,
};

const loggerConfig: LoggerConfig = {
  level: LogLevel.Info,
  filePath: "logs/stabilize.log",
  maxFileSize: 5 * 1024 * 1024, // 5MB
  maxFiles: 3,
};

export const orm = new Stabilize(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.ts
import { defineModel, DataTypes, RelationType } from "stabilize-orm";
import { UserRole } from "./UserRole";

const User = 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 paged results and total counts in a single call.

const page = await userRepository.paginate(2, 10);
// page = { data: [...], total: N, page: 2, pageSize: 10 }

Or, use the query builder:

const page = await userRepository.find().where('isActive = ?', true).paginate(1, 20).execute();

๐Ÿ›ก๏ธ 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.

const User = 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";

const User = defineModel({
  tableName: "users",
  versioned: true,
  columns: {
    id: { type: DataTypes.Integer, required: true },
    name: { type: DataTypes.String, length: 100 },
  },
});

// --- Using versioning features:

const userRepository = orm.getRepository(User);

// Rollback to a previous version
await userRepository.rollback(1, 3); // roll back user with id=1 to version 3

// Get a snapshot as of a specific date
const userAsOf = await userRepository.asOf(1, new Date("2025-01-01T00:00:00Z"));
console.log(userAsOf);

// View full version history
const history = await userRepository.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";

const User = 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 = new Date();
    },
    beforeUpdate: (entity) => {
      entity.updatedAt = new Date();
    },
    afterCreate: (entity) => {
      console.log(`User created: ${entity.name}`);
    },
  },
});

// Add a hook as a class method
User.prototype.afterUpdate = async function () {
  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

Database & Migration Management

  • Run all pending migrations:

    stabilize-cli migrate
  • Roll back the last migration:

    stabilize-cli migrate:rollback
  • 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

๐Ÿง‘โ€๐Ÿ’ป Querying Data

Basic CRUD with Repositories

import { orm } from "./db";
import { User } from "./models/User";

const userRepository = orm.getRepository(User);

const newUser = await userRepository.create({ email: "lwazicd@icloud.com" });
const foundUser = await userRepository.findOne(newUser.id);
const updatedUser = await userRepository.update(newUser.id, { email: "admin@offbytesecure.com" });
await userRepository.delete(newUser.id);

Advanced Queries with the Query Builder

const activeAdmins = await orm
  .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>;
  join(table: string, condition: string): QueryBuilder<User>;
  orderBy(clause: string): QueryBuilder<User>;
  limit(limit: number): QueryBuilder<User>;
  offset(offset: number): QueryBuilder<User>;
  scope(name: string, ...args: any[]): 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";

const User = 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 >= ?", new Date(Date.now() - days * 24 * 60 * 60 * 1000)),
  },
});

const userRepository = orm.getRepository(User);

// Fetch active users
const activeUsers = await userRepository.scope("active").execute();

// Fetch users created in the last 7 days
const recentUsers = await userRepository.scope("recent", 7).execute();

// Combine scopes with other query operations
const recentActiveUsers = await userRepository
  .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";

const User = 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",
  },
});

const userRepository = orm.getRepository(User);

// Create a user (createdAt and updatedAt set automatically)
const newUser = await userRepository.create({ email: "lwazicd@icloud.com" });
console.log(newUser.createdAt, newUser.updatedAt); // Outputs current timestamp

// Update a user (updatedAt updated automatically)
const updatedUser = await userRepository.update(newUser.id, { email: "admin@offbytesecure.com" });
console.log(updatedUser.updatedAt); // Outputs new timestamp

// Bulk create users
const newUsers = await userRepository.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";

const User = 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 },
  },
});

const userRepository = orm.getRepository(User);
await userRepository.create({ email: "lwazicd@icloud.com" });
await userRepository.delete(1); // Soft delete
await userRepository.recover(1); // Recover

๐ŸŒ Express.js Integration

Stabilize ORM works seamlessly with web frameworks like Express.

import express from "express";
import { orm } from "./db";
import { User } from "./models/User";

const app = express();
app.use(express.json());

const userRepository = orm.getRepository(User);

app.get("/users", async (req, res) => {
  try {
    const users = await userRepository.find().execute();
    res.json(users);
  } catch (err) {
    res.status(500).json({ error: "Failed to fetch users." });
  }
});

app.post("/users", async (req, res) => {
  try {
    const user = await userRepository.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.

๐Ÿ“‘ License

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


Created with โค๏ธ by ElectronSz
File last updated: 2025-10-19 11:12:00 SAST

About

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

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published