gambitorm
v1.1.0
Published
A modern, type-safe ORM for Node.js with full TypeScript support, migrations, relationships, validation, and hooks
Maintainers
Readme
GambitORM
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 gambitormOptional Database Drivers
SQLite Support:
npm install better-sqlite3Note 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 requiredEmailValidator- Valid email formatMinLengthValidator/MaxLengthValidator- String length constraintsMinValidator/MaxValidator- Numeric value constraintsTypeValidator- 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 recordbeforeUpdate/afterUpdate- Before/after updating a recordbeforeDelete/afterDelete- Before/after deleting a recordbeforeValidate/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 gambitormConfiguration
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 migrateRuns all pending migrations.
Rollback Migrations
# Rollback last batch
gambit migrate:rollback
# Rollback all migrations
gambit migrate:rollback --allCheck Migration Status
gambit migrate:statusShows which migrations have been executed and which are pending.
Create Migration
gambit migrate:create create_users_tableCreates 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.jsonAdvanced 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
- API Documentation - Complete API reference
- Usage Examples - Comprehensive usage examples
- Migration Guide - Migrating from other ORMs
- Best Practices - Best practices and guidelines
- Contributing - How to contribute
- Code of Conduct - Community guidelines
- Changelog - Version history
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 databaseQuick 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;
}