6 changed files with 207 additions and 85 deletions
@ -0,0 +1,14 @@ |
|||||
|
export type SqlValue = string | number | null | Uint8Array; |
||||
|
|
||||
|
export interface QueryExecResult { |
||||
|
columns: Array<string>; |
||||
|
values: Array<Array<SqlValue>>; |
||||
|
} |
||||
|
|
||||
|
export interface DatabaseService { |
||||
|
initialize(): Promise<void>; |
||||
|
query(sql: string, params?: any[]): Promise<QueryExecResult[]>; |
||||
|
run(sql: string, params?: any[]): Promise<{ changes: number; lastId?: number }>; |
||||
|
get(sql: string, params?: any[]): Promise<any[] | undefined>; |
||||
|
all(sql: string, params?: any[]): Promise<any[][]>; |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
import { DatabaseService } from '../interfaces/database'; |
||||
|
|
||||
|
declare module '@jlongster/sql.js' { |
||||
|
interface SQL { |
||||
|
Database: any; |
||||
|
FS: any; |
||||
|
register_for_idb: (fs: any) => void; |
||||
|
} |
||||
|
|
||||
|
function initSqlJs(config: { locateFile: (file: string) => string }): Promise<SQL>; |
||||
|
export default initSqlJs; |
||||
|
} |
||||
|
|
||||
|
declare module 'absurd-sql' { |
||||
|
export class SQLiteFS { |
||||
|
constructor(fs: any, backend: any); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
declare module 'absurd-sql/dist/indexeddb-backend' { |
||||
|
export default class IndexedDBBackend { |
||||
|
constructor(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
declare const databaseService: DatabaseService; |
||||
|
export default databaseService; |
@ -1,81 +0,0 @@ |
|||||
import initSqlJs from '@jlongster/sql.js'; |
|
||||
import { SQLiteFS } from 'absurd-sql'; |
|
||||
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend'; |
|
||||
import { runMigrations } from '../db-sql/migration'; |
|
||||
|
|
||||
class DatabaseService { |
|
||||
constructor() { |
|
||||
this.db = null; |
|
||||
this.SQL = null; |
|
||||
this.initialized = false; |
|
||||
} |
|
||||
|
|
||||
async initialize() { |
|
||||
if (this.initialized) return; |
|
||||
|
|
||||
this.SQL = await initSqlJs({ |
|
||||
locateFile: file => { |
|
||||
return new URL(`/node_modules/@jlongster/sql.js/dist/${file}`, import.meta.url).href; |
|
||||
} |
|
||||
}); |
|
||||
|
|
||||
let sqlFS = new SQLiteFS(this.SQL.FS, new IndexedDBBackend()); |
|
||||
this.SQL.register_for_idb(sqlFS); |
|
||||
|
|
||||
this.SQL.FS.mkdir('/sql'); |
|
||||
this.SQL.FS.mount(sqlFS, {}, '/sql'); |
|
||||
|
|
||||
const path = '/sql/db.sqlite'; |
|
||||
if (typeof SharedArrayBuffer === 'undefined') { |
|
||||
let stream = this.SQL.FS.open(path, 'a+'); |
|
||||
await stream.node.contents.readIfFallback(); |
|
||||
this.SQL.FS.close(stream); |
|
||||
} |
|
||||
|
|
||||
this.db = new this.SQL.Database(path, { filename: true }); |
|
||||
this.db.exec(` |
|
||||
PRAGMA journal_mode=MEMORY; |
|
||||
`);
|
|
||||
const sqlExec = this.db.exec.bind(this.db); |
|
||||
|
|
||||
// Run migrations
|
|
||||
await runMigrations(sqlExec); |
|
||||
|
|
||||
this.initialized = true; |
|
||||
} |
|
||||
|
|
||||
async query(sql, params = []) { |
|
||||
if (!this.initialized) { |
|
||||
await this.initialize(); |
|
||||
} |
|
||||
return this.db.exec(sql, params); |
|
||||
} |
|
||||
|
|
||||
async run(sql, params = []) { |
|
||||
if (!this.initialized) { |
|
||||
await this.initialize(); |
|
||||
} |
|
||||
return this.db.run(sql, params); |
|
||||
} |
|
||||
|
|
||||
async get(sql, params = []) { |
|
||||
if (!this.initialized) { |
|
||||
await this.initialize(); |
|
||||
} |
|
||||
const result = await this.db.exec(sql, params); |
|
||||
return result[0]?.values[0]; |
|
||||
} |
|
||||
|
|
||||
async all(sql, params = []) { |
|
||||
if (!this.initialized) { |
|
||||
await this.initialize(); |
|
||||
} |
|
||||
const result = await this.db.exec(sql, params); |
|
||||
return result[0]?.values || []; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Create a singleton instance
|
|
||||
const databaseService = new DatabaseService(); |
|
||||
|
|
||||
export default databaseService; |
|
@ -0,0 +1,97 @@ |
|||||
|
// Add type declarations for external modules
|
||||
|
declare module '@jlongster/sql.js'; |
||||
|
declare module 'absurd-sql'; |
||||
|
declare module 'absurd-sql/dist/indexeddb-backend'; |
||||
|
|
||||
|
import initSqlJs from '@jlongster/sql.js'; |
||||
|
import { SQLiteFS } from 'absurd-sql'; |
||||
|
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend'; |
||||
|
import { runMigrations } from '../db-sql/migration'; |
||||
|
import { QueryExecResult } from './migrationService'; |
||||
|
|
||||
|
interface SQLDatabase { |
||||
|
exec: (sql: string, params?: any[]) => Promise<QueryExecResult[]>; |
||||
|
run: (sql: string, params?: any[]) => Promise<{ changes: number; lastId?: number }>; |
||||
|
} |
||||
|
|
||||
|
class DatabaseService { |
||||
|
private db: SQLDatabase | null; |
||||
|
private initialized: boolean; |
||||
|
|
||||
|
constructor() { |
||||
|
this.db = null; |
||||
|
this.initialized = false; |
||||
|
} |
||||
|
|
||||
|
async initialize(): Promise<void> { |
||||
|
if (this.initialized) return; |
||||
|
|
||||
|
const SQL = await initSqlJs({ |
||||
|
locateFile: (file: string) => { |
||||
|
return new URL(`/node_modules/@jlongster/sql.js/dist/${file}`, import.meta.url).href; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
let sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend()); |
||||
|
SQL.register_for_idb(sqlFS); |
||||
|
|
||||
|
SQL.FS.mkdir('/sql'); |
||||
|
SQL.FS.mount(sqlFS, {}, '/sql'); |
||||
|
|
||||
|
const path = '/sql/db.sqlite'; |
||||
|
if (typeof SharedArrayBuffer === 'undefined') { |
||||
|
let stream = SQL.FS.open(path, 'a+'); |
||||
|
await stream.node.contents.readIfFallback(); |
||||
|
SQL.FS.close(stream); |
||||
|
} |
||||
|
|
||||
|
this.db = new SQL.Database(path, { filename: true }); |
||||
|
if (!this.db) { |
||||
|
throw new Error('Failed to initialize database'); |
||||
|
} |
||||
|
|
||||
|
await this.db.exec(` |
||||
|
PRAGMA journal_mode=MEMORY; |
||||
|
`);
|
||||
|
const sqlExec = this.db.exec.bind(this.db); |
||||
|
|
||||
|
// Run migrations
|
||||
|
await runMigrations(sqlExec); |
||||
|
|
||||
|
this.initialized = true; |
||||
|
} |
||||
|
|
||||
|
private ensureInitialized(): void { |
||||
|
if (!this.initialized || !this.db) { |
||||
|
throw new Error('Database not initialized'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Used for inserts, updates, and deletes
|
||||
|
async run(sql: string, params: any[] = []): Promise<{ changes: number; lastId?: number }> { |
||||
|
this.ensureInitialized(); |
||||
|
return this.db!.run(sql, params); |
||||
|
} |
||||
|
|
||||
|
async query(sql: string, params: any[] = []): Promise<QueryExecResult[]> { |
||||
|
this.ensureInitialized(); |
||||
|
return this.db!.exec(sql, params); |
||||
|
} |
||||
|
|
||||
|
async get(sql: string, params: any[] = []): Promise<any[] | undefined> { |
||||
|
this.ensureInitialized(); |
||||
|
const result = await this.db!.exec(sql, params); |
||||
|
return result[0]?.values[0]; |
||||
|
} |
||||
|
|
||||
|
async all(sql: string, params: any[] = []): Promise<any[][]> { |
||||
|
this.ensureInitialized(); |
||||
|
const result = await this.db!.exec(sql, params); |
||||
|
return result[0]?.values || []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Create a singleton instance
|
||||
|
const databaseService = new DatabaseService(); |
||||
|
|
||||
|
export default databaseService; |
Loading…
Reference in new issue