bun-sqlite-orm
v1.5.1
Published
A lightweight TypeScript ORM for Bun runtime with Bun SQLite, featuring Active Record pattern and decorator-based entities
Maintainers
Readme
BunSQLiteORM
A lightweight, type-safe TypeScript ORM designed specifically for Bun runtime with native SQLite integration. Features Active Record pattern, decorator-based entities, and comprehensive validation.
✨ Key Features
- 🚀 Built for Bun - Leverages Bun's native SQLite performance and capabilities
- 🎯 TypeScript First - Complete type safety with decorator-based entity definitions and type-safe updates
- 🔄 Active Record Pattern - Intuitive entity lifecycle management with familiar Rails-like syntax
- ✅ Built-in Validation - Seamless integration with class-validator decorators
- 🛠️ Auto Migrations - Automatic table creation from entity metadata, zero-config setup
- 🔍 Rich Querying - Type-safe query methods with find, count, exists, and bulk operations
- ⚡ Statement Caching - Automatic prepared statement caching for 30-50% performance improvement
- 🔀 Transaction Support - Comprehensive transaction support with ACID compliance, savepoints, and isolation levels
- 📈 Database Indexing - Comprehensive index support with simple, composite, and unique indexes
- 📝 Flexible Primary Keys - Support for auto-increment, UUID, custom, and composite primary key strategies
- 🔒 Enhanced Error System - Comprehensive error handling with base class and entity context
- ✅ Validation & Safety - Automatic entity validation with detailed error reporting
- 📊 Entity State Tracking - Built-in change tracking and dirty state management
- 🎨 Decorator Driven - Clean, declarative entity definitions using TypeScript decorators
📦 Installation
bun add bun-sqlite-ormPrerequisites
- Bun >= 1.1.21
- TypeScript >= 5.0.0
🚀 Quick Start
1. Define Your Entities
Create type-safe entity classes with decorators:
import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, Index } from 'bun-sqlite-orm';
import { IsNotEmpty, IsEmail, MinLength } from 'class-validator';
@Entity('users')
@Index('idx_name_age', ['name', 'age']) // Composite index for common queries
export class User extends BaseEntity {
@PrimaryGeneratedColumn('int')
id!: number;
@Column({ type: 'text' })
@IsNotEmpty()
@MinLength(2)
name!: string;
@Column({ type: 'text', unique: true, index: true }) // Indexed for fast lookups
@IsEmail()
email!: string;
@Column({ type: 'integer', nullable: true })
age?: number;
@Column({ sqlDefault: 'CURRENT_TIMESTAMP' })
createdAt!: Date;
}2. Initialize DataSource
Set up your database connection and entities:
import { DataSource } from 'bun-sqlite-orm';
import { User } from './entities/User';
const dataSource = new DataSource({
database: './database.db', // File path or ':memory:' for in-memory
entities: [User],
// Optional: Add custom logger
// logger: new ConsoleDbLogger()
});
// Initialize and create tables
await dataSource.initialize();
await dataSource.runMigrations(); // Creates tables automatically from entity metadata3. Use Active Record Methods
Interact with your data using intuitive Active Record methods:
// Create and save entities
const user = User.build({
name: 'John Doe',
email: '[email protected]',
age: 30
});
await user.save(); // Validates and inserts
// Create in one step with validation
const user2 = await User.create({
name: 'Jane Smith',
email: '[email protected]'
});
// Query methods
const users = await User.find({ age: 30 }); // Find all matching
const user = await User.get(1); // Find by primary key
const firstUser = await User.findFirst({ name: 'John' }); // Find first match
// Composite primary key queries
const userRole = await UserRole.get({ userId: 1, roleId: 2 }); // Find by composite key
// Aggregation methods
const totalUsers = await User.count(); // Count all
const adultCount = await User.count({ age: { gte: 18 } }); // Count with conditions
const userExists = await User.exists({ email: '[email protected]' });
// Update operations
user.age = 31;
await user.save(); // Updates only changed fields
// Type-safe partial updates with Partial<T>
await user.update({ name: 'Johnny Doe', age: 32 }); // ✅ Full IntelliSense support
// await user.update({ invalidField: 'value' }); // ❌ TypeScript error - invalid property
// await user.update({ age: 'not-a-number' }); // ❌ TypeScript error - wrong type
// Delete operations
await user.remove(); // Delete single entity
// Bulk operations
await User.updateAll({ status: 'active' }, { age: { gte: 18 } });
await User.deleteAll({ status: 'inactive' });
// Transaction support for atomic operations
const result = await dataSource.transaction(async (tx) => {
const user = await User.create({
name: 'Alice',
email: '[email protected]'
});
const profile = await Profile.create({
userId: user.id,
bio: 'Software Engineer'
});
return { user, profile }; // Both created atomically
});🎯 Enhanced Type Safety
BunSQLiteORM provides comprehensive TypeScript type safety throughout the API, with recent improvements to the update() method for better developer experience.
Type-Safe Updates with Partial
The instance update() method now uses Partial<T> for complete type safety:
@Entity('users')
class User extends BaseEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column()
name!: string;
@Column()
email!: string;
@Column()
age!: number;
@Column()
preferences?: {
theme: 'light' | 'dark';
notifications: boolean;
};
@Column()
tags?: string[];
}
const user = await User.get(1);
// ✅ Type-safe updates with IntelliSense
await user.update({
name: 'Updated Name', // ✅ Autocompleted, type-checked
email: '[email protected]', // ✅ Valid string property
age: 30, // ✅ Valid number property
});
// ✅ Partial updates (any subset of properties)
await user.update({
name: 'Just the name' // ✅ Only updating one field
});
// ✅ Complex object properties supported
await user.update({
preferences: {
theme: 'dark',
notifications: true
},
tags: ['admin', 'verified']
});
// ❌ TypeScript errors prevent runtime issues
// await user.update({
// invalidProperty: 'value' // ❌ Property doesn't exist
// });
// await user.update({
// name: 123, // ❌ Wrong type (should be string)
// age: 'not-a-number' // ❌ Wrong type (should be number)
// });Benefits of Enhanced Type Safety
- IntelliSense Support: Full autocomplete for entity properties
- Compile-time Validation: Catch typos and type errors before runtime
- Complex Types: Support for objects, arrays, and nested structures
- Consistent API: Matches the type safety of
create()method - Developer Experience: Faster development with fewer bugs
API Consistency
All entity methods now provide consistent type safety:
// All methods support the same level of type safety
const user1 = await User.create({ // ✅ Partial<User>
name: 'Alice',
email: '[email protected]'
});
await user1.update({ // ✅ Partial<User> (improved!)
age: 25
});
// Static methods maintain their existing signatures
await User.updateAll({ // Record<string, SQLQueryBindings>
status: 'active'
}, { age: { gte: 18 } });🔀 Transaction Support
BunSQLiteORM provides comprehensive transaction support for atomic database operations with automatic rollback on errors, ensuring data consistency and integrity.
Basic Transactions
// Simple transaction with automatic commit/rollback
const result = await dataSource.transaction(async (tx) => {
const user = await User.create({
name: 'John Doe',
email: '[email protected]'
});
const profile = await Profile.create({
userId: user.id,
bio: 'Software Engineer',
avatar: 'avatar.jpg'
});
// If any operation fails, entire transaction is rolled back
if (someBusinessRule(user, profile)) {
throw new Error('Business logic violation'); // Triggers rollback
}
return { user, profile }; // Success - transaction commits
});Advanced Transaction Patterns
// Parallel operations within transaction
const [users, posts] = await dataSource.transactionParallel([
async (tx) => Promise.all([
User.create({ name: 'Alice' }),
User.create({ name: 'Bob' })
]),
async (tx) => Promise.all([
Post.create({ title: 'Post 1', content: '...' }),
Post.create({ title: 'Post 2', content: '...' })
])
]);
// Sequential operations with result chaining
const finalResult = await dataSource.transactionSequential([
async (tx) => User.create({ name: 'John' }),
async (tx, user) => Post.create({
title: 'Hello',
userId: user.id
}),
async (tx, post) => Comment.create({
text: 'Great post!',
postId: post.id
})
]);Savepoints (Nested Transactions)
await dataSource.transaction(async (tx) => {
const user = await User.create({ name: 'John' });
// Create savepoint for risky operations
const savepoint = await tx.savepoint('user_profile');
try {
await Profile.create({ userId: user.id, bio: 'Complex bio...' });
await tx.releaseSavepoint(savepoint); // Success
} catch (error) {
await tx.rollbackToSavepoint(savepoint); // Rollback to savepoint
// User still exists, but profile creation was undone
}
return user;
});Transaction Isolation Levels
// Use IMMEDIATE isolation for critical operations
await dataSource.transaction(async (tx) => {
// High-priority operations that need immediate locks
const account = await Account.get(accountId);
account.balance -= withdrawAmount;
await account.save();
}, { isolation: 'IMMEDIATE' });
// Available isolation levels:
// - 'DEFERRED' (default): Transaction starts when first read/write occurs
// - 'IMMEDIATE': Transaction starts immediately, blocks other writers
// - 'EXCLUSIVE': Transaction starts immediately, blocks all other connectionsManual Transaction Control
const tx = dataSource.createTransaction({ isolation: 'IMMEDIATE' });
try {
await tx.begin();
const user = await User.create({ name: 'John' });
const profile = await Profile.create({ userId: user.id });
await tx.commit();
return { user, profile };
} catch (error) {
await tx.rollback();
throw error;
}🎨 Decorators Reference
Entity Decorators
| Decorator | Description | Example |
|-----------|-------------|---------|
| @Entity(tableName?) | Mark class as database entity | @Entity('users') |
| @PrimaryColumn() | Define primary key column (supports composite keys) | @PrimaryColumn() id!: string; |
| @PrimaryGeneratedColumn(strategy) | Auto-generated primary key | @PrimaryGeneratedColumn('uuid') |
| @Column(options) | Define regular column | @Column({ type: 'text', nullable: true }) |
| @Index() | Create index on property | @Index() @Column() email!: string; |
| @Index(name, columns, options) | Create composite index on class | @Index('idx_name', ['firstName', 'lastName']) |
Column Options
@Column({
type: 'text' | 'integer' | 'real' | 'blob' | 'json', // SQLite data types
nullable?: boolean, // Allow NULL values (default: false)
unique?: boolean, // Add unique constraint (default: false)
default?: any | (() => any), // JavaScript default value or function
sqlDefault?: string | number | boolean | null, // SQL default value or expression
index?: boolean | string, // Create index: true for auto-named, string for custom name
transformer?: ColumnTransformer // Custom data transformation for save/load operations
})Primary Key Strategies
// Auto-incrementing integer
@PrimaryGeneratedColumn('int')
id!: number;
// UUID v4
@PrimaryGeneratedColumn('uuid')
id!: string;
// Manual primary key
@PrimaryColumn()
customId!: string;
// Composite primary keys
@Entity('user_roles')
export class UserRole extends BaseEntity {
@PrimaryColumn()
userId!: number;
@PrimaryColumn()
roleId!: number;
@Column()
assignedAt!: string;
}Database Indexing
BunSQLiteORM provides comprehensive indexing support to optimize query performance. Indexes are automatically created during table migrations.
Column-Level Indexing
Add indexes directly to column definitions:
@Entity('users')
export class User extends BaseEntity {
@PrimaryGeneratedColumn('int')
id!: number;
// Auto-named index: idx_users_email
@Column({ type: 'text', index: true })
email!: string;
// Custom-named index
@Column({ type: 'text', index: 'idx_custom_username' })
username!: string;
@Column({ type: 'text' })
firstName!: string;
@Column({ type: 'text' })
lastName!: string;
}Property-Level Indexing
Use the @Index() decorator on properties:
@Entity('users')
export class User extends BaseEntity {
@PrimaryGeneratedColumn('int')
id!: number;
// Auto-named index: idx_users_email
@Index()
@Column({ type: 'text' })
email!: string;
// Custom-named index
@Index('idx_user_phone')
@Column({ type: 'text' })
phone!: string;
}Composite Indexes
Create indexes spanning multiple columns using class-level decorators:
@Entity('posts')
@Index('idx_author_date', ['authorId', 'createdAt']) // Regular composite index
@Index('idx_unique_slug_status', ['slug', 'status'], { unique: true }) // Unique composite index
export class Post extends BaseEntity {
@PrimaryGeneratedColumn('int')
id!: number;
@Column({ type: 'integer' })
authorId!: number;
@Column({ type: 'text' })
slug!: string;
@Column({ type: 'text' })
status!: string;
@Column({ sqlDefault: 'CURRENT_TIMESTAMP' })
createdAt!: Date;
}Index Options
// Index with options
@Index('idx_unique_email', ['email'], { unique: true })
// Available options:
interface IndexOptions {
unique?: boolean; // Create unique index (default: false)
}Generated Index Names
When not providing custom names, indexes are auto-named using the pattern:
- Column-level:
idx_{tableName}_{columnName} - Property-level:
idx_{tableName}_{propertyName}
@Entity('user_profiles')
export class UserProfile extends BaseEntity {
@Column({ index: true }) // Creates: idx_user_profiles_email
email!: string;
@Index() // Creates: idx_user_profiles_phone
@Column()
phone!: string;
}✅ Validation Integration
BunSQLiteORM integrates seamlessly with class-validator:
import { IsEmail, MinLength, IsOptional, IsInt, Min, Max } from 'class-validator';
@Entity('users')
export class User extends BaseEntity {
@Column({ type: 'text' })
@MinLength(2, { message: 'Name must be at least 2 characters' })
name!: string;
@Column({ type: 'text', unique: true })
@IsEmail({}, { message: 'Must be a valid email address' })
email!: string;
@Column({ type: 'integer', nullable: true })
@IsOptional()
@IsInt()
@Min(0)
@Max(150)
age?: number;
@Column({ type: 'text', nullable: true })
@IsOptional()
@MinLength(10)
bio?: string;
}Validation behavior:
- Automatically runs on
save()andcreate()methods - Throws detailed
ValidationErroron validation failure - Preserves entity state on validation errors (won't save invalid data)
🔑 Composite Primary Keys
BunSQLiteORM provides complete support for composite primary keys, ideal for junction tables, many-to-many relationships, and multi-dimensional data models.
Defining Composite Primary Keys
Use multiple @PrimaryColumn() decorators to create composite primary keys:
@Entity('user_roles')
export class UserRole extends BaseEntity {
@PrimaryColumn()
userId!: number;
@PrimaryColumn()
roleId!: number;
@Column()
assignedBy!: string;
@Column({ sqlDefault: 'CURRENT_TIMESTAMP' })
assignedAt!: Date;
}
@Entity('order_items')
export class OrderItem extends BaseEntity {
@PrimaryColumn()
orderId!: string;
@PrimaryColumn()
productSku!: string;
@Column()
quantity!: number;
@Column()
unitPrice!: number;
}Working with Composite Keys
All standard Active Record methods work seamlessly with composite primary keys:
// Create entities with composite keys
const userRole = UserRole.build({
userId: 1,
roleId: 2,
assignedBy: 'admin',
assignedAt: new Date()
});
await userRole.save();
// Find by composite primary key
const role = await UserRole.get({ userId: 1, roleId: 2 });
// Update and reload work automatically
role.assignedBy = 'manager';
await role.save();
await role.reload(); // Refreshes from database
// Remove by composite key
await role.remove();
// Query operations with composite key conditions
const userRoles = await UserRole.find({ userId: 1 });
const exists = await UserRole.exists({ userId: 1, roleId: 2 });
const count = await UserRole.count({ userId: 1 });
// Bulk operations
await UserRole.deleteAll({ userId: 1 });
await UserRole.updateAll({ assignedBy: 'system' }, { userId: 1 });Composite Key Features
- Type Safety: Full TypeScript support with compile-time validation
- Automatic SQL Generation: Generates proper
PRIMARY KEY (col1, col2)constraints - Backward Compatibility: Single primary key entities work exactly as before
- Flexible Object Notation: Single keys can use either
Entity.get(1)orEntity.get({ id: 1 }) - Validation: Comprehensive error messages for missing or invalid key properties
- Performance: Optimized queries with proper primary key indexing
SQL Output
BunSQLiteORM generates standards-compliant SQLite syntax for composite primary keys:
-- Generated table creation SQL
CREATE TABLE IF NOT EXISTS "user_roles" (
"userId" INTEGER,
"roleId" INTEGER,
"assignedBy" TEXT NOT NULL,
"assignedAt" TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("userId", "roleId")
);Error Handling
Composite primary keys include comprehensive validation and clear error messages:
try {
// Missing required key property
await UserRole.get({ userId: 1 }); // Missing roleId
} catch (error) {
console.log(error.message);
// "Missing primary key property 'roleId' for entity UserRole"
}
try {
// Invalid key format for composite key entity
await UserRole.get(123); // Should be an object
} catch (error) {
console.log(error.message);
// "Entity UserRole has 2 primary keys. Expected object with keys: userId, roleId"
}⚡ Statement Caching
BunSQLiteORM includes automatic prepared statement caching that provides 30-50% performance improvement for repeated queries. Statement caching works transparently - there's no configuration required and no changes needed to your existing code.
How It Works
The StatementCache automatically:
- Caches prepared statements by SQL string for fast reuse
- Tracks performance metrics including hit rates and cache statistics
- Manages resource cleanup to prevent memory leaks
- Provides test mode support for unit test compatibility
Performance Benefits
// These repeated queries will benefit from statement caching:
const users = await User.find({ age: 25 }); // Cache MISS - creates statement
const users2 = await User.find({ age: 25 }); // Cache HIT - reuses statement
const users3 = await User.find({ age: 25 }); // Cache HIT - reuses statement
const count = await User.count({ status: 'active' }); // Cache MISS - new pattern
const count2 = await User.count({ status: 'active' }); // Cache HIT - reuses statement
// Performance improvements of 30-50% for repeated query patternsCache Statistics
Access cache performance metrics for monitoring:
import { StatementCache } from 'bun-sqlite-orm';
// Get cache statistics
const stats = StatementCache.getStats();
console.log(stats);
// Output: {
// size: 5, // Number of cached statements
// hitCount: 23, // Number of cache hits
// missCount: 5, // Number of cache misses
// hitRate: 0.82, // Hit rate (82%)
// enabled: true // Cache enabled status
// }Cache Management
While caching is automatic, you can control it when needed:
// Disable caching (for testing or debugging)
StatementCache.setEnabled(false);
// Re-enable caching
StatementCache.setEnabled(true);
// Invalidate cache entries by pattern (useful for schema changes)
StatementCache.invalidate(/user_table/); // Removes statements containing "user_table"
// Clear entire cache
StatementCache.cleanup();
// Reset statistics
StatementCache.resetStats();Test Mode
For unit testing with mocks, StatementCache provides a test mode that bypasses caching:
// In your test setup
StatementCache.setTestMode(true); // Enables mock compatibility
// ... run tests with mocked database
StatementCache.setTestMode(false); // Restore normal cachingFeatures
- Zero Configuration: Works automatically with no setup required
- Transparent Operation: No changes needed to existing code
- Memory Safe: Automatic cleanup prevents resource leaks
- Statistics Monitoring: Built-in performance tracking
- Test Friendly: Special mode for unit test compatibility
- Pattern-based Invalidation: Targeted cache clearing for schema changes
📊 Entity State Tracking
Track entity changes and state with built-in methods:
const user = User.build({ name: 'John', email: '[email protected]' });
// Check entity state
console.log(user.isNew()); // true (not yet saved)
console.log(user.isChanged()); // false (no changes since creation)
// Make changes
user.name = 'Johnny';
user.age = 25;
// Track changes
console.log(user.isChanged()); // true (has unsaved changes)
console.log(user.getChanges());
// Output: {
// name: { from: 'John', to: 'Johnny' },
// age: { from: undefined, to: 25 }
// }
// Save changes
await user.save();
console.log(user.isNew()); // false (now persisted)
console.log(user.isChanged()); // false (changes saved)
// Clean JSON serialization (excludes internal ORM properties)
console.log(user.toJSON());
// Output: { id: 1, name: 'Johnny', email: '[email protected]', age: 25, createdAt: '...' }
// Works seamlessly with JSON.stringify() for API responses
const apiResponse = { user: user.toJSON(), timestamp: new Date() };
console.log(JSON.stringify(apiResponse)); // Clean output without _isNew, _originalValues🔧 Advanced Usage
Custom Data Types and Defaults
@Entity('posts')
export class Post extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'text' })
title!: string;
@Column({ type: 'text' })
content!: string;
@Column({ type: 'text', default: 'draft' })
status!: string;
@Column({ type: 'integer', default: 0 })
viewCount!: number;
// SQL default - handled by SQLite
@Column({ sqlDefault: 'CURRENT_TIMESTAMP' })
createdAt!: Date;
// JavaScript default - handled by application
@Column({ default: () => new Date() })
updatedAt!: Date;
@Column({ type: 'text', default: () => JSON.stringify([]) })
tags!: string; // Store JSON as text
}Default Value Options
SQL Defaults (sqlDefault): Handled by SQLite in the database with enhanced expression detection
// SQL expressions (case-insensitive detection)
@Column({ sqlDefault: 'CURRENT_TIMESTAMP' })
createdAt!: Date;
@Column({ sqlDefault: 'current_time' }) // lowercase works
timeField!: string;
@Column({ sqlDefault: 'Current_Date' }) // mixed case works
dateField!: string;
// SQLite functions
@Column({ sqlDefault: 'RANDOM()' })
randomValue!: number;
@Column({ sqlDefault: 'DEFAULT' })
defaultValue!: string;
// Numeric values (no quotes needed)
@Column({ sqlDefault: 0 })
repos!: number;
@Column({ sqlDefault: 3.14 })
pi!: number;
@Column({ sqlDefault: -1.5 })
negativeValue!: number;
// Boolean values (stored as 1/0 in SQLite)
@Column({ sqlDefault: true })
isActive!: boolean;
@Column({ sqlDefault: false })
isDeleted!: boolean;
// Null values
@Column({ nullable: true, sqlDefault: null })
optionalField?: string;
// String literals (quotes added automatically)
@Column({ sqlDefault: 'active' })
status!: string;
@Column({ sqlDefault: 'default_value' })
name!: string;Enhanced SQL Expression Detection:
- ✅ Case-insensitive SQL functions:
CURRENT_TIMESTAMP,current_time,Current_Date - ✅ SQLite functions:
RANDOM(),ABS(),COALESCE(),DEFAULT - ✅ Smart detection: Distinguishes between SQL expressions and string literals
- ✅ Type support:
string | number | boolean | nullfor maximum flexibility - ✅ Automatic quoting: String literals are automatically quoted, SQL expressions are not
JavaScript Defaults (default): Handled by the application
@Column({ default: () => new Date() })
updatedAt!: Date;
@Column({ default: 'pending' })
status!: string;
@Column({ default: () => Math.random() })
randomValue!: number;JSON Columns and Data Transformers
BunSQLiteORM provides powerful support for JSON data storage and custom data transformations, enabling seamless handling of complex data types while maintaining type safety.
JSON Column Type
Use the json column type for automatic JSON serialization and deserialization:
@Entity('user_profiles')
export class UserProfile extends BaseEntity {
@PrimaryGeneratedColumn('int')
id!: number;
// Explicit JSON column type
@Column({ type: 'json' })
preferences!: {
theme: 'light' | 'dark';
notifications: boolean;
language: string;
};
// Auto-inferred JSON type for objects and arrays
@Column()
metadata!: { tags: string[]; created: Date; settings: Record<string, unknown> };
@Column()
favoriteColors!: string[];
// Nullable JSON column
@Column({ type: 'json', nullable: true })
customData?: { [key: string]: unknown };
}
// Usage examples
const profile = await UserProfile.create({
preferences: {
theme: 'dark',
notifications: true,
language: 'en'
},
metadata: {
tags: ['premium', 'verified'],
created: new Date(),
settings: { autoSave: true, maxFiles: 100 }
},
favoriteColors: ['blue', 'green', 'purple'],
customData: { plan: 'premium', level: 5 }
});
// JSON data is automatically serialized/deserialized
console.log(profile.preferences.theme); // 'dark'
console.log(profile.favoriteColors.length); // 3Type Inference for JSON Columns
TypeScript Object and Array types are automatically inferred as JSON columns:
@Entity('documents')
export class Document extends BaseEntity {
@PrimaryGeneratedColumn('int')
id!: number;
// These automatically become JSON columns (type: 'json')
@Column()
content!: { title: string; body: string; sections: Array<{ name: string; text: string }> };
@Column()
tags!: string[];
@Column()
authors!: Array<{ name: string; email: string; role: string }>;
// Regular text column for comparison
@Column({ type: 'text' })
status!: string;
}Custom Data Transformers
Implement the ColumnTransformer interface for advanced data conversion:
import { ColumnTransformer } from 'bun-sqlite-orm';
// Date to ISO string transformer
const dateStringTransformer: ColumnTransformer<Date, string> = {
to: (value: Date) => value.toISOString(),
from: (value: string) => new Date(value)
};
// Encrypt/decrypt transformer
const encryptionTransformer: ColumnTransformer<string, string> = {
to: (value: string) => Buffer.from(value, 'utf-8').toString('base64'),
from: (value: string) => Buffer.from(value, 'base64').toString('utf-8')
};
// Array to comma-separated string transformer
const arrayTransformer: ColumnTransformer<string[], string> = {
to: (value: string[]) => value.join(','),
from: (value: string) => value.split(',')
};
// Custom object serialization
const customObjectTransformer: ColumnTransformer<UserSettings, string> = {
to: (value: UserSettings) => `${value.theme}:${value.fontSize}:${value.autoSave}`,
from: (value: string) => {
const [theme, fontSize, autoSave] = value.split(':');
return { theme, fontSize: Number(fontSize), autoSave: autoSave === 'true' };
}
};
interface UserSettings {
theme: string;
fontSize: number;
autoSave: boolean;
}Using Transformers in Entities
Apply transformers to columns for custom data handling:
@Entity('user_data')
export class UserData extends BaseEntity {
@PrimaryGeneratedColumn('int')
id!: number;
// Custom date storage format
@Column({ transformer: dateStringTransformer })
lastLoginAt!: Date;
// Encrypted sensitive data
@Column({ transformer: encryptionTransformer })
sensitiveInfo!: string;
// Array stored as comma-separated values
@Column({ transformer: arrayTransformer })
permissions!: string[];
// Custom object serialization
@Column({ transformer: customObjectTransformer })
userSettings!: UserSettings;
// JSON column with custom transformer (transformer takes precedence)
@Column({ type: 'json', transformer: customObjectTransformer })
advancedSettings!: UserSettings;
}
// Usage examples
const userData = await UserData.create({
lastLoginAt: new Date('2023-12-01T10:30:00Z'),
sensitiveInfo: 'secret data',
permissions: ['read', 'write', 'admin'],
userSettings: { theme: 'dark', fontSize: 16, autoSave: true },
advancedSettings: { theme: 'light', fontSize: 14, autoSave: false }
});
// Values are automatically transformed during save/load
console.log(userData.lastLoginAt instanceof Date); // true
console.log(userData.permissions); // ['read', 'write', 'admin']
console.log(userData.sensitiveInfo); // 'secret data' (decrypted)Transformer Error Handling
Transformers include built-in error handling for robust data processing:
const safeJsonTransformer: ColumnTransformer<unknown, string> = {
to: (value: unknown) => {
try {
return JSON.stringify(value);
} catch (error) {
throw new Error(`JSON serialization failed: ${error.message}`);
}
},
from: (value: string) => {
try {
return JSON.parse(value);
} catch (error) {
throw new Error(`JSON deserialization failed: ${error.message}`);
}
}
};
@Entity('flexible_data')
export class FlexibleData extends BaseEntity {
@Column({ transformer: safeJsonTransformer })
dynamicContent!: unknown;
}
// Error handling in application code
try {
const data = await FlexibleData.create({
dynamicContent: { complex: 'data', with: ['arrays', 'and', 'objects'] }
});
} catch (error) {
if (error.message.includes('serialization failed')) {
console.error('Data transformation error:', error.message);
}
}JSON vs Transformer Priority
When both type: 'json' and transformer are specified, the custom transformer takes precedence:
@Entity('priority_example')
export class PriorityExample extends BaseEntity {
// JSON serialization (built-in)
@Column({ type: 'json' })
jsonData!: { key: string; value: number };
// Custom transformer takes precedence over JSON
@Column({ type: 'json', transformer: customObjectTransformer })
customData!: UserSettings; // Uses customObjectTransformer, not JSON.stringify/parse
}Performance Considerations
- JSON Columns: Optimized for complex objects and automatic type inference
- Custom Transformers: Best for specific data formats and security requirements
- Storage: JSON columns use SQLite TEXT storage with validation
- Indexing: JSON columns support standard SQLite indexing on the stored TEXT
🚨 Enhanced Error System
BunSQLiteORM features a comprehensive error handling system with a common base class and rich context information for better debugging and user experience.
Error Base Class
All ORM-specific errors extend BunSqliteOrmError, enabling graceful error handling:
import {
BunSqliteOrmError,
ValidationError,
DatabaseError,
EntityNotFoundError,
TransactionError,
ConstraintViolationError
} from 'bun-sqlite-orm';
try {
const user = await User.get(invalidId);
} catch (error) {
if (error instanceof BunSqliteOrmError) {
// All ORM errors have common properties
console.log(`Entity: ${error.entityName}`);
console.log(`Error Type: ${error.constructor.name}`);
console.log(`Timestamp: ${error.timestamp}`);
// Handle specific error types
if (error instanceof EntityNotFoundError) {
console.log(`Search criteria: ${JSON.stringify(error.criteria)}`);
console.log(`Entity: ${error.entity}`); // Getter for entityName
} else if (error instanceof ValidationError) {
console.log(`Validation errors for ${error.entityName}:`, error.errors);
} else if (error instanceof ConstraintViolationError) {
console.log(`Constraint violation: ${error.constraintType} on ${error.columnName}`);
}
} else {
// Handle non-ORM errors
console.log('Non-ORM error:', error);
}
}Specialized Error Classes
Core Entity Errors
EntityNotFoundError- Entity lookup failures with criteria contextValidationError- Entity validation failures with detailed field errorsDatabaseError- Database operation failures with operation context
Advanced Error Types
TransactionError- Transaction operation failures (begin, commit, rollback, savepoint)ConnectionError- Database connection issues with path and connection typeConstraintViolationError- Database constraint violations with constraint detailsConfigurationError- Configuration and setup issuesQueryError- SQL query execution failures with SQL and parametersTypeConversionError- Type conversion failures with property contextMigrationError- Migration operation failures with direction and migration name
Error Handling Examples
Validation Errors
try {
const user = await User.create({
name: '', // Invalid: too short
email: 'invalid-email' // Invalid: not an email
});
} catch (error) {
if (error instanceof ValidationError) {
console.log(`Validation failed for ${error.entityName}:`);
error.errors.forEach(err => {
console.log(` ${err.property}: ${err.message}`);
});
}
}Entity Not Found
try {
const user = await User.get(999); // Non-existent ID
} catch (error) {
if (error instanceof EntityNotFoundError) {
console.log(`${error.entity} not found with criteria:`, error.criteria);
}
}Database Constraint Violations
try {
await User.create({ email: '[email protected]' }); // Duplicate email
} catch (error) {
if (error instanceof ConstraintViolationError) {
console.log(`Constraint violation: ${error.constraintType}`);
console.log(`Column: ${error.columnName}, Value: ${error.value}`);
}
}Transaction Errors
try {
await dataSource.transaction(async (tx) => {
// Complex transaction operations
throw new Error('Simulated failure');
});
} catch (error) {
if (error instanceof TransactionError) {
console.log(`Transaction ${error.operation} failed`);
console.log(`Transaction ID: ${error.transactionId}`);
}
}User-Friendly Error Messages
Create user-facing error messages easily:
function renderErrorToUser(error: unknown): string {
if (error instanceof EntityNotFoundError) {
return `${error.entity} not found. Please check your search criteria.`;
} else if (error instanceof ValidationError) {
const fieldErrors = error.errors.map(e => `${e.property}: ${e.message}`).join(', ');
return `Invalid ${error.entityName}: ${fieldErrors}`;
} else if (error instanceof ConstraintViolationError) {
return `Data conflict: ${error.constraintType} constraint violation`;
} else if (error instanceof BunSqliteOrmError) {
return `Database operation failed: ${error.message}`;
} else {
return 'An unexpected error occurred';
}
}Error Context Benefits
- Entity Names: Know which entity caused the error
- Timestamps: Track when errors occurred for debugging
- Operation Context: Understand what operation failed (create, update, delete, etc.)
- Structured Data: Access error-specific properties (criteria, SQL, constraints, etc.)
- Type Safety: Full TypeScript support for all error properties
🛠️ Development
Setup Development Environment
# Clone the repository
git clone https://github.com/angelxmoreno/bun-sqlite-orm.git
cd bun-sqlite-orm
# Install dependencies
bun install
# Run tests
bun test # All tests
bun run test:unit # Unit tests only
bun run test:integration # Integration tests only
bun run test:coverage # With coverage report
# Code quality
bun run lint # Check code style
bun run lint:fix # Fix auto-fixable issues
bun run typecheck # TypeScript type checkingTesting
The project maintains high test coverage with comprehensive unit and integration tests:
- Unit Tests: Test individual components in isolation
- Integration Tests: Test complete workflows with real database operations
- Coverage: 98%+ line coverage maintained
📚 Documentation
- Architecture Overview
- Testing Strategy
- Base Methods Reference
- AI Assistant Guide - Comprehensive reference for AI assistants and code generation tools
🤝 Contributing
We welcome contributions! Please see our Contributing Guide for details.
Quick Links
📄 License
🙏 Acknowledgments
- Built with Bun - The fast all-in-one JavaScript runtime
- Validation powered by class-validator
- Dependency injection via tsyringe
- Inspired by TypeORM and ActiveRecord
