npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

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

About

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

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

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

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

Open Software & Tools

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

© 2026 – Pkg Stats / Ryan Hefner

gambitorm

v1.1.0

Published

A modern, type-safe ORM for Node.js with full TypeScript support, migrations, relationships, validation, and hooks

Readme

GambitORM

npm version License: MIT TypeScript Node.js Build Status

A modern, type-safe ORM for Node.js built with TypeScript.

Features

  • 🚀 Type-safe database queries
  • 📦 Lightweight and performant
  • 🔧 Flexible query builder
  • 🎯 Model-based approach
  • 🔄 Migration support
  • 💪 Full TypeScript support
  • 🔗 Relationship support (hasOne, hasMany, belongsTo)
  • ⚡ Eager loading
  • 🔀 Join queries
  • 💼 Transaction support
  • ✅ Model validation with custom validators
  • 🎣 Lifecycle hooks (beforeSave, afterSave, beforeDelete, etc.)
  • 🛠️ CLI tool for migration management
  • 🔍 Advanced query methods (whereIn, whereNull, whereBetween, subqueries, raw SQL)
  • 🍃 MongoDB support with native operations
  • 🕐 Automatic timestamps (created_at, updated_at)
  • 🗑️ Soft deletes support
  • ⚡ Quick helper methods (count, exists, pluck, first, last, increment, decrement, touch, fresh, isDirty, isClean)

Installation

npm install gambitorm

Optional Database Drivers

SQLite Support:

npm install better-sqlite3

Note for Windows users: better-sqlite3 requires native compilation. You'll need:

  • Visual Studio Build Tools with "Desktop development with C++" workload
  • Windows SDK

MongoDB Support: MongoDB support is included by default. The mongodb package is already in dependencies.

If you don't need SQLite support, you can skip it. MySQL, PostgreSQL, and MongoDB will work without it.

Quick Start

import { GambitORM, Model } from 'gambitorm';

// Define your model
class User extends Model {
  static tableName = 'users';
  
  id!: number;
  name!: string;
  email!: string;
}

// Initialize the ORM
const orm = new GambitORM({
  host: 'localhost',
  port: 3306,
  database: 'mydb',
  user: 'user',
  password: 'password',
  dialect: 'mysql',
});

await orm.connect();

// Use your model
const users = await User.findAll();
const user = await User.findById(1);

Relationships

GambitORM supports three types of relationships:

HasOne

class User extends Model {
  static tableName = 'users';
  id!: number;
  name!: string;
}

class Profile extends Model {
  static tableName = 'profiles';
  id!: number;
  user_id!: number;
  bio!: string;
}

// Load a user's profile
const user = await User.findById(1);
const profile = await user.hasOne(Profile, 'user_id').load();

HasMany

class Post extends Model {
  static tableName = 'posts';
  id!: number;
  user_id!: number;
  title!: string;
}

// Load a user's posts
const user = await User.findById(1);
const posts = await user.hasMany(Post, 'user_id').load();

BelongsTo

// Load the author of a post
const post = await Post.findById(1);
const author = await post.belongsTo(User, 'user_id').load();

Join Queries

Use the QueryBuilder for complex join queries:

import { QueryBuilder } from 'gambitorm';

const connection = orm.getConnection();
const query = new QueryBuilder('users', connection)
  .select(['users.*', 'profiles.bio'])
  .leftJoin('profiles', { left: 'users.id', right: 'profiles.user_id' })
  .where('users.active', '=', true)
  .orderBy('users.name', 'ASC')
  .limit(10);

const result = await query.execute();

Eager Loading

Load relationships when fetching models:

// Load users with their profiles (basic support)
const users = await User.findAll({ include: ['profile'] });
const user = await User.findById(1, { include: ['profile', 'posts'] });

Transactions

GambitORM supports database transactions for atomic operations:

Manual Transaction Management

const transaction = await orm.beginTransaction();

try {
  await User.create({ name: 'John', email: '[email protected]' });
  await Post.create({ title: 'My Post', user_id: 1 });
  await transaction.commit();
} catch (error) {
  await transaction.rollback();
  throw error;
}

Automatic Transaction Management (Recommended)

// Automatically commits on success or rolls back on error
await orm.transaction(async (tx) => {
  await User.create({ name: 'John', email: '[email protected]' });
  await Post.create({ title: 'My Post', user_id: 1 });
});

