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 databaseService from './services/database'; |
||||
import { SQLiteFS } from 'absurd-sql'; |
|
||||
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend'; |
|
||||
|
|
||||
async function run() { |
async function run() { |
||||
console.log("----- initSqlJs"); |
await databaseService.initialize(); |
||||
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); |
|
||||
} |
} |
||||
run(); |
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