Skip to content

A lightweight TypeScript ORM for Bun runtime with Bun SQLite, featuring Active Record pattern and decorator-based entities

License

Notifications You must be signed in to change notification settings

angelxmoreno/bun-sqlite-orm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

BunSQLiteORM

NPM Version Bun TypeScript NPM License

Quality Gate Status codecov Maintainability Rating Security Rating Reliability Rating

CI GitHub issues GitHub stars


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-orm

Prerequisites

  • 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 metadata

3. 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: '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

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)
// });

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:

// 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

// 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

// 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 connections

Manual 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() 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:

@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) 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:

-- 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 patterns

Cache 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 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:

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

@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 | 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;

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); // 3

Type 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 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

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: '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

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 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

🀝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

Quick Links

πŸ“„ License

MIT Β© Angel S. Moreno

πŸ™ Acknowledgments


About

A lightweight TypeScript ORM for Bun runtime with Bun SQLite, featuring Active Record pattern and decorator-based entities

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages