bun-sqlite-orm
Version:
A lightweight TypeScript ORM for Bun runtime with Bun SQLite, featuring Active Record pattern and decorator-based entities
1,372 lines (1,058 loc) • 42.3 kB
Markdown
# BunSQLiteORM
<div align="center">
[](https://www.npmjs.com/package/bun-sqlite-orm)
[](https://bun.sh)
[](https://typescriptlang.org/)
[](https://opensource.org/licenses/MIT)
[](https://sonarcloud.io/summary/new_code?id=angelxmoreno_bun-sqlite-orm)
[](https://codecov.io/gh/angelxmoreno/bun-sqlite-orm)
[](https://sonarcloud.io/summary/new_code?id=angelxmoreno_bun-sqlite-orm)
[](https://sonarcloud.io/summary/new_code?id=angelxmoreno_bun-sqlite-orm)
[](https://sonarcloud.io/summary/new_code?id=angelxmoreno_bun-sqlite-orm)
[](https://github.com/angelxmoreno/bun-sqlite-orm/actions/workflows/pr-check.yml)
[](https://github.com/angelxmoreno/bun-sqlite-orm/issues)
[](https://github.com/angelxmoreno/bun-sqlite-orm/stargazers)
</div>
---
**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
```bash
bun add bun-sqlite-orm
```
### Prerequisites
- **Bun** >= 1.1.21
- **TypeScript** >= 5.0.0
## 🚀 Quick Start
### 1. Define Your Entities
Create type-safe entity classes with decorators:
```typescript
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:
```typescript
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
```
### 3. Use Active Record Methods
Interact with your data using intuitive Active Record methods:
```typescript
// 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
});
```
## 🎯 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<T>
The instance `update()` method now uses `Partial<T>` for complete type safety:
```typescript
@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)
// });
```
### Benefits of Enhanced Type Safety
1. **IntelliSense Support**: Full autocomplete for entity properties
2. **Compile-time Validation**: Catch typos and type errors before runtime
3. **Complex Types**: Support for objects, arrays, and nested structures
4. **Consistent API**: Matches the type safety of `create()` method
5. **Developer Experience**: Faster development with fewer bugs
### API Consistency
All entity methods now provide consistent type safety:
```typescript
// 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 } });
```
## 🔀 Transaction Support
BunSQLiteORM provides comprehensive transaction support for atomic database operations with automatic rollback on errors, ensuring data consistency and integrity.
### Basic Transactions
```typescript
// 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
});
```
### Advanced Transaction Patterns
```typescript
// 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)
```typescript
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
```typescript
// 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
```
### Manual Transaction Control
```typescript
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
```typescript
@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
```typescript
// 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:
```typescript
@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:
```typescript
@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:
```typescript
@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
```typescript
// 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}`
```typescript
@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](https://github.com/typestack/class-validator):
```typescript
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()` and `create()` methods
- Throws detailed `ValidationError` on 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:
```typescript
@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:
```typescript
// 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)` or `Entity.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:
```sql
-- 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:
```typescript
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
```typescript
// 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
```
### Cache Statistics
Access cache performance metrics for monitoring:
```typescript
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:
```typescript
// 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:
```typescript
// In your test setup
StatementCache.setTestMode(true); // Enables mock compatibility
// ... run tests with mocked database
StatementCache.setTestMode(false); // Restore normal caching
```
### Features
- **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:
```typescript
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
```
## 🔧 Advanced Usage
### Custom Data Types and Defaults
```typescript
@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
```typescript
// 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
```typescript
@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:
```typescript
@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
```
#### Type Inference for JSON Columns
TypeScript `Object` and `Array` types are automatically inferred as JSON columns:
```typescript
@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:
```typescript
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:
```typescript
@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:
```typescript
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:
```typescript
@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:
```typescript
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 context
- **`ValidationError`** - Entity validation failures with detailed field errors
- **`DatabaseError`** - 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 type
- **`ConstraintViolationError`** - Database constraint violations with constraint details
- **`ConfigurationError`** - Configuration and setup issues
- **`QueryError`** - SQL query execution failures with SQL and parameters
- **`TypeConversionError`** - Type conversion failures with property context
- **`MigrationError`** - Migration operation failures with direction and migration name
### Error Handling Examples
#### Validation Errors
```typescript
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
```typescript
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
```typescript
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}`);
}
}
```
#### Transaction Errors
```typescript
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:
```typescript
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
```bash
# 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
```
### Testing
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](./docs/architecture.md)
- [Testing Strategy](./docs/testing-strategy.md)
- [Base Methods Reference](./docs/base-methods.md)
- [AI Assistant Guide](./llm.txt) - Comprehensive reference for AI assistants and code generation tools
## 🤝 Contributing
We welcome contributions! Please see our [Contributing Guide](./CONTRIBUTING.md) for details.
### Quick Links
- [Code of Conduct](./CODE_OF_CONDUCT.md)
- [Issue Templates](https://github.com/angelxmoreno/bun-sqlite-orm/issues/new/choose)
- [Pull Request Template](./PULL_REQUEST_TEMPLATE.md)
## 📄 License
[MIT © Angel S. Moreno](https://github.com/angelxmoreno)
## 🙏 Acknowledgments
- Built with [Bun](https://bun.sh) - The fast all-in-one JavaScript runtime
- Validation powered by [class-validator](https://github.com/typestack/class-validator)
- Dependency injection via [tsyringe](https://github.com/microsoft/tsyringe)
- Inspired by [TypeORM](https://typeorm.io) and [ActiveRecord](https://guides.rubyonrails.org/active_record_basics.html)
---
<div align="center">
**[⭐ Star this repo](https://github.com/angelxmoreno/bun-sqlite-orm/stargazers) | [🐛 Report Bug](https://github.com/angelxmoreno/bun-sqlite-orm/issues) | [💡 Request Feature](https://github.com/angelxmoreno/bun-sqlite-orm/issues)**
</div>