Using Connection Directly

const connection = orm.getConnection();

await connection.beginTransaction();
try {
  await connection.query('UPDATE users SET balance = balance - 100 WHERE id = 1');
  await connection.query('UPDATE users SET balance = balance + 100 WHERE id = 2');
  await connection.commit();
} catch (error) {
  await connection.rollback();
  throw error;
}

Validation

GambitORM supports model validation before save/update operations:

Basic Validation

import { Model, RequiredValidator, EmailValidator, MinLengthValidator } from 'gambitorm';

class User extends Model {
  static tableName = 'users';
  
  // Define validation rules
  static validationRules = {
    name: [
      new RequiredValidator('Name is required'),
      new MinLengthValidator(3, 'Name must be at least 3 characters'),
    ],
    email: [
      new RequiredValidator(),
      new EmailValidator('Email must be valid'),
    ],
  };

  id!: number;
  name!: string;
  email!: string;
}

// Validation runs automatically on create, save, and update
try {
  await User.create({ name: 'Jo', email: 'invalid' });
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('Validation errors:', error.errors);
  }
}

Built-in Validators

  • RequiredValidator - Field is required
  • EmailValidator - Valid email format
  • MinLengthValidator / MaxLengthValidator - String length constraints
  • MinValidator / MaxValidator - Numeric value constraints
  • TypeValidator - Type checking (string, number, boolean, date, array, object)
  • CustomValidator - Custom validation function (supports async)

Custom Validators

import { CustomValidator } from 'gambitorm';

class Product extends Model {
  static tableName = 'products';
  
  static validationRules = {
    sku: [
      new RequiredValidator(),
      new CustomValidator(
        (value) => /^[A-Z0-9-]+$/.test(value),
        'SKU must contain only uppercase letters, numbers, and hyphens'
      ),
    ],
    price: [
      new CustomValidator(
        async (value) => {
          // Async validation example
          const isValid = await checkPriceFromAPI(value);
          return isValid;
        },
        'Price validation failed'
      ),
    ],
  };
}

Skip Validation

// Skip validation when needed
await user.save({ skipValidation: true });
await User.create(data, { skipValidation: true });
await user.update(data, { skipValidation: true });

Manual Validation

const user = new User();
user.name = 'John';
user.email = '[email protected]';

try {
  await user.validate();
  console.log('Validation passed');
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('Errors:', error.errors);
    console.error('Field errors:', error.getFieldErrors('name'));
  }
}

Lifecycle Hooks

GambitORM supports lifecycle hooks for models to execute code at specific points:

Available Hooks

  • beforeSave / afterSave - Before/after save (create or update)
  • beforeCreate / afterCreate - Before/after creating a new record
  • beforeUpdate / afterUpdate - Before/after updating a record
  • beforeDelete / afterDelete - Before/after deleting a record
  • beforeValidate / afterValidate - Before/after validation

Basic Usage

import { Model, HookEvent } from 'gambitorm';

class User extends Model {
  static tableName = 'users';
  id!: number;
  name!: string;
  email!: string;
  created_at?: Date;
  updated_at?: Date;
}

// Register hooks
User.hook(HookEvent.BEFORE_SAVE, async (user) => {
  user.updated_at = new Date();
  if (!user.id) {
    user.created_at = new Date();
  }
});

User.hook(HookEvent.AFTER_CREATE, async (user) => {
  console.log(`User created: ${user.name}`);
  // Send welcome email, etc.
});

User.hook(HookEvent.BEFORE_DELETE, async (user) => {
  if (user.email === '[email protected]') {
    throw new Error('Cannot delete admin user');
  }
});

// Hooks are automatically executed
const user = await User.create({ name: 'John', email: '[email protected]' });
await user.save();
await user.delete();

Hook Priority

Hooks can have priorities (lower numbers run first):

User.hook(HookEvent.BEFORE_SAVE, async (user) => {
  console.log('Runs first');
}, 10);

User.hook(HookEvent.BEFORE_SAVE, async (user) => {
  console.log('Runs second');
}, 50);

User.hook(HookEvent.BEFORE_SAVE, async (user) => {
  console.log('Runs last (default priority 100)');
});

Managing Hooks

const myHook = async (user: User) => {
  console.log('My hook');
};

