2.5 KiB
2.5 KiB
Database Layer Architecture
Responsibility
SQLite persistence with better-sqlite3, repository pattern (plain types), QueryQueue concurrency, type transformations.
Dependencies
- better-sqlite3: Native SQLite (requires rebuild for Electron)
- @redis-ui/core: Domain types
- p-queue: Query serialization
Consumers
- @redis-ui/services: Repositories via RepositoryFactory
- Main process: DatabaseManager initialization
Module Structure
src/
├── DatabaseManager.ts, QueryQueue.ts # Singleton, concurrency
├── BaseRepository.ts, RepositoryFactory.ts
├── schema.ts
└── repositories/ # One repo per entity
Repository Boundary (CRITICAL: Plain Types, NOT Result)
export class ConnectionRepository extends BaseRepository<ConnectionDB, Connection, ConnectionId> {
protected toApplication(db: ConnectionDB): Connection {
return {
id: ConnectionId.create(db.id),
host: db.host,
port: db.port,
sslEnabled: Boolean(db.ssl_enabled), // DB int → boolean
createdAt: new Date(db.created_at), // timestamp → Date
};
}
async findById(id: ConnectionId): Promise<Connection | null> {
return this.queue.enqueueRead((db) => {
const row = db.prepare('SELECT * FROM connections WHERE id = ?').get(id);
return row ? this.toApplication(row) : null;
});
}
}
// Service: Wraps repository in Result<T>
async createConnection(input: CreateInput): Promise<Result<Connection>> {
try {
const connection = await this.connectionRepo.create(input);
return Result.ok(connection);
} catch (error) {
return Result.fail(new InfrastructureError(error.message));
}
}
QueryQueue Pattern (Write Serialization)
export class QueryQueue {
private writeQueue = new PQueue({ concurrency: 1 }) // Single writer
private readQueue = new PQueue({ concurrency: 5 }) // Multiple readers
async enqueueWrite<T>(op: (db: Database) => T): Promise<T> {
return this.writeQueue.add(() => op(this.db))
}
}
Architectural Boundaries
- NO Result in repos: Services wrap with Result
- NO unqueued DB ops: Always use QueryQueue
- NO raw SQL in services: Use repositories