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.
- π 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
bun add bun-sqlite-orm
- Bun >= 1.1.21
- TypeScript >= 5.0.0
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;
}
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 metadata
Interact with your data using intuitive Active Record methods:
// Create and save entities
const user = User.build({
name: 'John Doe',
email: 'john@example.com',
age: 30
});
await user.save(); // Validates and inserts
// Create in one step with validation
const user2 = await User.create({
name: 'Jane Smith',
email: 'jane@example.com'
});
// 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: 'john@example.com' });
// 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: 'alice@example.com'
});
const profile = await Profile.create({
userId: user.id,
bio: 'Software Engineer'
});
return { user, profile }; // Both created atomically
});
BunSQLiteORM provides comprehensive TypeScript type safety throughout the API, with recent improvements to the update()
method for better developer experience.
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: 'new@email.com', // β
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)
// });
- 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
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: 'alice@example.com'
});
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 } });
BunSQLiteORM provides comprehensive transaction support for atomic database operations with automatic rollback on errors, ensuring data consistency and integrity.
// Simple transaction with automatic commit/rollback
const result = await dataSource.transaction(async (tx) => {
const user = await User.create({
name: 'John Doe',
email: 'john@example.com'
});
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
});
// 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
})
]);
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;
});
// 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 connections
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;
}
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({
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
})
// 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;
}
BunSQLiteORM provides comprehensive indexing support to optimize query performance. Indexes are automatically created during table migrations.
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;
}
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;
}
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 with options
@Index('idx_unique_email', ['email'], { unique: true })
// Available options:
interface IndexOptions {
unique?: boolean; // Create unique index (default: false)
}
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;
}
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
ValidationError
on validation failure - Preserves entity state on validation errors (won't save invalid data)
BunSQLiteORM provides complete support for composite primary keys, ideal for junction tables, many-to-many relationships, and multi-dimensional data models.
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;
}
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 });
- 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
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")
);
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"
}
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.
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
// 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 patterns
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
// }
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();
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 caching
- 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
Track entity changes and state with built-in methods:
const user = User.build({ name: 'John', email: 'john@example.com' });
// 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: 'john@example.com', 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
@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
}
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 | null
for 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;
BunSQLiteORM provides powerful support for JSON data storage and custom data transformations, enabling seamless handling of complex data types while maintaining type safety.
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); // 3
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;
}
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;
}
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)
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);
}
}
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
}
- 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
BunSQLiteORM features a comprehensive error handling system with a common base class and rich context information for better debugging and user experience.
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);
}
}
EntityNotFoundError
- Entity lookup failures with criteria contextValidationError
- Entity validation failures with detailed field errorsDatabaseError
- Database operation failures with operation context
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
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}`);
});
}
}
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);
}
}
try {
await User.create({ email: 'existing@example.com' }); // Duplicate email
} catch (error) {
if (error instanceof ConstraintViolationError) {
console.log(`Constraint violation: ${error.constraintType}`);
console.log(`Column: ${error.columnName}, Value: ${error.value}`);
}
}
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}`);
}
}
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';
}
}
- 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
# 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 checking
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
- Architecture Overview
- Testing Strategy
- Base Methods Reference
- AI Assistant Guide - Comprehensive reference for AI assistants and code generation tools
We welcome contributions! Please see our Contributing Guide for details.
- Built with Bun - The fast all-in-one JavaScript runtime
- Validation powered by class-validator
- Dependency injection via tsyringe
- Inspired by TypeORM and ActiveRecord