// Register
User.hook(HookEvent.BEFORE_SAVE, myHook);

// Unregister
User.unhook(HookEvent.BEFORE_SAVE, myHook);

// Clear all hooks for an event
User.clearHooks(HookEvent.BEFORE_SAVE);

CLI Tool

GambitORM includes a CLI tool for managing migrations:

Installation

After installing GambitORM, the gambit command is available:

npm install -g gambitorm
# or use npx
npx gambitorm

Configuration

Create a .gambitorm.json file in your project root:

{
  "host": "localhost",
  "port": 3306,
  "database": "mydb",
  "user": "root",
  "password": "password",
  "dialect": "mysql"
}

Commands

Run Migrations

gambit migrate

Runs all pending migrations.

Rollback Migrations

# Rollback last batch
gambit migrate:rollback

# Rollback all migrations
gambit migrate:rollback --all

Check Migration Status

gambit migrate:status

Shows which migrations have been executed and which are pending.

Create Migration

gambit migrate:create create_users_table

Creates a new migration file in the migrations directory:

import { Migration } from 'gambitorm';

export class CreateUsersTable extends Migration {
  async up(): Promise<void> {
    await this.schema('users')
      .id()
      .string('name')
      .string('email')
      .timestamp('created_at')
      .create();
  }

  async down(): Promise<void> {
    await this.schema('users').drop();
  }

  getName(): string {
    return 'create_users_table';
  }
}

Custom Config Path

gambit migrate --config ./config/database.json

Advanced QueryBuilder Features

Additional WHERE Methods

import { QueryBuilder } from 'gambitorm';

const query = new QueryBuilder('users', connection);

// WHERE IN / NOT IN
query.whereIn('id', [1, 2, 3]);
query.whereNotIn('status', ['deleted', 'banned']);

// WHERE NULL / NOT NULL
query.whereNull('deleted_at');
query.whereNotNull('email');

// WHERE BETWEEN / NOT BETWEEN
query.whereBetween('age', 18, 65);
query.whereNotBetween('salary', 0, 50000);

// WHERE LIKE / NOT LIKE
query.whereLike('email', '%@gmail.com');
query.whereNotLike('name', '%test%');

// OR WHERE
query.where('status', '=', 'active');
query.orWhere('status', '=', 'pending');

// Raw WHERE
query.whereRaw('(age > ? OR salary > ?) AND status = ?', [18, 50000, 'active']);

Subqueries

// Create a subquery
const subquery = QueryBuilder.subquery('orders', connection);
subquery.select(['user_id']).where('total', '>', 1000);

// Use in WHERE clause
const query = new QueryBuilder('users', connection);
query.whereSubquery('id', 'IN', subquery);

// Results in: SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > ?)

Aggregate Functions

// Count
query.count('*', 'total_users');
query.count('id', 'user_count');

// Sum, Average, Max, Min
query.sum('total', 'total_revenue');
query.avg('price', 'avg_price');
query.max('amount', 'max_amount');
query.min('amount', 'min_amount');

Raw SQL Execution

// Using ORM
const result = await orm.raw('SELECT * FROM users WHERE id = ?', [1]);

// Using QueryBuilder static method
const result = await QueryBuilder.raw(connection, 'SELECT * FROM users WHERE id = ?', [1]);

Documentation

Contributing

Contributions are welcome! Please read our Contributing Guide and Code of Conduct before submitting pull requests.

License

MIT

Examples

Model with Relationships

class User extends Model {
  static tableName = 'users';
  id!: number;
  name!: string;
  
  profile() {
    return this.hasOne(Profile, { foreignKey: 'user_id' });
  }
  
  posts() {
    return this.hasMany(Post, { foreignKey: 'user_id' });
  }
}

class Post extends Model {
  static tableName = 'posts';
  id!: number;
  user_id!: number;
  title!: string;
  
  user() {
    return this.belongsTo(User, { foreignKey: 'user_id' });
  }
}

// Usage
const user = await User.findById(1, { include: ['profile', 'posts'] });

Advanced Query

const query = new QueryBuilder('users', connection);
query
  .select(['users.*', 'COUNT(orders.id) as order_count'])
  .leftJoin('orders', { left: 'users.id', right: 'orders.user_id' })
  .where('users.status', '=', 'active')
  .whereIn('users.role', ['customer', 'premium'])
  .whereNotNull('users.email')
  .groupBy('users.id')
  .having('COUNT(orders.id)', '>', 0)
  .orderBy('order_count', 'DESC')
  .limit(10);

