From 946e88d90366141faa1263eafdc8859be5cf1add Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 25 May 2025 20:27:06 -0600 Subject: [PATCH 1/4] add a input area for arbitrary SQL on the test page --- src/db/index.ts | 7 +++-- src/views/TestView.vue | 60 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/db/index.ts b/src/db/index.ts index 444a98db..27256da0 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -144,9 +144,8 @@ export async function updateDefaultSettings( // console.log("Database version:", db.verno); await safeOpenDatabase(); } catch (openError: unknown) { - console.error("Failed to open database:", openError); - const errorMessage = openError instanceof Error ? openError.message : String(openError); - throw new Error(`Database connection failed: ${errorMessage}. Please try again or restart the app.`); + console.error("Failed to open database:", openError, String(openError)); + throw new Error(`The database connection failed. We recommend you try again or restart the app.`); } const result = await db.settings.update(MASTER_SETTINGS_KEY, settingsChanges); return result; @@ -155,7 +154,7 @@ export async function updateDefaultSettings( if (error instanceof Error) { throw error; // Re-throw if it's already an Error with a message } else { - throw new Error(`Failed to update settings: ${error}`); + throw new Error(`Failed to update settings. We recommend you try again or restart the app.`); } } } diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 619096dc..10bf8bfb 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -161,6 +161,38 @@ +
+

SQL Operations

+
+
+ +
+ +
+ +
+ +
+
+

Result:

+
{{ JSON.stringify(sqlResult, null, 2) }}
+
+
+

Image Sharing

Populates the "shared-photo" view as if they used "share_target". @@ -271,6 +303,7 @@ import { AppString, NotificationIface } from "../constants/app"; import { db, retrieveSettingsForActiveAccount } from "../db/index"; import * as vcLib from "../libs/crypto/vc"; import * as cryptoLib from "../libs/crypto"; +import databaseService from "../services/database"; import { PeerSetup, @@ -316,6 +349,10 @@ export default class Help extends Vue { peerSetup?: PeerSetup; userName?: string; + // for SQL operations + sqlQuery = ""; + sqlResult: any = null; + cryptoLib = cryptoLib; async mounted() { @@ -492,5 +529,28 @@ export default class Help extends Vue { ); logger.log("decoded", decoded); } + + async executeSql() { + try { + const isSelect = this.sqlQuery.trim().toLowerCase().startsWith('select'); + if (isSelect) { + this.sqlResult = await databaseService.query(this.sqlQuery); + } else { + this.sqlResult = await databaseService.run(this.sqlQuery); + } + console.log("SQL Result:", this.sqlResult); + } catch (error) { + console.error("SQL Error:", error); + this.$notify( + { + group: "alert", + type: "danger", + title: "SQL Error", + text: error instanceof Error ? error.message : String(error), + }, + 5000 + ); + } + } } From 5057d7d07f3ff48ce686bcefa52b3e32f7031bea Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 25 May 2025 20:37:16 -0600 Subject: [PATCH 2/4] don't always apply the camera-implementation cursor rules --- .../crowd-funder-for-time-pwa/docs/camera-implementation.mdc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.cursor/rules/crowd-funder-for-time-pwa/docs/camera-implementation.mdc b/.cursor/rules/crowd-funder-for-time-pwa/docs/camera-implementation.mdc index 22e02a57..e7fef13c 100644 --- a/.cursor/rules/crowd-funder-for-time-pwa/docs/camera-implementation.mdc +++ b/.cursor/rules/crowd-funder-for-time-pwa/docs/camera-implementation.mdc @@ -1,7 +1,7 @@ --- description: globs: -alwaysApply: true +alwaysApply: false --- # Camera Implementation Documentation From 5f24f4975dd2031a36f2ca1b7dfee9437be29586 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 25 May 2025 20:48:33 -0600 Subject: [PATCH 3/4] fix linting --- src/components/DataExportSection.vue | 2 +- src/db-sql/migration.ts | 14 +++--- src/db/index.ts | 22 +++++++--- src/interfaces/database.ts | 5 ++- src/libs/util.ts | 10 +++-- src/main.web.ts | 11 +++-- src/registerSQLWorker.js | 2 +- src/services/database.d.ts | 12 ++--- src/services/database.ts | 65 +++++++++++++++++----------- src/services/migrationService.ts | 18 +++++--- src/views/AccountViewView.vue | 10 +++-- src/views/ContactQRScanShowView.vue | 10 +++-- src/views/NewIdentifierView.vue | 10 +++-- src/views/TestView.vue | 14 +++--- 14 files changed, 130 insertions(+), 75 deletions(-) diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue index be565536..a0d3eaca 100644 --- a/src/components/DataExportSection.vue +++ b/src/components/DataExportSection.vue @@ -136,7 +136,7 @@ export default class DataExportSection extends Vue { transform: (table, value, key) => { if (table === "contacts") { // Dexie inserts a number 0 when some are undefined, so we need to totally remove them. - Object.keys(value).forEach(prop => { + Object.keys(value).forEach((prop) => { if (value[prop] === undefined) { delete value[prop]; } diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index bf7c50fb..d1f10090 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -1,10 +1,10 @@ -import migrationService from '../services/migrationService'; -import type { QueryExecResult } from '../services/migrationService'; +import migrationService from "../services/migrationService"; +import type { QueryExecResult } from "../services/migrationService"; // Each migration can include multiple SQL statements (with semicolons) const MIGRATIONS = [ { - name: '001_initial', + name: "001_initial", // see ../db/tables files for explanations of the fields sql: ` CREATE TABLE IF NOT EXISTS accounts ( @@ -84,8 +84,8 @@ const MIGRATIONS = [ id TEXT PRIMARY KEY, blobB64 TEXT ); - ` - } + `, + }, ]; export async function registerMigrations(): Promise { @@ -96,8 +96,8 @@ export async function registerMigrations(): Promise { } export async function runMigrations( - sqlExec: (sql: string, params?: any[]) => Promise> + sqlExec: (sql: string, params?: any[]) => Promise>, ): Promise { await registerMigrations(); await migrationService.runMigrations(sqlExec); -} \ No newline at end of file +} diff --git a/src/db/index.ts b/src/db/index.ts index 27256da0..d25d0ed9 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -90,7 +90,10 @@ db.on("populate", async () => { try { await db.settings.add(DEFAULT_SETTINGS); } catch (error) { - console.error("Error populating the database with default settings:", error); + console.error( + "Error populating the database with default settings:", + error, + ); } }); @@ -105,7 +108,7 @@ async function safeOpenDatabase(retries = 1, delay = 500): Promise { // Create a promise that rejects after 5 seconds const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Database open timed out')), 500); + setTimeout(() => reject(new Error("Database open timed out")), 500); }); // Race between the open operation and the timeout @@ -123,7 +126,7 @@ async function safeOpenDatabase(retries = 1, delay = 500): Promise { console.error(`Attempt ${i + 1}: Database open failed:`, error); if (i < retries - 1) { console.log(`Attempt ${i + 1}: Waiting ${delay}ms before retry...`); - await new Promise(resolve => setTimeout(resolve, delay)); + await new Promise((resolve) => setTimeout(resolve, delay)); } else { throw error; } @@ -145,16 +148,23 @@ export async function updateDefaultSettings( await safeOpenDatabase(); } catch (openError: unknown) { console.error("Failed to open database:", openError, String(openError)); - throw new Error(`The database connection failed. We recommend you try again or restart the app.`); + throw new Error( + `The database connection failed. We recommend you try again or restart the app.`, + ); } - const result = await db.settings.update(MASTER_SETTINGS_KEY, settingsChanges); + const result = await db.settings.update( + MASTER_SETTINGS_KEY, + settingsChanges, + ); return result; } catch (error) { console.error("Error updating default settings:", error); if (error instanceof Error) { throw error; // Re-throw if it's already an Error with a message } else { - throw new Error(`Failed to update settings. We recommend you try again or restart the app.`); + throw new Error( + `Failed to update settings. We recommend you try again or restart the app.`, + ); } } } diff --git a/src/interfaces/database.ts b/src/interfaces/database.ts index f828eb00..0e024c55 100644 --- a/src/interfaces/database.ts +++ b/src/interfaces/database.ts @@ -8,7 +8,10 @@ export interface QueryExecResult { export interface DatabaseService { initialize(): Promise; query(sql: string, params?: any[]): Promise; - run(sql: string, params?: any[]): Promise<{ changes: number; lastId?: number }>; + run( + sql: string, + params?: any[], + ): Promise<{ changes: number; lastId?: number }>; getOneRow(sql: string, params?: any[]): Promise; getAll(sql: string, params?: any[]): Promise; } diff --git a/src/libs/util.ts b/src/libs/util.ts index 573a5b37..51b1f063 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -561,14 +561,16 @@ export const generateSaveAndActivateIdentity = async (): Promise => { newId.did, identity, mnemonic, - newId.keys[0].publicKeyHex - ] + newId.keys[0].publicKeyHex, + ], ); - + await updateDefaultSettings({ activeDid: newId.did }); } catch (error) { console.error("Failed to update default settings:", error); - throw new Error("Failed to set default settings. Please try again or restart the app."); + throw new Error( + "Failed to set default settings. Please try again or restart the app.", + ); } await updateAccountSettings(newId.did, { isRegistered: false }); return newId.did; diff --git a/src/main.web.ts b/src/main.web.ts index ad974034..51280cc1 100644 --- a/src/main.web.ts +++ b/src/main.web.ts @@ -1,4 +1,4 @@ -import { initBackend } from 'absurd-sql/dist/indexeddb-main-thread'; +import { initBackend } from "absurd-sql/dist/indexeddb-main-thread"; import { initializeApp } from "./main.common"; import "./registerServiceWorker"; // Web PWA support @@ -6,9 +6,12 @@ const app = initializeApp(); function sqlInit() { // see https://github.com/jlongster/absurd-sql - let worker = new Worker(new URL('./registerSQLWorker.js', import.meta.url), { - type: 'module' - }); + const worker = new Worker( + new URL("./registerSQLWorker.js", import.meta.url), + { + type: "module", + }, + ); // This is only required because Safari doesn't support nested // workers. This installs a handler that will proxy creating web // workers through the main thread diff --git a/src/registerSQLWorker.js b/src/registerSQLWorker.js index cbff7f15..cac722b3 100644 --- a/src/registerSQLWorker.js +++ b/src/registerSQLWorker.js @@ -1,4 +1,4 @@ -import databaseService from './services/database'; +import databaseService from "./services/database"; async function run() { await databaseService.initialize(); diff --git a/src/services/database.d.ts b/src/services/database.d.ts index 032cc419..08032bfd 100644 --- a/src/services/database.d.ts +++ b/src/services/database.d.ts @@ -1,23 +1,25 @@ -import { DatabaseService } from '../interfaces/database'; +import { DatabaseService } from "../interfaces/database"; -declare module '@jlongster/sql.js' { +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; + function initSqlJs(config: { + locateFile: (file: string) => string; + }): Promise; export default initSqlJs; } -declare module 'absurd-sql' { +declare module "absurd-sql" { export class SQLiteFS { constructor(fs: any, backend: any); } } -declare module 'absurd-sql/dist/indexeddb-backend' { +declare module "absurd-sql/dist/indexeddb-backend" { export default class IndexedDBBackend { constructor(); } diff --git a/src/services/database.ts b/src/services/database.ts index 0b645adc..907a0e80 100644 --- a/src/services/database.ts +++ b/src/services/database.ts @@ -1,18 +1,21 @@ // Add type declarations for external modules -declare module '@jlongster/sql.js'; -declare module 'absurd-sql'; -declare module 'absurd-sql/dist/indexeddb-backend'; +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 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 type { QueryExecResult } from '../interfaces/database'; +import { runMigrations } from "../db-sql/migration"; +import type { QueryExecResult } from "../interfaces/database"; interface SQLDatabase { exec: (sql: string, params?: any[]) => Promise; - run: (sql: string, params?: any[]) => Promise<{ changes: number; lastId?: number }>; + run: ( + sql: string, + params?: any[], + ) => Promise<{ changes: number; lastId?: number }>; } class DatabaseService { @@ -62,34 +65,39 @@ class DatabaseService { const SQL = await initSqlJs({ locateFile: (file: string) => { - return new URL(`/node_modules/@jlongster/sql.js/dist/${file}`, import.meta.url).href; - } + return new URL( + `/node_modules/@jlongster/sql.js/dist/${file}`, + import.meta.url, + ).href; + }, }); - let sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend()); + const sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend()); SQL.register_for_idb(sqlFS); - SQL.FS.mkdir('/sql'); - SQL.FS.mount(sqlFS, {}, '/sql'); + 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+'); + const path = "/sql/db.sqlite"; + if (typeof SharedArrayBuffer === "undefined") { + const 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('The database initialization failed. We recommend you restart or reinstall.'); + throw new Error( + "The database initialization failed. We recommend you restart or reinstall.", + ); } - + await this.db.exec(`PRAGMA journal_mode=MEMORY;`); const sqlExec = this.db.exec.bind(this.db); - + // Run migrations await runMigrations(sqlExec); - + this.initialized = true; } @@ -108,13 +116,20 @@ class DatabaseService { // If initialized but no db, something went wrong if (!this.db) { - console.error(`Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`); - throw new Error(`The database could not be initialized. We recommend you restart or reinstall.`); + console.error( + `Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null`, + ); + throw new Error( + `The database could not be initialized. We recommend you restart or reinstall.`, + ); } } // Used for inserts, updates, and deletes - async run(sql: string, params: any[] = []): Promise<{ changes: number; lastId?: number }> { + async run( + sql: string, + params: any[] = [], + ): Promise<{ changes: number; lastId?: number }> { await this.waitForInitialization(); return this.db!.run(sql, params); } @@ -141,4 +156,4 @@ class DatabaseService { // Create a singleton instance const databaseService = DatabaseService.getInstance(); -export default databaseService; \ No newline at end of file +export default databaseService; diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index ca640694..18f49b56 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -1,4 +1,4 @@ -import { QueryExecResult } from '../interfaces/database'; +import { QueryExecResult } from "../interfaces/database"; interface Migration { name: string; @@ -23,7 +23,7 @@ export class MigrationService { } async runMigrations( - sqlExec: (sql: string, params?: any[]) => Promise> + sqlExec: (sql: string, params?: any[]) => Promise>, ): Promise { // Create migrations table if it doesn't exist await sqlExec(` @@ -35,12 +35,16 @@ export class MigrationService { `); // Get list of executed migrations - const result: QueryExecResult[] = await sqlExec('SELECT name FROM migrations;'); + const result: QueryExecResult[] = await sqlExec( + "SELECT name FROM migrations;", + ); let executedMigrations: Set = new Set(); // Even with that query, the QueryExecResult may be [] (which doesn't make sense to me). if (result.length > 0) { const singleResult = result[0]; - executedMigrations = new Set(singleResult.values.map((row: any[]) => row[0])); + executedMigrations = new Set( + singleResult.values.map((row: any[]) => row[0]), + ); } // Run pending migrations in order @@ -48,7 +52,9 @@ export class MigrationService { if (!executedMigrations.has(migration.name)) { try { await sqlExec(migration.sql); - await sqlExec('INSERT INTO migrations (name) VALUES (?)', [migration.name]); + 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); @@ -59,4 +65,4 @@ export class MigrationService { } } -export default MigrationService.getInstance(); \ No newline at end of file +export default MigrationService.getInstance(); diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 4dc68149..b3ae0d9e 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -76,7 +76,8 @@ Set Your Name

- (Don't worry: this is not visible to anyone until you share it with them. It's not sent to any servers.) + (Don't worry: this is not visible to anyone until you share it with + them. It's not sent to any servers.)

@@ -964,7 +965,7 @@ import { AxiosError } from "axios"; import { Buffer } from "buffer/"; import Dexie from "dexie"; import "dexie-export-import"; -// @ts-ignore - they aren't exporting it but it's there +// @ts-expect-error - they aren't exporting it but it's there import { ImportProgress } from "dexie-export-import"; import { LeafletMouseEvent } from "leaflet"; import * as R from "ramda"; @@ -1610,12 +1611,13 @@ export default class AccountViewView extends Vue { */ async submitImportFile() { if (inputImportFileNameRef.value != null) { - await db.delete() + await db + .delete() .then(async () => { // BulkError: settings.bulkAdd(): 1 of 21 operations failed. Errors: ConstraintError: Key already exists in the object store. await Dexie.import(inputImportFileNameRef.value as Blob, { progressCallback: this.progressCallback, - }) + }); }) .catch((error) => { logger.error("Error importing file:", error); diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 65257a15..f9351cf5 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -726,9 +726,11 @@ export default class ContactQRScanShow extends Vue { // Apply mirroring after a short delay to ensure video element is ready setTimeout(() => { - const videoElement = document.querySelector('.qr-scanner video') as HTMLVideoElement; + const videoElement = document.querySelector( + ".qr-scanner video", + ) as HTMLVideoElement; if (videoElement) { - videoElement.style.transform = 'scaleX(-1)'; + videoElement.style.transform = "scaleX(-1)"; } }, 1000); } @@ -943,7 +945,9 @@ export default class ContactQRScanShow extends Vue { // Add method to detect desktop browser private detectDesktopBrowser(): boolean { const userAgent = navigator.userAgent.toLowerCase(); - return !/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent); + return !/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test( + userAgent, + ); } // Update the computed property for camera mirroring diff --git a/src/views/NewIdentifierView.vue b/src/views/NewIdentifierView.vue index 547c43ca..7613b6ff 100644 --- a/src/views/NewIdentifierView.vue +++ b/src/views/NewIdentifierView.vue @@ -34,9 +34,13 @@
Error Creating Identity - +

- Try fully restarting the app. If that doesn't work, back up all data (identities and other data) and reinstall the app. + Try fully restarting the app. If that doesn't work, back up all data + (identities and other data) and reinstall the app.

@@ -85,7 +89,7 @@ export default class NewIdentifierView extends Vue { .catch((error) => { this.loading = false; this.hitError = true; - console.error('Failed to generate identity:', error); + console.error("Failed to generate identity:", error); }); } } diff --git a/src/views/TestView.vue b/src/views/TestView.vue index 10bf8bfb..abf6d87e 100644 --- a/src/views/TestView.vue +++ b/src/views/TestView.vue @@ -167,7 +167,9 @@
@@ -178,7 +180,7 @@ placeholder="Enter your SQL query here..." >
- +
@@ -532,7 +536,7 @@ export default class Help extends Vue { async executeSql() { try { - const isSelect = this.sqlQuery.trim().toLowerCase().startsWith('select'); + const isSelect = this.sqlQuery.trim().toLowerCase().startsWith("select"); if (isSelect) { this.sqlResult = await databaseService.query(this.sqlQuery); } else { @@ -548,7 +552,7 @@ export default class Help extends Vue { title: "SQL Error", text: error instanceof Error ? error.message : String(error), }, - 5000 + 5000, ); } } From 603823d808e215ec8379bfe6e655f6c60d78e531 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 25 May 2025 20:48:51 -0600 Subject: [PATCH 4/4] add to build instructions for electron on mac --- BUILDING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BUILDING.md b/BUILDING.md index 165ad1aa..1a764e9a 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -241,7 +241,9 @@ docker run -d \ 1. Build the electron app in production mode: ```bash - npm run build:electron-prod + npm run build:web + npm run electron:build + npm run build:electron-mac ``` 2. Package the Electron app for macOS: