6 changed files with 185 additions and 31 deletions
@ -0,0 +1,38 @@ |
|||
import migrationService from '../services/migrationService'; |
|||
import type { QueryExecResult } from '../services/migrationService'; |
|||
|
|||
const MIGRATIONS = [ |
|||
{ |
|||
name: '001_create_accounts_table', |
|||
// see ../db/tables files for explanations
|
|||
sql: ` |
|||
CREATE TABLE IF NOT EXISTS accounts ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
dateCreated TEXT NOT NULL, |
|||
derivationPath TEXT, |
|||
did TEXT NOT NULL, |
|||
identity TEXT, |
|||
mnemonic TEXT, |
|||
passkeyCredIdHex TEXT, |
|||
publicKeyHex TEXT NOT NULL |
|||
); |
|||
|
|||
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did); |
|||
CREATE INDEX IF NOT EXISTS idx_accounts_publicKeyHex ON accounts(publicKeyHex); |
|||
` |
|||
} |
|||
]; |
|||
|
|||
export async function registerMigrations(): Promise<void> { |
|||
// Register all migrations
|
|||
for (const migration of MIGRATIONS) { |
|||
await migrationService.registerMigration(migration); |
|||
} |
|||
} |
|||
|
|||
export async function runMigrations( |
|||
sqlExec: (sql: string, params?: any[]) => Promise<Array<QueryExecResult>> |
|||
): Promise<void> { |
|||
await registerMigrations(); |
|||
await migrationService.runMigrations(sqlExec); |
|||
} |
@ -1,33 +1,6 @@ |
|||
import initSqlJs from '@jlongster/sql.js'; |
|||
import { SQLiteFS } from 'absurd-sql'; |
|||
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend'; |
|||
import databaseService from './services/database'; |
|||
|
|||
async function run() { |
|||
console.log("----- initSqlJs"); |
|||
let SQL = await initSqlJs({ |
|||
locateFile: file => { |
|||
// In Vite, we need to use the full URL to the WASM file
|
|||
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); |
|||
} |
|||
|
|||
let db = new SQL.Database(path, { filename: true }); |
|||
// You might want to try `PRAGMA page_size=8192;` too!
|
|||
db.exec(` |
|||
PRAGMA journal_mode=MEMORY; |
|||
`);
|
|||
console.log("----- db", db); |
|||
await databaseService.initialize(); |
|||
} |
|||
run(); |
|||
|
@ -0,0 +1,81 @@ |
|||
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,63 @@ |
|||
type SqlValue = string | number | null | Uint8Array; |
|||
|
|||
export interface QueryExecResult { |
|||
columns: Array<string>; |
|||
values: Array<Array<SqlValue>>; |
|||
} |
|||
|
|||
interface Migration { |
|||
name: string; |
|||
sql: string; |
|||
} |
|||
|
|||
export class MigrationService { |
|||
private static instance: MigrationService; |
|||
private migrations: Migration[] = []; |
|||
|
|||
private constructor() {} |
|||
|
|||
static getInstance(): MigrationService { |
|||
if (!MigrationService.instance) { |
|||
MigrationService.instance = new MigrationService(); |
|||
} |
|||
return MigrationService.instance; |
|||
} |
|||
|
|||
async registerMigration(migration: Migration): Promise<void> { |
|||
this.migrations.push(migration); |
|||
} |
|||
|
|||
async runMigrations( |
|||
sqlExec: (sql: string, params?: any[]) => Promise<Array<QueryExecResult>> |
|||
): Promise<void> { |
|||
// Create migrations table if it doesn't exist
|
|||
await sqlExec(` |
|||
CREATE TABLE IF NOT EXISTS migrations ( |
|||
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|||
name TEXT NOT NULL UNIQUE, |
|||
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
|||
) |
|||
`);
|
|||
|
|||
// Get list of executed migrations
|
|||
const result = await sqlExec('SELECT name FROM migrations'); |
|||
const singleResult = result[0]; |
|||
const executedMigrations = new Set(singleResult.values.map(row => row[0])); |
|||
|
|||
// Run pending migrations in order
|
|||
for (const migration of this.migrations) { |
|||
if (!executedMigrations.has(migration.name)) { |
|||
try { |
|||
await sqlExec(migration.sql); |
|||
await sqlExec('INSERT INTO migrations (name) VALUES (?)', [migration.name]); |
|||
console.log(`Migration ${migration.name} executed successfully`); |
|||
} catch (error) { |
|||
console.error(`Error executing migration ${migration.name}:`, error); |
|||
throw error; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
export default MigrationService.getInstance(); |
Loading…
Reference in new issue