const results = await query.execute();

Migration Example

import { Migration } from 'gambitorm';

export class CreateUsersTable extends Migration {
  async up(): Promise<void> {
    await this.schema('users')
      .id()
      .string('name')
      .string('email', 255)
      .unique()
      .notNull()
      .timestamp('created_at')
      .timestamp('updated_at')
      .create();
  }

  async down(): Promise<void> {
    await this.schema('users').drop();
  }

  getName(): string {
    return 'create_users_table';
  }
}

Validation Example

class User extends Model {
  static tableName = 'users';
  static validationRules = {
    name: [
      new RequiredValidator('Name is required'),
      new MinLengthValidator(3),
    ],
    email: [
      new RequiredValidator(),
      new EmailValidator('Invalid email'),
    ],
    age: [
      new TypeValidator('number'),
      new MinValidator(18),
      new MaxValidator(120),
    ],
  };
}

Automatic Timestamps Example

Soft Deletes Example

class User extends Model {
  static tableName = 'users';
  static timestamps = true; // Enable automatic timestamps
  static createdAt = 'created_at'; // Optional: customize field name
  static updatedAt = 'updated_at'; // Optional: customize field name
  
  id!: number;
  name!: string;
  created_at?: Date;
  updated_at?: Date;
}

// Create - automatically sets created_at and updated_at
const user = await User.create({ name: 'John' });
console.log(user.created_at); // Current timestamp
console.log(user.updated_at); // Current timestamp

// Save (new) - automatically sets both timestamps
const newUser = new User();
newUser.name = 'Jane';
await newUser.save(); // Sets created_at and updated_at

// Save (existing) - automatically updates updated_at
user.name = 'John Updated';
await user.save(); // Updates updated_at, keeps created_at unchanged

// Update - automatically updates updated_at
await user.update({ name: 'John Doe' }); // Updates updated_at
  static softDeletes = true; // Enable soft deletes
  static deletedAt = 'deleted_at'; // Optional: customize field name
  
  id!: number;
  name!: string;
  email!: string;
  deleted_at?: Date | null;
}

// Soft delete (sets deleted_at instead of removing)
const user = await User.findById(1);
await user.delete(); // Sets deleted_at to current timestamp

// Find all (excludes soft-deleted by default)
const users = await User.findAll(); // Only non-deleted users

// Include soft-deleted records
const allUsers = await User.withTrashed().findAll();

// Only soft-deleted records
const deletedUsers = await User.onlyTrashed().findAll();

// Restore a soft-deleted record
await user.restore(); // Sets deleted_at to null

// Permanently delete (force delete)
await user.forceDelete(); // Actually removes from database

Quick Helper Methods Example

class User extends Model {
  static tableName = 'users';
  id!: number;
  name!: string;
  views!: number;
}

// Count records
const totalUsers = await User.count();
const activeUsers = await User.count({ status: 'active' });

// Check existence
const exists = await User.exists({ email: '[email protected]' });

// Pluck column values
const names = await User.pluck('name');
const topNames = await User.pluck('name', { limit: 10 });

// Get first/last record
const firstUser = await User.first();
const lastUser = await User.last();

// Increment/Decrement
const user = await User.findById(1);
await user.increment('views', 5); // Add 5
await user.decrement('views', 2);  // Subtract 2

// Touch timestamp
await user.touch(); // Updates updated_at

// Reload from database
await user.fresh(); // Reloads all attributes

// Check if modified
if (user.isDirty('name')) {
  console.log('Name has been changed');
}

if (user.isClean()) {
  console.log('No changes made');
}

Transaction Example

// Automatic transaction
await orm.transaction(async (tx) => {
  const user = await User.create({ name: 'John', email: '[email protected]' });
  await Profile.create({ user_id: user.id, bio: 'Developer' });
  // Automatically commits or rolls back on error
});

// Manual transaction
const tx = await orm.beginTransaction();
try {
  await User.create({ name: 'John' });
  await Profile.create({ user_id: 1 });
  await tx.commit();
} catch (error) {
  await tx.rollback();
  throw error;
}

Support