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