From 8c3920e1080a75d699120f60da040e6e5c18c0a1 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 25 May 2025 11:06:30 -0600 Subject: [PATCH] add DB setup with migrations --- src/components/ImageMethodDialog.vue | 1 - src/db-sql/migration.ts | 38 +++++++++++++ src/registerSQLWorker.js | 31 +---------- src/services/database.js | 81 ++++++++++++++++++++++++++++ src/services/migrationService.ts | 63 ++++++++++++++++++++++ vite.config.ts | 2 +- 6 files changed, 185 insertions(+), 31 deletions(-) create mode 100644 src/db-sql/migration.ts create mode 100644 src/services/database.js create mode 100644 src/services/migrationService.ts diff --git a/src/components/ImageMethodDialog.vue b/src/components/ImageMethodDialog.vue index f3938740..9929db86 100644 --- a/src/components/ImageMethodDialog.vue +++ b/src/components/ImageMethodDialog.vue @@ -333,7 +333,6 @@ export default class ImageMethodDialog extends Vue { * @throws {Error} When settings retrieval fails */ async mounted() { - logger.log("ImageMethodDialog mounted"); try { const settings = await retrieveSettingsForActiveAccount(); this.activeDid = settings.activeDid || ""; diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts new file mode 100644 index 00000000..8f4cc69c --- /dev/null +++ b/src/db-sql/migration.ts @@ -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 { + // Register all migrations + for (const migration of MIGRATIONS) { + await migrationService.registerMigration(migration); + } +} + +export async function runMigrations( + sqlExec: (sql: string, params?: any[]) => Promise> +): Promise { + await registerMigrations(); + await migrationService.runMigrations(sqlExec); +} \ No newline at end of file diff --git a/src/registerSQLWorker.js b/src/registerSQLWorker.js index d728a8e9..cbff7f15 100644 --- a/src/registerSQLWorker.js +++ b/src/registerSQLWorker.js @@ -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(); diff --git a/src/services/database.js b/src/services/database.js new file mode 100644 index 00000000..ea872ae1 --- /dev/null +++ b/src/services/database.js @@ -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; \ No newline at end of file diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts new file mode 100644 index 00000000..106d06d3 --- /dev/null +++ b/src/services/migrationService.ts @@ -0,0 +1,63 @@ +type SqlValue = string | number | null | Uint8Array; + +export interface QueryExecResult { + columns: Array; + values: Array>; +} + +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 { + this.migrations.push(migration); + } + + async runMigrations( + sqlExec: (sql: string, params?: any[]) => Promise> + ): Promise { + // 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(); \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 61a62e08..7f80c569 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -49,5 +49,5 @@ export default defineConfig({ } } }, - assetsInclude: ['**/*.wasm'] + assetsInclude: ['**/*.wasm', '**/*.sql'] }); \ No newline at end of file