bun-sqlite-orm
Version:
A lightweight TypeScript ORM for Bun runtime with Bun SQLite, featuring Active Record pattern and decorator-based entities
276 lines (240 loc) • 10.1 kB
text/typescript
import { Database } from 'bun:sqlite';
import { container } from 'tsyringe';
import { getGlobalMetadataContainer, typeBunContainer } from './container';
import { NullLogger } from './logger';
import { MetadataContainer } from './metadata';
import { QueryBuilder, SqlGenerator } from './sql';
import { StatementCache } from './statement-cache';
import {
type SequentialTransactionCallback,
type TransactionCallback,
TransactionManager,
type TransactionOptions,
} from './transaction';
import type { DataSourceOptions, DbLogger } from './types';
export class DataSource {
private database: Database;
private typeBunContainer = container.createChildContainer();
private isInitialized = false;
private transactionManager!: TransactionManager;
constructor(private options: DataSourceOptions) {
this.database = new Database(options.database);
}
async initialize(): Promise<void> {
if (this.isInitialized) {
throw new Error('DataSource is already initialized');
}
// Register database connection in both containers
this.typeBunContainer.register('DatabaseConnection', {
useValue: this.database,
});
typeBunContainer.register('DatabaseConnection', {
useValue: this.database,
});
// Register logger or use NullLogger as default in both containers
const logger = this.options.logger || new NullLogger();
this.typeBunContainer.register('DbLogger', {
useValue: logger,
});
typeBunContainer.register('DbLogger', {
useValue: logger,
});
// Register MetadataContainer (only in local container)
this.typeBunContainer.registerSingleton('MetadataContainer', MetadataContainer);
// Register SqlGenerator (only in local container)
this.typeBunContainer.registerSingleton('SqlGenerator', SqlGenerator);
// Register QueryBuilder (only in local container - global already has it)
this.typeBunContainer.registerSingleton('QueryBuilder', QueryBuilder);
// Initialize TransactionManager
this.transactionManager = new TransactionManager(this.database, logger);
// Process entities and populate metadata
const metadataContainer = this.typeBunContainer.resolve<MetadataContainer>('MetadataContainer');
for (const entityClass of this.options.entities) {
// Force decorator execution by accessing the constructor
// This ensures all decorators have been processed
const _ = new (entityClass as new () => unknown)();
}
// BaseEntity now uses typeBunContainer directly
logger.info('bun-sqlite-orm DataSource initialized', {
database: this.options.database,
entities: this.options.entities.map((e) => e.name),
});
this.isInitialized = true;
}
async destroy(): Promise<void> {
if (!this.isInitialized) {
return;
}
const logger = this.typeBunContainer.resolve<DbLogger>('DbLogger');
logger.info('Destroying bun-sqlite-orm DataSource');
// Log statement cache statistics before cleanup
StatementCache.logStats(logger);
// Clean up all cached prepared statements before closing database
StatementCache.cleanup();
this.database.close();
this.isInitialized = false;
}
getDatabase(): Database {
if (!this.isInitialized) {
throw new Error('DataSource must be initialized before accessing database');
}
return this.database;
}
getMetadataContainer(): MetadataContainer {
if (!this.isInitialized) {
throw new Error('DataSource must be initialized before accessing metadata');
}
return getGlobalMetadataContainer();
}
getSqlGenerator(): SqlGenerator {
if (!this.isInitialized) {
throw new Error('DataSource must be initialized before accessing SqlGenerator');
}
return this.typeBunContainer.resolve<SqlGenerator>('SqlGenerator');
}
async runMigrations(): Promise<void> {
if (!this.isInitialized) {
throw new Error('DataSource must be initialized before running migrations');
}
const logger = this.typeBunContainer.resolve<DbLogger>('DbLogger');
logger.info('Running migrations...');
// TODO: Implement migration system
// For now, just create tables based on entity metadata
await this.createTables();
}
private async createTables(): Promise<void> {
const metadataContainer = this.getMetadataContainer();
const sqlGenerator = this.typeBunContainer.resolve<SqlGenerator>('SqlGenerator');
const entities = metadataContainer.getExplicitEntities(); // Only use entities with @Entity decorator
const logger = this.typeBunContainer.resolve<DbLogger>('DbLogger');
for (const entity of entities) {
const createTableSql = sqlGenerator.generateCreateTable(entity);
logger.debug(`Creating table: ${entity.tableName}`, { sql: createTableSql });
try {
this.database.exec(createTableSql);
logger.info(`Created table: ${entity.tableName}`);
} catch (error) {
logger.error(`Failed to create table: ${entity.tableName}`, error);
throw error;
}
}
// Create indexes after all tables are created
for (const entity of entities) {
const indexStatements = sqlGenerator.generateIndexes(entity);
for (const indexSql of indexStatements) {
logger.debug(`Creating index for table: ${entity.tableName}`, { sql: indexSql });
try {
this.database.exec(indexSql);
logger.info(`Created index for table: ${entity.tableName}`);
} catch (error) {
logger.error(`Failed to create index for table: ${entity.tableName}`, error);
throw error;
}
}
}
}
/**
* Execute a callback within a database transaction.
* The transaction will be automatically committed if the callback succeeds,
* or rolled back if an error occurs.
*
* @param callback Function to execute within the transaction
* @param options Transaction configuration options
* @returns Promise resolving to the callback result
*
* @example
* ```typescript
* const result = await dataSource.transaction(async (tx) => {
* const user = await User.create({ name: 'John' });
* const post = await Post.create({ title: 'Hello', userId: user.id });
* return { user, post };
* });
* ```
*/
async transaction<T>(callback: TransactionCallback<T>, options?: TransactionOptions): Promise<T> {
if (!this.isInitialized) {
throw new Error('DataSource must be initialized before starting transactions');
}
return this.transactionManager.execute(callback, options);
}
/**
* Execute multiple operations in parallel within a single transaction.
* All operations must succeed or the entire transaction will be rolled back.
*
* @param operations Array of functions to execute in parallel
* @param options Transaction configuration options
* @returns Promise resolving to array of results
*
* @example
* ```typescript
* const [user, posts] = await dataSource.transactionParallel([
* (tx) => User.create({ name: 'John' }),
* (tx) => Promise.all([
* Post.create({ title: 'Post 1' }),
* Post.create({ title: 'Post 2' })
* ])
* ]);
* ```
*/
async transactionParallel<T extends readonly unknown[] | []>(
operations: readonly [...{ [K in keyof T]: TransactionCallback<T[K]> }],
options?: TransactionOptions
): Promise<T> {
if (!this.isInitialized) {
throw new Error('DataSource must be initialized before starting transactions');
}
return this.transactionManager.executeParallel(operations, options);
}
/**
* Execute operations in sequence within a transaction.
* Each operation receives the result of the previous operation.
*
* @param operations Array of functions to execute in sequence
* @param options Transaction configuration options
* @returns Promise resolving to the final result
*
* @example
* ```typescript
* const result = await dataSource.transactionSequential([
* (tx) => User.create({ name: 'John' }),
* (tx, user) => Post.create({ title: 'Hello', userId: user.id }),
* (tx, post) => Comment.create({ text: 'Nice!', postId: post.id })
* ]);
* ```
*/
async transactionSequential<T>(
operations: SequentialTransactionCallback<unknown>[],
options?: TransactionOptions
): Promise<T> {
if (!this.isInitialized) {
throw new Error('DataSource must be initialized before starting transactions');
}
return this.transactionManager.executeSequential(operations, options);
}
/**
* Create a new transaction without auto-commit/rollback.
* You are responsible for managing the transaction lifecycle.
*
* @param options Transaction configuration options
* @returns New Transaction instance
*
* @example
* ```typescript
* const tx = dataSource.createTransaction();
* try {
* await tx.begin();
* // ... perform operations
* await tx.commit();
* } catch (error) {
* await tx.rollback();
* throw error;
* }
* ```
*/
createTransaction(options?: TransactionOptions) {
if (!this.isInitialized) {
throw new Error('DataSource must be initialized before creating transactions');
}
return this.transactionManager.createTransaction(options);
}
}