147 changed files with 10861 additions and 3758 deletions
			
			
		| @ -0,0 +1,153 @@ | |||
| --- | |||
| description:  | |||
| globs:  | |||
| alwaysApply: true | |||
| --- | |||
| # Absurd SQL - Cursor Development Guide | |||
| 
 | |||
| ## Project Overview | |||
| Absurd SQL is a backend implementation for sql.js that enables persistent SQLite databases in the browser by using IndexedDB as a block storage system. This guide provides rules and best practices for developing with this project in Cursor. | |||
| 
 | |||
| ## Project Structure | |||
| ``` | |||
| absurd-sql/ | |||
| ├── src/               # Source code | |||
| ├── dist/             # Built files | |||
| ├── package.json      # Dependencies and scripts | |||
| ├── rollup.config.js  # Build configuration | |||
| └── jest.config.js    # Test configuration | |||
| ``` | |||
| 
 | |||
| ## Development Rules | |||
| 
 | |||
| ### 1. Worker Thread Requirements | |||
| - All SQL operations MUST be performed in a worker thread | |||
| - Main thread should only handle worker initialization and communication | |||
| - Never block the main thread with database operations | |||
| 
 | |||
| ### 2. Code Organization | |||
| - Keep worker code in separate files (e.g., `*.worker.js`) | |||
| - Use ES modules for imports/exports | |||
| - Follow the project's existing module structure | |||
| 
 | |||
| ### 3. Required Headers | |||
| When developing locally or deploying, ensure these headers are set: | |||
| ``` | |||
| Cross-Origin-Opener-Policy: same-origin | |||
| Cross-Origin-Embedder-Policy: require-corp | |||
| ``` | |||
| 
 | |||
| ### 4. Browser Compatibility | |||
| - Primary target: Modern browsers with SharedArrayBuffer support | |||
| - Fallback mode: Safari (with limitations) | |||
| - Always test in both modes | |||
| 
 | |||
| ### 5. Database Configuration | |||
| Recommended database settings: | |||
| ```sql | |||
| PRAGMA journal_mode=MEMORY; | |||
| PRAGMA page_size=8192;  -- Optional, but recommended | |||
| ``` | |||
| 
 | |||
| ### 6. Development Workflow | |||
| 1. Install dependencies: | |||
|    ```bash | |||
|    yarn add @jlongster/sql.js absurd-sql | |||
|    ``` | |||
| 
 | |||
| 2. Development commands: | |||
|    - `yarn build` - Build the project | |||
|    - `yarn jest` - Run tests | |||
|    - `yarn serve` - Start development server | |||
| 
 | |||
| ### 7. Testing Guidelines | |||
| - Write tests for both SharedArrayBuffer and fallback modes | |||
| - Use Jest for testing | |||
| - Include performance benchmarks for critical operations | |||
| 
 | |||
| ### 8. Performance Considerations | |||
| - Use bulk operations when possible | |||
| - Monitor read/write performance | |||
| - Consider using transactions for multiple operations | |||
| - Avoid unnecessary database connections | |||
| 
 | |||
| ### 9. Error Handling | |||
| - Implement proper error handling for: | |||
|   - Worker initialization failures | |||
|   - Database connection issues | |||
|   - Concurrent access conflicts (in fallback mode) | |||
|   - Storage quota exceeded scenarios | |||
| 
 | |||
| ### 10. Security Best Practices | |||
| - Never expose database operations directly to the client | |||
| - Validate all SQL queries | |||
| - Implement proper access controls | |||
| - Handle sensitive data appropriately | |||
| 
 | |||
| ### 11. Code Style | |||
| - Follow ESLint configuration | |||
| - Use async/await for asynchronous operations | |||
| - Document complex database operations | |||
| - Include comments for non-obvious optimizations | |||
| 
 | |||
| ### 12. Debugging | |||
| - Use `jest-debug` for debugging tests | |||
| - Monitor IndexedDB usage in browser dev tools | |||
| - Check worker communication in console | |||
| - Use performance monitoring tools | |||
| 
 | |||
| ## Common Patterns | |||
| 
 | |||
| ### Worker Initialization | |||
| ```javascript | |||
| // Main thread | |||
| import { initBackend } from 'absurd-sql/dist/indexeddb-main-thread'; | |||
| 
 | |||
| function init() { | |||
|   let worker = new Worker(new URL('./index.worker.js', import.meta.url)); | |||
|   initBackend(worker); | |||
| } | |||
| ``` | |||
| 
 | |||
| ### Database Setup | |||
| ```javascript | |||
| // Worker thread | |||
| import initSqlJs from '@jlongster/sql.js'; | |||
| import { SQLiteFS } from 'absurd-sql'; | |||
| import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend'; | |||
| 
 | |||
| async function setupDatabase() { | |||
|   let SQL = await initSqlJs({ locateFile: file => file }); | |||
|   let sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend()); | |||
|   SQL.register_for_idb(sqlFS); | |||
|    | |||
|   SQL.FS.mkdir('/sql'); | |||
|   SQL.FS.mount(sqlFS, {}, '/sql'); | |||
|    | |||
|   return new SQL.Database('/sql/db.sqlite', { filename: true }); | |||
| } | |||
| ``` | |||
| 
 | |||
| ## Troubleshooting | |||
| 
 | |||
| ### Common Issues | |||
| 1. SharedArrayBuffer not available | |||
|    - Check COOP/COEP headers | |||
|    - Verify browser support | |||
|    - Test fallback mode | |||
| 
 | |||
| 2. Worker initialization failures | |||
|    - Check file paths | |||
|    - Verify module imports | |||
|    - Check browser console for errors | |||
| 
 | |||
| 3. Performance issues | |||
|    - Monitor IndexedDB usage | |||
|    - Check for unnecessary operations | |||
|    - Verify transaction usage | |||
| 
 | |||
| ## Resources | |||
| - [Project Demo](https://priceless-keller-d097e5.netlify.app/) | |||
| - [Example Project](https://github.com/jlongster/absurd-example-project) | |||
| - [Blog Post](https://jlongster.com/future-sql-web) | |||
| - [SQL.js Documentation](https://github.com/sql-js/sql.js/)  | |||
| @ -1,6 +0,0 @@ | |||
| # Admin DID credentials | |||
| ADMIN_DID=did:ethr:0x0000694B58C2cC69658993A90D3840C560f2F51F | |||
| ADMIN_PRIVATE_KEY=2b6472c026ec2aa2c4235c994a63868fc9212d18b58f6cbfe861b52e71330f5b | |||
| 
 | |||
| # API Configuration | |||
| ENDORSER_API_URL=https://test-api.endorser.ch/api/v2/claim  | |||
| @ -1,7 +1,15 @@ | |||
| package app.timesafari; | |||
| 
 | |||
| import android.os.Bundle; | |||
| import com.getcapacitor.BridgeActivity; | |||
| //import com.getcapacitor.community.sqlite.SQLite;
 | |||
| 
 | |||
| public class MainActivity extends BridgeActivity { | |||
|     // ... existing code ...
 | |||
|     @Override | |||
|     public void onCreate(Bundle savedInstanceState) { | |||
|         super.onCreate(savedInstanceState); | |||
|          | |||
|         // Initialize SQLite
 | |||
|         //registerPlugin(SQLite.class);
 | |||
|     } | |||
| }  | |||
| @ -1,5 +0,0 @@ | |||
| package timesafari.app; | |||
| 
 | |||
| import com.getcapacitor.BridgeActivity; | |||
| 
 | |||
| public class MainActivity extends BridgeActivity {} | |||
| @ -0,0 +1,399 @@ | |||
| # Dexie to absurd-sql Mapping Guide | |||
| 
 | |||
| ## Schema Mapping | |||
| 
 | |||
| ### Current Dexie Schema | |||
| ```typescript | |||
| // Current Dexie schema | |||
| const db = new Dexie('TimeSafariDB'); | |||
| 
 | |||
| db.version(1).stores({ | |||
|   accounts: 'did, publicKeyHex, createdAt, updatedAt', | |||
|   settings: 'key, value, updatedAt', | |||
|   contacts: 'id, did, name, createdAt, updatedAt' | |||
| }); | |||
| ``` | |||
| 
 | |||
| ### New SQLite Schema | |||
| ```sql | |||
| -- New SQLite schema | |||
| CREATE TABLE accounts ( | |||
|   did TEXT PRIMARY KEY, | |||
|   public_key_hex TEXT NOT NULL, | |||
|   created_at INTEGER NOT NULL, | |||
|   updated_at INTEGER NOT NULL | |||
| ); | |||
| 
 | |||
| CREATE TABLE settings ( | |||
|   key TEXT PRIMARY KEY, | |||
|   value TEXT NOT NULL, | |||
|   updated_at INTEGER NOT NULL | |||
| ); | |||
| 
 | |||
| CREATE TABLE contacts ( | |||
|   id TEXT PRIMARY KEY, | |||
|   did TEXT NOT NULL, | |||
|   name TEXT, | |||
|   created_at INTEGER NOT NULL, | |||
|   updated_at INTEGER NOT NULL, | |||
|   FOREIGN KEY (did) REFERENCES accounts(did) | |||
| ); | |||
| 
 | |||
| -- Indexes for performance | |||
| CREATE INDEX idx_accounts_created_at ON accounts(created_at); | |||
| CREATE INDEX idx_contacts_did ON contacts(did); | |||
| CREATE INDEX idx_settings_updated_at ON settings(updated_at); | |||
| ``` | |||
| 
 | |||
| ## Query Mapping | |||
| 
 | |||
| ### 1. Account Operations | |||
| 
 | |||
| #### Get Account by DID | |||
| ```typescript | |||
| // Dexie | |||
| const account = await db.accounts.get(did); | |||
| 
 | |||
| // absurd-sql | |||
| const result = await db.exec(` | |||
|   SELECT * FROM accounts WHERE did = ? | |||
| `, [did]); | |||
| const account = result[0]?.values[0]; | |||
| ``` | |||
| 
 | |||
| #### Get All Accounts | |||
| ```typescript | |||
| // Dexie | |||
| const accounts = await db.accounts.toArray(); | |||
| 
 | |||
| // absurd-sql | |||
| const result = await db.exec(` | |||
|   SELECT * FROM accounts ORDER BY created_at DESC | |||
| `); | |||
| const accounts = result[0]?.values || []; | |||
| ``` | |||
| 
 | |||
| #### Add Account | |||
| ```typescript | |||
| // Dexie | |||
| await db.accounts.add({ | |||
|   did, | |||
|   publicKeyHex, | |||
|   createdAt: Date.now(), | |||
|   updatedAt: Date.now() | |||
| }); | |||
| 
 | |||
| // absurd-sql | |||
| await db.run(` | |||
|   INSERT INTO accounts (did, public_key_hex, created_at, updated_at) | |||
|   VALUES (?, ?, ?, ?) | |||
| `, [did, publicKeyHex, Date.now(), Date.now()]); | |||
| ``` | |||
| 
 | |||
| #### Update Account | |||
| ```typescript | |||
| // Dexie | |||
| await db.accounts.update(did, { | |||
|   publicKeyHex, | |||
|   updatedAt: Date.now() | |||
| }); | |||
| 
 | |||
| // absurd-sql | |||
| await db.run(` | |||
|   UPDATE accounts  | |||
|   SET public_key_hex = ?, updated_at = ? | |||
|   WHERE did = ? | |||
| `, [publicKeyHex, Date.now(), did]); | |||
| ``` | |||
| 
 | |||
| ### 2. Settings Operations | |||
| 
 | |||
| #### Get Setting | |||
| ```typescript | |||
| // Dexie | |||
| const setting = await db.settings.get(key); | |||
| 
 | |||
| // absurd-sql | |||
| const result = await db.exec(` | |||
|   SELECT * FROM settings WHERE key = ? | |||
| `, [key]); | |||
| const setting = result[0]?.values[0]; | |||
| ``` | |||
| 
 | |||
| #### Set Setting | |||
| ```typescript | |||
| // Dexie | |||
| await db.settings.put({ | |||
|   key, | |||
|   value, | |||
|   updatedAt: Date.now() | |||
| }); | |||
| 
 | |||
| // absurd-sql | |||
| await db.run(` | |||
|   INSERT INTO settings (key, value, updated_at) | |||
|   VALUES (?, ?, ?) | |||
|   ON CONFLICT(key) DO UPDATE SET | |||
|     value = excluded.value, | |||
|     updated_at = excluded.updated_at | |||
| `, [key, value, Date.now()]); | |||
| ``` | |||
| 
 | |||
| ### 3. Contact Operations | |||
| 
 | |||
| #### Get Contacts by Account | |||
| ```typescript | |||
| // Dexie | |||
| const contacts = await db.contacts | |||
|   .where('did') | |||
|   .equals(accountDid) | |||
|   .toArray(); | |||
| 
 | |||
| // absurd-sql | |||
| const result = await db.exec(` | |||
|   SELECT * FROM contacts  | |||
|   WHERE did = ? | |||
|   ORDER BY created_at DESC | |||
| `, [accountDid]); | |||
| const contacts = result[0]?.values || []; | |||
| ``` | |||
| 
 | |||
| #### Add Contact | |||
| ```typescript | |||
| // Dexie | |||
| await db.contacts.add({ | |||
|   id: generateId(), | |||
|   did: accountDid, | |||
|   name, | |||
|   createdAt: Date.now(), | |||
|   updatedAt: Date.now() | |||
| }); | |||
| 
 | |||
| // absurd-sql | |||
| await db.run(` | |||
|   INSERT INTO contacts (id, did, name, created_at, updated_at) | |||
|   VALUES (?, ?, ?, ?, ?) | |||
| `, [generateId(), accountDid, name, Date.now(), Date.now()]); | |||
| ``` | |||
| 
 | |||
| ## Transaction Mapping | |||
| 
 | |||
| ### Batch Operations | |||
| ```typescript | |||
| // Dexie | |||
| await db.transaction('rw', [db.accounts, db.contacts], async () => { | |||
|   await db.accounts.add(account); | |||
|   await db.contacts.bulkAdd(contacts); | |||
| }); | |||
| 
 | |||
| // absurd-sql | |||
| await db.exec('BEGIN TRANSACTION;'); | |||
| try { | |||
|   await db.run(` | |||
|     INSERT INTO accounts (did, public_key_hex, created_at, updated_at) | |||
|     VALUES (?, ?, ?, ?) | |||
|   `, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]); | |||
| 
 | |||
|   for (const contact of contacts) { | |||
|     await db.run(` | |||
|       INSERT INTO contacts (id, did, name, created_at, updated_at) | |||
|       VALUES (?, ?, ?, ?, ?) | |||
|     `, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]); | |||
|   } | |||
|   await db.exec('COMMIT;'); | |||
| } catch (error) { | |||
|   await db.exec('ROLLBACK;'); | |||
|   throw error; | |||
| } | |||
| ``` | |||
| 
 | |||
| ## Migration Helper Functions | |||
| 
 | |||
| ### 1. Data Export (Dexie to JSON) | |||
| ```typescript | |||
| async function exportDexieData(): Promise<MigrationData> { | |||
|   const db = new Dexie('TimeSafariDB'); | |||
|    | |||
|   return { | |||
|     accounts: await db.accounts.toArray(), | |||
|     settings: await db.settings.toArray(), | |||
|     contacts: await db.contacts.toArray(), | |||
|     metadata: { | |||
|       version: '1.0.0', | |||
|       timestamp: Date.now(), | |||
|       dexieVersion: Dexie.version | |||
|     } | |||
|   }; | |||
| } | |||
| ``` | |||
| 
 | |||
| ### 2. Data Import (JSON to absurd-sql) | |||
| ```typescript | |||
| async function importToAbsurdSql(data: MigrationData): Promise<void> { | |||
|   await db.exec('BEGIN TRANSACTION;'); | |||
|   try { | |||
|     // Import accounts | |||
|     for (const account of data.accounts) { | |||
|       await db.run(` | |||
|         INSERT INTO accounts (did, public_key_hex, created_at, updated_at) | |||
|         VALUES (?, ?, ?, ?) | |||
|       `, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]); | |||
|     } | |||
|      | |||
|     // Import settings | |||
|     for (const setting of data.settings) { | |||
|       await db.run(` | |||
|         INSERT INTO settings (key, value, updated_at) | |||
|         VALUES (?, ?, ?) | |||
|       `, [setting.key, setting.value, setting.updatedAt]); | |||
|     } | |||
|      | |||
|     // Import contacts | |||
|     for (const contact of data.contacts) { | |||
|       await db.run(` | |||
|         INSERT INTO contacts (id, did, name, created_at, updated_at) | |||
|         VALUES (?, ?, ?, ?, ?) | |||
|       `, [contact.id, contact.did, contact.name, contact.createdAt, contact.updatedAt]); | |||
|     } | |||
|     await db.exec('COMMIT;'); | |||
|   } catch (error) { | |||
|     await db.exec('ROLLBACK;'); | |||
|     throw error; | |||
|   } | |||
| } | |||
| ``` | |||
| 
 | |||
| ### 3. Verification | |||
| ```typescript | |||
| async function verifyMigration(dexieData: MigrationData): Promise<boolean> { | |||
|   // Verify account count | |||
|   const accountResult = await db.exec('SELECT COUNT(*) as count FROM accounts'); | |||
|   const accountCount = accountResult[0].values[0][0]; | |||
|   if (accountCount !== dexieData.accounts.length) { | |||
|     return false; | |||
|   } | |||
|    | |||
|   // Verify settings count | |||
|   const settingsResult = await db.exec('SELECT COUNT(*) as count FROM settings'); | |||
|   const settingsCount = settingsResult[0].values[0][0]; | |||
|   if (settingsCount !== dexieData.settings.length) { | |||
|     return false; | |||
|   } | |||
|    | |||
|   // Verify contacts count | |||
|   const contactsResult = await db.exec('SELECT COUNT(*) as count FROM contacts'); | |||
|   const contactsCount = contactsResult[0].values[0][0]; | |||
|   if (contactsCount !== dexieData.contacts.length) { | |||
|     return false; | |||
|   } | |||
|    | |||
|   // Verify data integrity | |||
|   for (const account of dexieData.accounts) { | |||
|     const result = await db.exec( | |||
|       'SELECT * FROM accounts WHERE did = ?', | |||
|       [account.did] | |||
|     ); | |||
|     const migratedAccount = result[0]?.values[0]; | |||
|     if (!migratedAccount ||  | |||
|         migratedAccount[1] !== account.publicKeyHex) { // public_key_hex is second column | |||
|       return false; | |||
|     } | |||
|   } | |||
|    | |||
|   return true; | |||
| } | |||
| ``` | |||
| 
 | |||
| ## Performance Considerations | |||
| 
 | |||
| ### 1. Indexing | |||
| - Dexie automatically creates indexes based on the schema | |||
| - absurd-sql requires explicit index creation | |||
| - Added indexes for frequently queried fields | |||
| - Use `PRAGMA journal_mode=MEMORY;` for better performance | |||
| 
 | |||
| ### 2. Batch Operations | |||
| - Dexie has built-in bulk operations | |||
| - absurd-sql uses transactions for batch operations | |||
| - Consider chunking large datasets | |||
| - Use prepared statements for repeated queries | |||
| 
 | |||
| ### 3. Query Optimization | |||
| - Dexie uses IndexedDB's native indexing | |||
| - absurd-sql requires explicit query optimization | |||
| - Use prepared statements for repeated queries | |||
| - Consider using `PRAGMA synchronous=NORMAL;` for better performance | |||
| 
 | |||
| ## Error Handling | |||
| 
 | |||
| ### 1. Common Errors | |||
| ```typescript | |||
| // Dexie errors | |||
| try { | |||
|   await db.accounts.add(account); | |||
| } catch (error) { | |||
|   if (error instanceof Dexie.ConstraintError) { | |||
|     // Handle duplicate key | |||
|   } | |||
| } | |||
| 
 | |||
| // absurd-sql errors | |||
| try { | |||
|   await db.run(` | |||
|     INSERT INTO accounts (did, public_key_hex, created_at, updated_at) | |||
|     VALUES (?, ?, ?, ?) | |||
|   `, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]); | |||
| } catch (error) { | |||
|   if (error.message.includes('UNIQUE constraint failed')) { | |||
|     // Handle duplicate key | |||
|   } | |||
| } | |||
| ``` | |||
| 
 | |||
| ### 2. Transaction Recovery | |||
| ```typescript | |||
| // Dexie transaction | |||
| try { | |||
|   await db.transaction('rw', db.accounts, async () => { | |||
|     // Operations | |||
|   }); | |||
| } catch (error) { | |||
|   // Dexie automatically rolls back | |||
| } | |||
| 
 | |||
| // absurd-sql transaction | |||
| try { | |||
|   await db.exec('BEGIN TRANSACTION;'); | |||
|   // Operations | |||
|   await db.exec('COMMIT;'); | |||
| } catch (error) { | |||
|   await db.exec('ROLLBACK;'); | |||
|   throw error; | |||
| } | |||
| ``` | |||
| 
 | |||
| ## Migration Strategy | |||
| 
 | |||
| 1. **Preparation** | |||
|    - Export all Dexie data | |||
|    - Verify data integrity | |||
|    - Create SQLite schema | |||
|    - Setup indexes | |||
| 
 | |||
| 2. **Migration** | |||
|    - Import data in transactions | |||
|    - Verify each batch | |||
|    - Handle errors gracefully | |||
|    - Maintain backup | |||
| 
 | |||
| 3. **Verification** | |||
|    - Compare record counts | |||
|    - Verify data integrity | |||
|    - Test common queries | |||
|    - Validate relationships | |||
| 
 | |||
| 4. **Cleanup** | |||
|    - Remove Dexie database | |||
|    - Clear IndexedDB storage | |||
|    - Update application code | |||
|    - Remove old dependencies  | |||
| @ -0,0 +1,339 @@ | |||
| # Secure Storage Implementation Guide for TimeSafari App | |||
| 
 | |||
| ## Overview | |||
| 
 | |||
| This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on: | |||
| 
 | |||
| 1. **Platform-Specific Storage Solutions**: | |||
|    - Web: SQLite with IndexedDB backend (absurd-sql) | |||
|    - Electron: SQLite with Node.js backend | |||
|    - Native: (Planned) SQLCipher with platform-specific secure storage | |||
| 
 | |||
| 2. **Key Features**: | |||
|    - SQLite-based storage using absurd-sql for web | |||
|    - Platform-specific service factory pattern | |||
|    - Consistent API across platforms | |||
|    - Migration support from Dexie.js | |||
| 
 | |||
| ## Quick Start | |||
| 
 | |||
| ### 1. Installation | |||
| 
 | |||
| ```bash | |||
| # Core dependencies | |||
| npm install @jlongster/sql.js | |||
| npm install absurd-sql | |||
| 
 | |||
| # Platform-specific dependencies (for future native support) | |||
| npm install @capacitor/preferences | |||
| npm install @capacitor-community/biometric-auth | |||
| ``` | |||
| 
 | |||
| ### 2. Basic Usage | |||
| 
 | |||
| ```typescript | |||
| // Using the platform service | |||
| import { PlatformServiceFactory } from '../services/PlatformServiceFactory'; | |||
| 
 | |||
| // Get platform-specific service instance | |||
| const platformService = PlatformServiceFactory.getInstance(); | |||
| 
 | |||
| // Example database operations | |||
| async function example() { | |||
|   try { | |||
|     // Query example | |||
|     const result = await platformService.dbQuery( | |||
|       "SELECT * FROM accounts WHERE did = ?", | |||
|       [did] | |||
|     ); | |||
| 
 | |||
|     // Execute example | |||
|     await platformService.dbExec( | |||
|       "INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)", | |||
|       [did, publicKeyHex] | |||
|     ); | |||
| 
 | |||
|     } catch (error) { | |||
|     console.error('Database operation failed:', error); | |||
|   } | |||
| } | |||
| ``` | |||
| 
 | |||
| ### 3. Platform Detection | |||
| 
 | |||
| ```typescript | |||
| // src/services/PlatformServiceFactory.ts | |||
| export class PlatformServiceFactory { | |||
|   static getInstance(): PlatformService { | |||
|     if (process.env.ELECTRON) { | |||
|       // Electron platform | |||
|       return new ElectronPlatformService(); | |||
|     } else { | |||
|       // Web platform (default) | |||
|       return new AbsurdSqlDatabaseService(); | |||
|     } | |||
|   } | |||
| } | |||
| ``` | |||
| 
 | |||
| ### 4. Current Implementation Details | |||
| 
 | |||
| #### Web Platform (AbsurdSqlDatabaseService) | |||
| 
 | |||
| The web platform uses absurd-sql with IndexedDB backend: | |||
| 
 | |||
| ```typescript | |||
| // src/services/AbsurdSqlDatabaseService.ts | |||
| export class AbsurdSqlDatabaseService implements PlatformService { | |||
|   private static instance: AbsurdSqlDatabaseService | null = null; | |||
|   private db: AbsurdSqlDatabase | null = null; | |||
|   private initialized: boolean = false; | |||
|        | |||
|   // Singleton pattern | |||
|   static getInstance(): AbsurdSqlDatabaseService { | |||
|     if (!AbsurdSqlDatabaseService.instance) { | |||
|       AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService(); | |||
|     } | |||
|     return AbsurdSqlDatabaseService.instance; | |||
|   } | |||
| 
 | |||
|   // Database operations | |||
|   async dbQuery(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> { | |||
|     await this.waitForInitialization(); | |||
|     return this.queueOperation<QueryExecResult[]>("query", sql, params); | |||
|     } | |||
| 
 | |||
|   async dbExec(sql: string, params: unknown[] = []): Promise<void> { | |||
|     await this.waitForInitialization(); | |||
|     await this.queueOperation<void>("run", sql, params); | |||
|   } | |||
| } | |||
| ``` | |||
| 
 | |||
| Key features: | |||
| - Uses absurd-sql for SQLite in the browser | |||
| - Implements operation queuing for thread safety | |||
| - Handles initialization and connection management | |||
| - Provides consistent API across platforms | |||
| 
 | |||
| ### 5. Migration from Dexie.js | |||
| 
 | |||
| The current implementation supports gradual migration from Dexie.js: | |||
| 
 | |||
| ```typescript | |||
| // Example of dual-storage pattern | |||
| async function getAccount(did: string): Promise<Account | undefined> { | |||
|   // Try SQLite first | |||
|   const platform = PlatformServiceFactory.getInstance(); | |||
|   let account = await platform.dbQuery( | |||
|     "SELECT * FROM accounts WHERE did = ?", | |||
|     [did] | |||
|   ); | |||
| 
 | |||
|   // Fallback to Dexie if needed | |||
|   if (USE_DEXIE_DB) { | |||
|     account = await db.accounts.get(did); | |||
|   } | |||
| 
 | |||
|   return account; | |||
|    } | |||
| ``` | |||
| 
 | |||
| #### A. Modifying Code | |||
| 
 | |||
| When converting from Dexie.js to SQL-based implementation, follow these patterns: | |||
| 
 | |||
| 1. **Database Access Pattern** | |||
|    ```typescript | |||
|    // Before (Dexie) | |||
|    const result = await db.table.where("field").equals(value).first(); | |||
| 
 | |||
|    // After (SQL) | |||
|    const platform = PlatformServiceFactory.getInstance(); | |||
|    let result = await platform.dbQuery( | |||
|      "SELECT * FROM table WHERE field = ?", | |||
|      [value] | |||
|    ); | |||
|    result = databaseUtil.mapQueryResultToValues(result); | |||
|     | |||
|    // Fallback to Dexie if needed | |||
|    if (USE_DEXIE_DB) { | |||
|      result = await db.table.where("field").equals(value).first(); | |||
|    } | |||
|    ``` | |||
| 
 | |||
| 2. **Update Operations** | |||
|    ```typescript | |||
|    // Before (Dexie) | |||
|    await db.table.where("id").equals(id).modify(changes); | |||
| 
 | |||
|    // After (SQL) | |||
|    // For settings updates, use the utility methods: | |||
|    await databaseUtil.updateDefaultSettings(changes); | |||
|    // OR | |||
|    await databaseUtil.updateAccountSettings(did, changes); | |||
| 
 | |||
|    // For other tables, use direct SQL: | |||
|    const platform = PlatformServiceFactory.getInstance(); | |||
|    await platform.dbExec( | |||
|      "UPDATE table SET field1 = ?, field2 = ? WHERE id = ?", | |||
|      [changes.field1, changes.field2, id] | |||
|    ); | |||
| 
 | |||
|    // Fallback to Dexie if needed | |||
|    if (USE_DEXIE_DB) { | |||
|      await db.table.where("id").equals(id).modify(changes); | |||
|    } | |||
|    ``` | |||
| 
 | |||
| 3. **Insert Operations** | |||
|    ```typescript | |||
|    // Before (Dexie) | |||
|    await db.table.add(item); | |||
| 
 | |||
|    // After (SQL) | |||
|    const platform = PlatformServiceFactory.getInstance(); | |||
|    const columns = Object.keys(item); | |||
|    const values = Object.values(item); | |||
|    const placeholders = values.map(() => '?').join(', '); | |||
|    const sql = `INSERT INTO table (${columns.join(', ')}) VALUES (${placeholders})`; | |||
|    await platform.dbExec(sql, values); | |||
| 
 | |||
|    // Fallback to Dexie if needed | |||
|    if (USE_DEXIE_DB) { | |||
|      await db.table.add(item); | |||
|    } | |||
|    ``` | |||
| 
 | |||
| 4. **Delete Operations** | |||
|    ```typescript | |||
|    // Before (Dexie) | |||
|    await db.table.where("id").equals(id).delete(); | |||
| 
 | |||
|    // After (SQL) | |||
|    const platform = PlatformServiceFactory.getInstance(); | |||
|    await platform.dbExec("DELETE FROM table WHERE id = ?", [id]); | |||
| 
 | |||
|    // Fallback to Dexie if needed | |||
|    if (USE_DEXIE_DB) { | |||
|      await db.table.where("id").equals(id).delete(); | |||
|    } | |||
|    ``` | |||
| 
 | |||
| 5. **Result Processing** | |||
|    ```typescript | |||
|    // Before (Dexie) | |||
|    const items = await db.table.toArray(); | |||
| 
 | |||
|    // After (SQL) | |||
|    const platform = PlatformServiceFactory.getInstance(); | |||
|    let items = await platform.dbQuery("SELECT * FROM table"); | |||
|    items = databaseUtil.mapQueryResultToValues(items); | |||
| 
 | |||
|    // Fallback to Dexie if needed | |||
|    if (USE_DEXIE_DB) { | |||
|      items = await db.table.toArray(); | |||
|    } | |||
|    ``` | |||
| 
 | |||
| 6. **Using Utility Methods** | |||
| 
 | |||
| When working with settings or other common operations, use the utility methods in `db/index.ts`: | |||
| 
 | |||
| ```typescript | |||
| // Settings operations | |||
| await databaseUtil.updateDefaultSettings(settings); | |||
| await databaseUtil.updateAccountSettings(did, settings); | |||
| const settings = await databaseUtil.retrieveSettingsForDefaultAccount(); | |||
| const settings = await databaseUtil.retrieveSettingsForActiveAccount(); | |||
| 
 | |||
| // Logging operations | |||
| await databaseUtil.logToDb(message); | |||
| await databaseUtil.logConsoleAndDb(message, showInConsole); | |||
| ``` | |||
| 
 | |||
| Key Considerations: | |||
| - Always use `databaseUtil.mapQueryResultToValues()` to process SQL query results | |||
| - Use utility methods from `db/index.ts` when available instead of direct SQL | |||
| - Keep Dexie fallbacks wrapped in `if (USE_DEXIE_DB)` checks | |||
| - For queries that return results, use `let` variables to allow Dexie fallback to override | |||
| - For updates/inserts/deletes, execute both SQL and Dexie operations when `USE_DEXIE_DB` is true | |||
| 
 | |||
| Example Migration: | |||
| ```typescript | |||
| // Before (Dexie) | |||
| export async function updateSettings(settings: Settings): Promise<void> { | |||
|   await db.settings.put(settings); | |||
| } | |||
| 
 | |||
| // After (SQL) | |||
| export async function updateSettings(settings: Settings): Promise<void> { | |||
|   const platform = PlatformServiceFactory.getInstance(); | |||
|   const { sql, params } = generateUpdateStatement( | |||
|     settings, | |||
|     "settings", | |||
|     "id = ?", | |||
|     [settings.id] | |||
|   ); | |||
|   await platform.dbExec(sql, params); | |||
| } | |||
| ``` | |||
| 
 | |||
| Remember to: | |||
| - Create database access code to use the platform service, putting it in front of the Dexie version | |||
| - Instead of removing Dexie-specific code, keep it. | |||
| 
 | |||
|   - For creates & updates & deletes, the duplicate code is fine. | |||
| 
 | |||
|   - For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a check for USE_DEXIE_DB from app.ts and if | |||
| it's true then use that result instead of the SQL code's result. | |||
| 
 | |||
| - Consider data migration needs, and warn if there are any potential migration problems | |||
| 
 | |||
| ## Success Criteria | |||
| 
 | |||
| 1. **Functionality** | |||
|    - [x] Basic CRUD operations work correctly | |||
|    - [x] Platform service factory pattern implemented | |||
|    - [x] Error handling in place | |||
|    - [ ] Native platform support (planned) | |||
| 
 | |||
| 2. **Performance** | |||
|    - [x] Database operations complete within acceptable time | |||
|    - [x] Operation queuing for thread safety | |||
|    - [x] Proper initialization handling | |||
|    - [ ] Performance monitoring (planned) | |||
| 
 | |||
| 3. **Security** | |||
|    - [x] Basic data integrity | |||
|    - [ ] Encryption (planned for native platforms) | |||
|    - [ ] Secure key storage (planned) | |||
|    - [ ] Platform-specific security features (planned) | |||
| 
 | |||
| 4. **Testing** | |||
|    - [x] Basic unit tests | |||
|    - [ ] Comprehensive integration tests (planned) | |||
|    - [ ] Platform-specific tests (planned) | |||
|    - [ ] Migration tests (planned) | |||
| 
 | |||
| ## Next Steps | |||
| 
 | |||
| 1. **Native Platform Support** | |||
|    - Implement SQLCipher for iOS/Android | |||
|    - Add platform-specific secure storage | |||
|    - Implement biometric authentication | |||
| 
 | |||
| 2. **Enhanced Security** | |||
|    - Add encryption for sensitive data | |||
|    - Implement secure key storage | |||
|    - Add platform-specific security features | |||
| 
 | |||
| 3. **Testing and Monitoring** | |||
|    - Add comprehensive test coverage | |||
|    - Implement performance monitoring | |||
|    - Add error tracking and analytics | |||
| 
 | |||
| 4. **Documentation** | |||
|    - Add API documentation | |||
|    - Create migration guides | |||
|    - Document security measures | |||
| @ -0,0 +1,329 @@ | |||
| # Storage Implementation Checklist | |||
| 
 | |||
| ## Core Services | |||
| 
 | |||
| ### 1. Storage Service Layer | |||
| - [x] Create base `PlatformService` interface | |||
|   - [x] Define common methods for all platforms | |||
|   - [x] Add platform-specific method signatures | |||
|   - [x] Include error handling types | |||
|   - [x] Add migration support methods | |||
| 
 | |||
| - [x] Implement platform-specific services | |||
|   - [x] `AbsurdSqlDatabaseService` (web) | |||
|     - [x] Database initialization | |||
|     - [x] VFS setup with IndexedDB backend | |||
|     - [x] Connection management | |||
|     - [x] Operation queuing | |||
|   - [ ] `NativeSQLiteService` (iOS/Android) (planned) | |||
|     - [ ] SQLCipher integration | |||
|     - [ ] Native bridge setup | |||
|     - [ ] File system access | |||
|   - [ ] `ElectronSQLiteService` (planned) | |||
|     - [ ] Node SQLite integration | |||
|     - [ ] IPC communication | |||
|     - [ ] File system access | |||
| 
 | |||
| ### 2. Migration Services | |||
| - [x] Implement basic migration support | |||
|   - [x] Dual-storage pattern (SQLite + Dexie) | |||
|   - [x] Basic data verification | |||
|   - [ ] Rollback procedures (planned) | |||
|   - [ ] Progress tracking (planned) | |||
| - [ ] Create `MigrationUI` components (planned) | |||
|   - [ ] Progress indicators | |||
|   - [ ] Error handling | |||
|   - [ ] User notifications | |||
|   - [ ] Manual triggers | |||
| 
 | |||
| ### 3. Security Layer | |||
| - [x] Basic data integrity | |||
| - [ ] Implement `EncryptionService` (planned) | |||
|   - [ ] Key management | |||
|   - [ ] Encryption/decryption | |||
|   - [ ] Secure storage | |||
| - [ ] Add `BiometricService` (planned) | |||
|   - [ ] Platform detection | |||
|   - [ ] Authentication flow | |||
|   - [ ] Fallback mechanisms | |||
| 
 | |||
| ## Platform-Specific Implementation | |||
| 
 | |||
| ### Web Platform | |||
| - [x] Setup absurd-sql | |||
|   - [x] Install dependencies | |||
|     ```json | |||
|     { | |||
|       "@jlongster/sql.js": "^1.8.0", | |||
|       "absurd-sql": "^1.8.0" | |||
|     } | |||
|     ``` | |||
|   - [x] Configure VFS with IndexedDB backend | |||
|   - [x] Setup worker threads | |||
|   - [x] Implement operation queuing | |||
|   - [x] Configure database pragmas | |||
|    | |||
|     ```sql | |||
|     PRAGMA journal_mode=MEMORY; | |||
|     PRAGMA synchronous=NORMAL; | |||
|     PRAGMA foreign_keys=ON; | |||
|     PRAGMA busy_timeout=5000; | |||
|     ``` | |||
| 
 | |||
| - [x] Update build configuration | |||
|   - [x] Modify `vite.config.ts` | |||
|   - [x] Add worker configuration | |||
|   - [x] Update chunk splitting | |||
|   - [x] Configure asset handling | |||
| 
 | |||
| - [x] Implement IndexedDB backend | |||
|   - [x] Create database service | |||
|   - [x] Add operation queuing | |||
|   - [x] Handle initialization | |||
|   - [x] Implement atomic operations | |||
| 
 | |||
| ### iOS Platform (Planned) | |||
| - [ ] Setup SQLCipher | |||
|   - [ ] Install pod dependencies | |||
|   - [ ] Configure encryption | |||
|   - [ ] Setup keychain access | |||
|   - [ ] Implement secure storage | |||
| 
 | |||
| - [ ] Update Capacitor config | |||
|   - [ ] Modify `capacitor.config.ts` | |||
|   - [ ] Add iOS permissions | |||
|   - [ ] Configure backup | |||
|   - [ ] Setup app groups | |||
| 
 | |||
| ### Android Platform (Planned) | |||
| - [ ] Setup SQLCipher | |||
|   - [ ] Add Gradle dependencies | |||
|   - [ ] Configure encryption | |||
|   - [ ] Setup keystore | |||
|   - [ ] Implement secure storage | |||
| 
 | |||
| - [ ] Update Capacitor config | |||
|   - [ ] Modify `capacitor.config.ts` | |||
|   - [ ] Add Android permissions | |||
|   - [ ] Configure backup | |||
|   - [ ] Setup file provider | |||
| 
 | |||
| ### Electron Platform (Planned) | |||
| - [ ] Setup Node SQLite | |||
|   - [ ] Install dependencies | |||
|   - [ ] Configure IPC | |||
|   - [ ] Setup file system access | |||
|   - [ ] Implement secure storage | |||
| 
 | |||
| - [ ] Update Electron config | |||
|   - [ ] Modify `electron.config.ts` | |||
|   - [ ] Add security policies | |||
|   - [ ] Configure file access | |||
|   - [ ] Setup auto-updates | |||
| 
 | |||
| ## Data Models and Types | |||
| 
 | |||
| ### 1. Database Schema | |||
| - [x] Define tables | |||
| 
 | |||
|   ```sql | |||
|   -- Accounts table | |||
|   CREATE TABLE accounts ( | |||
|     did TEXT PRIMARY KEY, | |||
|     public_key_hex TEXT NOT NULL, | |||
|     created_at INTEGER NOT NULL, | |||
|     updated_at INTEGER NOT NULL | |||
|   ); | |||
| 
 | |||
|   -- Settings table | |||
|   CREATE TABLE settings ( | |||
|     key TEXT PRIMARY KEY, | |||
|     value TEXT NOT NULL, | |||
|     updated_at INTEGER NOT NULL | |||
|   ); | |||
| 
 | |||
|   -- Contacts table | |||
|   CREATE TABLE contacts ( | |||
|     id TEXT PRIMARY KEY, | |||
|     did TEXT NOT NULL, | |||
|     name TEXT, | |||
|     created_at INTEGER NOT NULL, | |||
|     updated_at INTEGER NOT NULL, | |||
|     FOREIGN KEY (did) REFERENCES accounts(did) | |||
|   ); | |||
| 
 | |||
|   -- Indexes for performance | |||
|   CREATE INDEX idx_accounts_created_at ON accounts(created_at); | |||
|   CREATE INDEX idx_contacts_did ON contacts(did); | |||
|   CREATE INDEX idx_settings_updated_at ON settings(updated_at); | |||
|   ``` | |||
| 
 | |||
| - [x] Create indexes | |||
| - [x] Define constraints | |||
| - [ ] Add triggers (planned) | |||
| - [ ] Setup migrations (planned) | |||
| 
 | |||
| ### 2. Type Definitions | |||
| 
 | |||
| - [x] Create interfaces | |||
|   ```typescript | |||
|   interface Account { | |||
|     did: string; | |||
|     publicKeyHex: string; | |||
|     createdAt: number; | |||
|     updatedAt: number; | |||
|   } | |||
| 
 | |||
|   interface Setting { | |||
|     key: string; | |||
|     value: string; | |||
|     updatedAt: number; | |||
|   } | |||
| 
 | |||
|   interface Contact { | |||
|     id: string; | |||
|     did: string; | |||
|     name?: string; | |||
|     createdAt: number; | |||
|     updatedAt: number; | |||
|   } | |||
|   ``` | |||
| 
 | |||
| - [x] Add validation | |||
| - [x] Create DTOs | |||
| - [x] Define enums | |||
| - [x] Add type guards | |||
| 
 | |||
| ## UI Components | |||
| 
 | |||
| ### 1. Migration UI (Planned) | |||
| - [ ] Create components | |||
|   - [ ] `MigrationProgress.vue` | |||
|   - [ ] `MigrationError.vue` | |||
|   - [ ] `MigrationSettings.vue` | |||
|   - [ ] `MigrationStatus.vue` | |||
| 
 | |||
| ### 2. Settings UI (Planned) | |||
| - [ ] Update components | |||
|   - [ ] Add storage settings | |||
|   - [ ] Add migration controls | |||
|   - [ ] Add backup options | |||
|   - [ ] Add security settings | |||
| 
 | |||
| ### 3. Error Handling UI (Planned) | |||
| - [ ] Create components | |||
|   - [ ] `StorageError.vue` | |||
|   - [ ] `QuotaExceeded.vue` | |||
|   - [ ] `MigrationFailed.vue` | |||
|   - [ ] `RecoveryOptions.vue` | |||
| 
 | |||
| ## Testing | |||
| 
 | |||
| ### 1. Unit Tests | |||
| - [x] Basic service tests | |||
|   - [x] Platform service tests | |||
|   - [x] Database operation tests | |||
|   - [ ] Security service tests (planned) | |||
|   - [ ] Platform detection tests (planned) | |||
| 
 | |||
| ### 2. Integration Tests (Planned) | |||
| - [ ] Test migrations | |||
|   - [ ] Web platform tests | |||
|   - [ ] iOS platform tests | |||
|   - [ ] Android platform tests | |||
|   - [ ] Electron platform tests | |||
| 
 | |||
| ### 3. E2E Tests (Planned) | |||
| - [ ] Test workflows | |||
|   - [ ] Account management | |||
|   - [ ] Settings management | |||
|   - [ ] Contact management | |||
|   - [ ] Migration process | |||
| 
 | |||
| ## Documentation | |||
| 
 | |||
| ### 1. Technical Documentation | |||
| - [x] Update architecture docs | |||
| - [x] Add API documentation | |||
| - [ ] Create migration guides (planned) | |||
| - [ ] Document security measures (planned) | |||
| 
 | |||
| ### 2. User Documentation (Planned) | |||
| - [ ] Update user guides | |||
| - [ ] Add troubleshooting guides | |||
| - [ ] Create FAQ | |||
| - [ ] Document new features | |||
| 
 | |||
| ## Deployment | |||
| 
 | |||
| ### 1. Build Process | |||
| - [x] Update build scripts | |||
| - [x] Add platform-specific builds | |||
| - [ ] Configure CI/CD (planned) | |||
| - [ ] Setup automated testing (planned) | |||
| 
 | |||
| ### 2. Release Process (Planned) | |||
| - [ ] Create release checklist | |||
| - [ ] Add version management | |||
| - [ ] Setup rollback procedures | |||
| - [ ] Configure monitoring | |||
| 
 | |||
| ## Monitoring and Analytics (Planned) | |||
| 
 | |||
| ### 1. Error Tracking | |||
| - [ ] Setup error logging | |||
| - [ ] Add performance monitoring | |||
| - [ ] Configure alerts | |||
| - [ ] Create dashboards | |||
| 
 | |||
| ### 2. Usage Analytics | |||
| - [ ] Add storage metrics | |||
| - [ ] Track migration success | |||
| - [ ] Monitor performance | |||
| - [ ] Collect user feedback | |||
| 
 | |||
| ## Security Audit (Planned) | |||
| 
 | |||
| ### 1. Code Review | |||
| - [ ] Review encryption | |||
| - [ ] Check access controls | |||
| - [ ] Verify data handling | |||
| - [ ] Audit dependencies | |||
| 
 | |||
| ### 2. Penetration Testing | |||
| - [ ] Test data access | |||
| - [ ] Verify encryption | |||
| - [ ] Check authentication | |||
| - [ ] Review permissions | |||
| 
 | |||
| ## Success Criteria | |||
| 
 | |||
| ### 1. Performance | |||
| - [x] Query response time < 100ms | |||
| - [x] Operation queuing for thread safety | |||
| - [x] Proper initialization handling | |||
| - [ ] Migration time < 5s per 1000 records (planned) | |||
| - [ ] Storage overhead < 10% (planned) | |||
| - [ ] Memory usage < 50MB (planned) | |||
| 
 | |||
| ### 2. Reliability | |||
| - [x] Basic data integrity | |||
| - [x] Operation queuing | |||
| - [ ] Automatic recovery (planned) | |||
| - [ ] Backup verification (planned) | |||
| - [ ] Transaction atomicity (planned) | |||
| - [ ] Data consistency (planned) | |||
| 
 | |||
| ### 3. Security | |||
| - [x] Basic data integrity | |||
| - [ ] AES-256 encryption (planned) | |||
| - [ ] Secure key storage (planned) | |||
| - [ ] Access control (planned) | |||
| - [ ] Audit logging (planned) | |||
| 
 | |||
| ### 4. User Experience | |||
| - [x] Basic database operations | |||
| - [ ] Smooth migration (planned) | |||
| - [ ] Clear error messages (planned) | |||
| - [ ] Progress indicators (planned) | |||
| - [ ] Recovery options (planned)  | |||
								
									
										File diff suppressed because it is too large
									
								
							
						
					| @ -1,29 +0,0 @@ | |||
| const { app, BrowserWindow } = require('electron'); | |||
| const path = require('path'); | |||
| 
 | |||
| function createWindow() { | |||
|   const win = new BrowserWindow({ | |||
|     width: 1200, | |||
|     height: 800, | |||
|     webPreferences: { | |||
|       nodeIntegration: true, | |||
|       contextIsolation: false | |||
|     } | |||
|   }); | |||
| 
 | |||
|   win.loadFile(path.join(__dirname, 'dist-electron/www/index.html')); | |||
| } | |||
| 
 | |||
| app.whenReady().then(createWindow); | |||
| 
 | |||
| app.on('window-all-closed', () => { | |||
|   if (process.platform !== 'darwin') { | |||
|     app.quit(); | |||
|   } | |||
| }); | |||
| 
 | |||
| app.on('activate', () => { | |||
|   if (BrowserWindow.getAllWindows().length === 0) { | |||
|     createWindow(); | |||
|   } | |||
| });  | |||
								
									
										File diff suppressed because it is too large
									
								
							
						
					| @ -0,0 +1,15 @@ | |||
| const fs = require('fs'); | |||
| const path = require('path'); | |||
| 
 | |||
| // Create public/wasm directory if it doesn't exist
 | |||
| const wasmDir = path.join(__dirname, '../public/wasm'); | |||
| if (!fs.existsSync(wasmDir)) { | |||
|   fs.mkdirSync(wasmDir, { recursive: true }); | |||
| } | |||
| 
 | |||
| // Copy the WASM file from node_modules to public/wasm
 | |||
| const sourceFile = path.join(__dirname, '../node_modules/@jlongster/sql.js/dist/sql-wasm.wasm'); | |||
| const targetFile = path.join(wasmDir, 'sql-wasm.wasm'); | |||
| 
 | |||
| fs.copyFileSync(sourceFile, targetFile); | |||
| console.log('WASM file copied successfully!');  | |||
| @ -0,0 +1,138 @@ | |||
| import migrationService from "../services/migrationService"; | |||
| import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; | |||
| import { arrayBufferToBase64 } from "@/libs/crypto"; | |||
| 
 | |||
| // Generate a random secret for the secret table
 | |||
| 
 | |||
| // It's not really secure to maintain the secret next to the user's data.
 | |||
| // However, until we have better hooks into a real wallet or reliable secure
 | |||
| // storage, we'll do this for user convenience. As they sign more records
 | |||
| // and integrate with more people, they'll value it more and want to be more
 | |||
| // secure, so we'll prompt them to take steps to back it up, properly encrypt,
 | |||
| // etc. At the beginning, we'll prompt for a password, then we'll prompt for a
 | |||
| // PWA so it's not in a browser... and then we hope to be integrated with a
 | |||
| // real wallet or something else more secure.
 | |||
| 
 | |||
| // One might ask: why encrypt at all? We figure a basic encryption is better
 | |||
| // than none. Plus, we expect to support their own password or keystore or
 | |||
| // external wallet as better signing options in the future, so it's gonna be
 | |||
| // important to have the structure where each account access might require
 | |||
| // user action.
 | |||
| 
 | |||
| // (Once upon a time we stored the secret in localStorage, but it frequently
 | |||
| // got erased, even though the IndexedDB still had the identity data. This
 | |||
| // ended up throwing lots of errors to the user... and they'd end up in a state
 | |||
| // where they couldn't take action because they couldn't unlock that identity.)
 | |||
| 
 | |||
| const randomBytes = crypto.getRandomValues(new Uint8Array(32)); | |||
| const secretBase64 = arrayBufferToBase64(randomBytes); | |||
| 
 | |||
| // Each migration can include multiple SQL statements (with semicolons)
 | |||
| const MIGRATIONS = [ | |||
|   { | |||
|     name: "001_initial", | |||
|     // see ../db/tables files for explanations of the fields
 | |||
|     sql: ` | |||
|       CREATE TABLE IF NOT EXISTS accounts ( | |||
|         id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
|         dateCreated TEXT NOT NULL, | |||
|         derivationPath TEXT, | |||
|         did TEXT NOT NULL, | |||
|         identityEncrBase64 TEXT, -- encrypted & base64-encoded | |||
|         mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded | |||
|         passkeyCredIdHex TEXT, | |||
|         publicKeyHex TEXT NOT NULL | |||
|       ); | |||
| 
 | |||
|       CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did); | |||
| 
 | |||
|       CREATE TABLE IF NOT EXISTS secret ( | |||
|         id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
|         secretBase64 TEXT NOT NULL | |||
|       ); | |||
| 
 | |||
|       INSERT INTO secret (id, secretBase64) VALUES (1, '${secretBase64}'); | |||
| 
 | |||
|       CREATE TABLE IF NOT EXISTS settings ( | |||
|         id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
|         accountDid TEXT, | |||
|         activeDid TEXT, | |||
|         apiServer TEXT, | |||
|         filterFeedByNearby BOOLEAN, | |||
|         filterFeedByVisible BOOLEAN, | |||
|         finishedOnboarding BOOLEAN, | |||
|         firstName TEXT, | |||
|         hideRegisterPromptOnNewContact BOOLEAN, | |||
|         isRegistered BOOLEAN, | |||
|         lastName TEXT, | |||
|         lastAckedOfferToUserJwtId TEXT, | |||
|         lastAckedOfferToUserProjectsJwtId TEXT, | |||
|         lastNotifiedClaimId TEXT, | |||
|         lastViewedClaimId TEXT, | |||
|         notifyingNewActivityTime TEXT, | |||
|         notifyingReminderMessage TEXT, | |||
|         notifyingReminderTime TEXT, | |||
|         partnerApiServer TEXT, | |||
|         passkeyExpirationMinutes INTEGER, | |||
|         profileImageUrl TEXT, | |||
|         searchBoxes TEXT, -- Stored as JSON string | |||
|         showContactGivesInline BOOLEAN, | |||
|         showGeneralAdvanced BOOLEAN, | |||
|         showShortcutBvc BOOLEAN, | |||
|         vapid TEXT, | |||
|         warnIfProdServer BOOLEAN, | |||
|         warnIfTestServer BOOLEAN, | |||
|         webPushServer TEXT | |||
|       ); | |||
| 
 | |||
|       CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid); | |||
| 
 | |||
|       INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}'); | |||
| 
 | |||
|       CREATE TABLE IF NOT EXISTS contacts ( | |||
|         id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
|         did TEXT NOT NULL, | |||
|         name TEXT, | |||
|         contactMethods TEXT, -- Stored as JSON string | |||
|         nextPubKeyHashB64 TEXT, | |||
|         notes TEXT, | |||
|         profileImageUrl TEXT, | |||
|         publicKeyBase64 TEXT, | |||
|         seesMe BOOLEAN, | |||
|         registered BOOLEAN | |||
|       ); | |||
| 
 | |||
|       CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did); | |||
|       CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name); | |||
| 
 | |||
|       CREATE TABLE IF NOT EXISTS logs ( | |||
|         date TEXT NOT NULL, | |||
|         message TEXT NOT NULL | |||
|       ); | |||
| 
 | |||
|       CREATE TABLE IF NOT EXISTS temp ( | |||
|         id TEXT PRIMARY KEY, | |||
|         blobB64 TEXT | |||
|       ); | |||
|       `,
 | |||
|   }, | |||
| ]; | |||
| 
 | |||
| /** | |||
|  * @param sqlExec - A function that executes a SQL statement and returns the result | |||
|  * @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations" | |||
|  */ | |||
| export async function runMigrations<T>( | |||
|   sqlExec: (sql: string) => Promise<unknown>, | |||
|   sqlQuery: (sql: string) => Promise<T>, | |||
|   extractMigrationNames: (result: T) => Set<string>, | |||
| ): Promise<void> { | |||
|   for (const migration of MIGRATIONS) { | |||
|     migrationService.registerMigration(migration); | |||
|   } | |||
|   await migrationService.runMigrations( | |||
|     sqlExec, | |||
|     sqlQuery, | |||
|     extractMigrationNames, | |||
|   ); | |||
| } | |||
| @ -0,0 +1,330 @@ | |||
| /** | |||
|  * This file is the SQL replacement of the index.ts file in the db directory. | |||
|  * That file will eventually be deleted. | |||
|  */ | |||
| 
 | |||
| import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; | |||
| import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings"; | |||
| import { logger } from "@/utils/logger"; | |||
| import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; | |||
| import { QueryExecResult } from "@/interfaces/database"; | |||
| 
 | |||
| export async function updateDefaultSettings( | |||
|   settingsChanges: Settings, | |||
| ): Promise<boolean> { | |||
|   delete settingsChanges.accountDid; // just in case
 | |||
|   // ensure there is no "id" that would override the key
 | |||
|   delete settingsChanges.id; | |||
|   try { | |||
|     const platformService = PlatformServiceFactory.getInstance(); | |||
|     const { sql, params } = generateUpdateStatement( | |||
|       settingsChanges, | |||
|       "settings", | |||
|       "id = ?", | |||
|       [MASTER_SETTINGS_KEY], | |||
|     ); | |||
|     const result = await platformService.dbExec(sql, params); | |||
|     return result.changes === 1; | |||
|   } catch (error) { | |||
|     logger.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.`, | |||
|       ); | |||
|     } | |||
|   } | |||
| } | |||
| 
 | |||
| export async function updateAccountSettings( | |||
|   accountDid: string, | |||
|   settingsChanges: Settings, | |||
| ): Promise<boolean> { | |||
|   settingsChanges.accountDid = accountDid; | |||
|   delete settingsChanges.id; // key off account, not ID
 | |||
| 
 | |||
|   const platform = PlatformServiceFactory.getInstance(); | |||
| 
 | |||
|   // First try to update existing record
 | |||
|   const { sql: updateSql, params: updateParams } = generateUpdateStatement( | |||
|     settingsChanges, | |||
|     "settings", | |||
|     "accountDid = ?", | |||
|     [accountDid], | |||
|   ); | |||
| 
 | |||
|   const updateResult = await platform.dbExec(updateSql, updateParams); | |||
| 
 | |||
|   // If no record was updated, insert a new one
 | |||
|   if (updateResult.changes === 1) { | |||
|     return true; | |||
|   } else { | |||
|     const columns = Object.keys(settingsChanges); | |||
|     const values = Object.values(settingsChanges); | |||
|     const placeholders = values.map(() => "?").join(", "); | |||
| 
 | |||
|     const insertSql = `INSERT INTO settings (${columns.join(", ")}) VALUES (${placeholders})`; | |||
|     const result = await platform.dbExec(insertSql, values); | |||
| 
 | |||
|     return result.changes === 1; | |||
|   } | |||
| } | |||
| 
 | |||
| const DEFAULT_SETTINGS: Settings = { | |||
|   id: MASTER_SETTINGS_KEY, | |||
|   activeDid: undefined, | |||
|   apiServer: DEFAULT_ENDORSER_API_SERVER, | |||
| }; | |||
| 
 | |||
| // retrieves default settings
 | |||
| export async function retrieveSettingsForDefaultAccount(): Promise<Settings> { | |||
|   const platform = PlatformServiceFactory.getInstance(); | |||
|   const sql = "SELECT * FROM settings WHERE id = ?"; | |||
|   const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]); | |||
|   if (!result) { | |||
|     return DEFAULT_SETTINGS; | |||
|   } else { | |||
|     const settings = mapColumnsToValues( | |||
|       result.columns, | |||
|       result.values, | |||
|     )[0] as Settings; | |||
|     if (settings.searchBoxes) { | |||
|       // @ts-expect-error - the searchBoxes field is a string in the DB
 | |||
|       settings.searchBoxes = JSON.parse(settings.searchBoxes); | |||
|     } | |||
|     return settings; | |||
|   } | |||
| } | |||
| 
 | |||
| /** | |||
|  * Retrieves settings for the active account, merging with default settings | |||
|  * | |||
|  * @returns Promise<Settings> Combined settings with account-specific overrides | |||
|  * @throws Will log specific errors for debugging but returns default settings on failure | |||
|  */ | |||
| export async function retrieveSettingsForActiveAccount(): Promise<Settings> { | |||
|   try { | |||
|     // Get default settings first
 | |||
|     const defaultSettings = await retrieveSettingsForDefaultAccount(); | |||
| 
 | |||
|     // If no active DID, return defaults
 | |||
|     if (!defaultSettings.activeDid) { | |||
|       logConsoleAndDb( | |||
|         "[databaseUtil] No active DID found, returning default settings", | |||
|       ); | |||
|       return defaultSettings; | |||
|     } | |||
| 
 | |||
|     // Get account-specific settings
 | |||
|     try { | |||
|       const platform = PlatformServiceFactory.getInstance(); | |||
|       const result = await platform.dbQuery( | |||
|         "SELECT * FROM settings WHERE accountDid = ?", | |||
|         [defaultSettings.activeDid], | |||
|       ); | |||
| 
 | |||
|       if (!result?.values?.length) { | |||
|         logConsoleAndDb( | |||
|           `[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`, | |||
|         ); | |||
|         return defaultSettings; | |||
|       } | |||
| 
 | |||
|       // Map and filter settings
 | |||
|       const overrideSettings = mapColumnsToValues( | |||
|         result.columns, | |||
|         result.values, | |||
|       )[0] as Settings; | |||
|       const overrideSettingsFiltered = Object.fromEntries( | |||
|         Object.entries(overrideSettings).filter(([_, v]) => v !== null), | |||
|       ); | |||
| 
 | |||
|       // Merge settings
 | |||
|       const settings = { ...defaultSettings, ...overrideSettingsFiltered }; | |||
| 
 | |||
|       // Handle searchBoxes parsing
 | |||
|       if (settings.searchBoxes) { | |||
|         try { | |||
|           // @ts-expect-error - the searchBoxes field is a string in the DB
 | |||
|           settings.searchBoxes = JSON.parse(settings.searchBoxes); | |||
|         } catch (error) { | |||
|           logConsoleAndDb( | |||
|             `[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`, | |||
|             true, | |||
|           ); | |||
|           // Reset to empty array on parse failure
 | |||
|           settings.searchBoxes = []; | |||
|         } | |||
|       } | |||
| 
 | |||
|       return settings; | |||
|     } catch (error) { | |||
|       logConsoleAndDb( | |||
|         `[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`, | |||
|         true, | |||
|       ); | |||
|       // Return defaults on error
 | |||
|       return defaultSettings; | |||
|     } | |||
|   } catch (error) { | |||
|     logConsoleAndDb( | |||
|       `[databaseUtil] Failed to retrieve default settings: ${error}`, | |||
|       true, | |||
|     ); | |||
|     // Return minimal default settings on complete failure
 | |||
|     return { | |||
|       id: MASTER_SETTINGS_KEY, | |||
|       activeDid: undefined, | |||
|       apiServer: DEFAULT_ENDORSER_API_SERVER, | |||
|     }; | |||
|   } | |||
| } | |||
| 
 | |||
| let lastCleanupDate: string | null = null; | |||
| export let memoryLogs: string[] = []; | |||
| 
 | |||
| /** | |||
|  * Logs a message to the database with proper handling of concurrent writes | |||
|  * @param message - The message to log | |||
|  * @author Matthew Raymer | |||
|  */ | |||
| export async function logToDb(message: string): Promise<void> { | |||
|   const platform = PlatformServiceFactory.getInstance(); | |||
|   const todayKey = new Date().toDateString(); | |||
|   const nowKey = new Date().toISOString(); | |||
| 
 | |||
|   try { | |||
|     memoryLogs.push(`${new Date().toISOString()} ${message}`); | |||
|     // Try to insert first, if it fails due to UNIQUE constraint, update instead
 | |||
|     await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [ | |||
|       nowKey, | |||
|       message, | |||
|     ]); | |||
| 
 | |||
|     // Clean up old logs (keep only last 7 days) - do this less frequently
 | |||
|     // Only clean up if the date is different from the last cleanup
 | |||
|     if (!lastCleanupDate || lastCleanupDate !== todayKey) { | |||
|       const sevenDaysAgo = new Date( | |||
|         new Date().getTime() - 7 * 24 * 60 * 60 * 1000, | |||
|       ); | |||
|       memoryLogs = memoryLogs.filter( | |||
|         (log) => log.split(" ")[0] > sevenDaysAgo.toDateString(), | |||
|       ); | |||
|       await platform.dbExec("DELETE FROM logs WHERE date < ?", [ | |||
|         sevenDaysAgo.toDateString(), | |||
|       ]); | |||
|       lastCleanupDate = todayKey; | |||
|     } | |||
|   } catch (error) { | |||
|     // Log to console as fallback
 | |||
|     // eslint-disable-next-line no-console
 | |||
|     console.error( | |||
|       "Error logging to database:", | |||
|       error, | |||
|       " ... for original message:", | |||
|       message, | |||
|     ); | |||
|   } | |||
| } | |||
| 
 | |||
| // similar method is in the sw_scripts/additional-scripts.js file
 | |||
| export async function logConsoleAndDb( | |||
|   message: string, | |||
|   isError = false, | |||
| ): Promise<void> { | |||
|   if (isError) { | |||
|     logger.error(`${new Date().toISOString()} ${message}`); | |||
|   } else { | |||
|     logger.log(`${new Date().toISOString()} ${message}`); | |||
|   } | |||
|   await logToDb(message); | |||
| } | |||
| 
 | |||
| /** | |||
|  * Generates an SQL INSERT statement and parameters from a model object. | |||
|  * @param model The model object containing fields to update | |||
|  * @param tableName The name of the table to update | |||
|  * @returns Object containing the SQL statement and parameters array | |||
|  */ | |||
| export function generateInsertStatement( | |||
|   model: Record<string, unknown>, | |||
|   tableName: string, | |||
| ): { sql: string; params: unknown[] } { | |||
|   const columns = Object.keys(model).filter((key) => model[key] !== undefined); | |||
|   const values = Object.values(model).filter((value) => value !== undefined); | |||
|   const placeholders = values.map(() => "?").join(", "); | |||
|   const insertSql = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`; | |||
|   return { | |||
|     sql: insertSql, | |||
|     params: values, | |||
|   }; | |||
| } | |||
| 
 | |||
| /** | |||
|  * Generates an SQL UPDATE statement and parameters from a model object. | |||
|  * @param model The model object containing fields to update | |||
|  * @param tableName The name of the table to update | |||
|  * @param whereClause The WHERE clause for the update (e.g. "id = ?") | |||
|  * @param whereParams Parameters for the WHERE clause | |||
|  * @returns Object containing the SQL statement and parameters array | |||
|  */ | |||
| export function generateUpdateStatement( | |||
|   model: Record<string, unknown>, | |||
|   tableName: string, | |||
|   whereClause: string, | |||
|   whereParams: unknown[] = [], | |||
| ): { sql: string; params: unknown[] } { | |||
|   // Filter out undefined/null values and create SET clause
 | |||
|   const setClauses: string[] = []; | |||
|   const params: unknown[] = []; | |||
| 
 | |||
|   Object.entries(model).forEach(([key, value]) => { | |||
|     if (value !== undefined) { | |||
|       setClauses.push(`${key} = ?`); | |||
|       params.push(value); | |||
|     } | |||
|   }); | |||
| 
 | |||
|   if (setClauses.length === 0) { | |||
|     throw new Error("No valid fields to update"); | |||
|   } | |||
| 
 | |||
|   const sql = `UPDATE ${tableName} SET ${setClauses.join(", ")} WHERE ${whereClause}`; | |||
| 
 | |||
|   return { | |||
|     sql, | |||
|     params: [...params, ...whereParams], | |||
|   }; | |||
| } | |||
| 
 | |||
| export function mapQueryResultToValues( | |||
|   record: QueryExecResult | undefined, | |||
| ): Array<Record<string, unknown>> { | |||
|   if (!record) { | |||
|     return []; | |||
|   } | |||
|   return mapColumnsToValues(record.columns, record.values) as Array< | |||
|     Record<string, unknown> | |||
|   >; | |||
| } | |||
| 
 | |||
| /** | |||
|  * Maps an array of column names to an array of value arrays, creating objects where each column name | |||
|  * is mapped to its corresponding value. | |||
|  * @param columns Array of column names to use as object keys | |||
|  * @param values Array of value arrays, where each inner array corresponds to one row of data | |||
|  * @returns Array of objects where each object maps column names to their corresponding values | |||
|  */ | |||
| export function mapColumnsToValues( | |||
|   columns: string[], | |||
|   values: unknown[][], | |||
| ): Array<Record<string, unknown>> { | |||
|   return values.map((row) => { | |||
|     const obj: Record<string, unknown> = {}; | |||
|     columns.forEach((column, index) => { | |||
|       obj[column] = row[index]; | |||
|     }); | |||
|     return obj; | |||
|   }); | |||
| } | |||
| @ -0,0 +1,59 @@ | |||
| import type { QueryExecResult, SqlValue } from "./database"; | |||
| 
 | |||
| declare module "@jlongster/sql.js" { | |||
|   interface SQL { | |||
|     Database: new (path: string, options?: { filename: boolean }) => AbsurdSqlDatabase; | |||
|     FS: { | |||
|       mkdir: (path: string) => void; | |||
|       mount: (fs: any, options: any, path: string) => void; | |||
|       open: (path: string, flags: string) => any; | |||
|       close: (stream: any) => void; | |||
|     }; | |||
|     register_for_idb: (fs: any) => void; | |||
|   } | |||
| 
 | |||
|   interface AbsurdSqlDatabase { | |||
|     exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>; | |||
|     run: ( | |||
|       sql: string, | |||
|       params?: unknown[], | |||
|     ) => Promise<{ changes: number; lastId?: number }>; | |||
|   } | |||
| 
 | |||
|   const initSqlJs: (options?: { | |||
|     locateFile?: (file: string) => string; | |||
|   }) => Promise<SQL>; | |||
| 
 | |||
|   export default initSqlJs; | |||
| } | |||
| 
 | |||
| declare module "absurd-sql" { | |||
|   import type { SQL } from "@jlongster/sql.js"; | |||
| 
 | |||
|   export class SQLiteFS { | |||
|     constructor(fs: any, backend: any); | |||
|   } | |||
| } | |||
| 
 | |||
| declare module "absurd-sql/dist/indexeddb-backend" { | |||
|   export default class IndexedDBBackend { | |||
|     constructor(); | |||
|   } | |||
| } | |||
| 
 | |||
| declare module "absurd-sql/dist/indexeddb-main-thread" { | |||
|   export interface SQLiteOptions { | |||
|     filename?: string; | |||
|     autoLoad?: boolean; | |||
|     debug?: boolean; | |||
|   } | |||
| 
 | |||
|   export interface SQLiteDatabase { | |||
|     exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>; | |||
|     close: () => Promise<void>; | |||
|   } | |||
| 
 | |||
|   export function initSqlJs(options?: any): Promise<any>; | |||
|   export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>; | |||
|   export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>; | |||
| }  | |||
| @ -0,0 +1,15 @@ | |||
| export type SqlValue = string | number | null | Uint8Array; | |||
| 
 | |||
| export interface QueryExecResult { | |||
|   columns: Array<string>; | |||
|   values: Array<Array<SqlValue>>; | |||
| } | |||
| 
 | |||
| export interface DatabaseService { | |||
|   initialize(): Promise<void>; | |||
|   query(sql: string, params?: unknown[]): Promise<QueryExecResult[]>; | |||
|   run( | |||
|     sql: string, | |||
|     params?: unknown[], | |||
|   ): Promise<{ changes: number; lastId?: number }>; | |||
| } | |||
| @ -1,7 +1,37 @@ | |||
| export * from "./claims"; | |||
| export * from "./claims-result"; | |||
| export * from "./common"; | |||
| export type { | |||
|   // From common.ts
 | |||
|   GenericCredWrapper, | |||
|   GenericVerifiableCredential, | |||
|   KeyMeta, | |||
|   // Exclude types that are also exported from other files
 | |||
|   // GiveVerifiableCredential,
 | |||
|   // OfferVerifiableCredential,
 | |||
|   // RegisterVerifiableCredential,
 | |||
|   // PlanSummaryRecord,
 | |||
|   // UserInfo,
 | |||
| } from "./common"; | |||
| 
 | |||
| export type { | |||
|   // From claims.ts
 | |||
|   GiveVerifiableCredential, | |||
|   OfferVerifiableCredential, | |||
|   RegisterVerifiableCredential, | |||
| } from "./claims"; | |||
| 
 | |||
| export type { | |||
|   // From claims-result.ts
 | |||
|   CreateAndSubmitClaimResult, | |||
| } from "./claims-result"; | |||
| 
 | |||
| export type { | |||
|   // From records.ts
 | |||
|   PlanSummaryRecord, | |||
| } from "./records"; | |||
| 
 | |||
| export type { | |||
|   // From user.ts
 | |||
|   UserInfo, | |||
| } from "./user"; | |||
| 
 | |||
| export * from "./limits"; | |||
| export * from "./records"; | |||
| export * from "./user"; | |||
| export * from "./deepLinks"; | |||
|  | |||
| @ -1,4 +1,16 @@ | |||
| import { initializeApp } from "./main.common"; | |||
| import { logger } from "./utils/logger"; | |||
| 
 | |||
| const platform = process.env.VITE_PLATFORM; | |||
| const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; | |||
| 
 | |||
| logger.info("[Electron] Initializing app"); | |||
| logger.info("[Electron] Platform:", { platform }); | |||
| logger.info("[Electron] PWA enabled:", { pwa_enabled }); | |||
| 
 | |||
| if (pwa_enabled) { | |||
|   logger.warn("[Electron] PWA is enabled, but not supported in electron"); | |||
| } | |||
| 
 | |||
| const app = initializeApp(); | |||
| app.mount("#app"); | |||
|  | |||
| @ -1,215 +0,0 @@ | |||
| import { createPinia } from "pinia"; | |||
| import { App as VueApp, ComponentPublicInstance, createApp } from "vue"; | |||
| import App from "./App.vue"; | |||
| import "./registerServiceWorker"; | |||
| import router from "./router"; | |||
| import axios from "axios"; | |||
| import VueAxios from "vue-axios"; | |||
| import Notifications from "notiwind"; | |||
| import "./assets/styles/tailwind.css"; | |||
| 
 | |||
| import { library } from "@fortawesome/fontawesome-svg-core"; | |||
| import { | |||
|   faArrowDown, | |||
|   faArrowLeft, | |||
|   faArrowRight, | |||
|   faArrowRotateBackward, | |||
|   faArrowUpRightFromSquare, | |||
|   faArrowUp, | |||
|   faBan, | |||
|   faBitcoinSign, | |||
|   faBurst, | |||
|   faCalendar, | |||
|   faCamera, | |||
|   faCameraRotate, | |||
|   faCaretDown, | |||
|   faChair, | |||
|   faCheck, | |||
|   faChevronDown, | |||
|   faChevronLeft, | |||
|   faChevronRight, | |||
|   faChevronUp, | |||
|   faCircle, | |||
|   faCircleCheck, | |||
|   faCircleInfo, | |||
|   faCircleQuestion, | |||
|   faCircleUser, | |||
|   faClock, | |||
|   faCoins, | |||
|   faComment, | |||
|   faCopy, | |||
|   faDollar, | |||
|   faEllipsis, | |||
|   faEllipsisVertical, | |||
|   faEnvelopeOpenText, | |||
|   faEraser, | |||
|   faEye, | |||
|   faEyeSlash, | |||
|   faFileContract, | |||
|   faFileLines, | |||
|   faFilter, | |||
|   faFloppyDisk, | |||
|   faFolderOpen, | |||
|   faForward, | |||
|   faGift, | |||
|   faGlobe, | |||
|   faHammer, | |||
|   faHand, | |||
|   faHandHoldingDollar, | |||
|   faHandHoldingHeart, | |||
|   faHouseChimney, | |||
|   faImage, | |||
|   faImagePortrait, | |||
|   faLeftRight, | |||
|   faLightbulb, | |||
|   faLink, | |||
|   faLocationDot, | |||
|   faLongArrowAltLeft, | |||
|   faLongArrowAltRight, | |||
|   faMagnifyingGlass, | |||
|   faMessage, | |||
|   faMinus, | |||
|   faPen, | |||
|   faPersonCircleCheck, | |||
|   faPersonCircleQuestion, | |||
|   faPlus, | |||
|   faQuestion, | |||
|   faQrcode, | |||
|   faRightFromBracket, | |||
|   faRotate, | |||
|   faShareNodes, | |||
|   faSpinner, | |||
|   faSquare, | |||
|   faSquareCaretDown, | |||
|   faSquareCaretUp, | |||
|   faSquarePlus, | |||
|   faTrashCan, | |||
|   faTriangleExclamation, | |||
|   faUser, | |||
|   faUsers, | |||
|   faXmark, | |||
| } from "@fortawesome/free-solid-svg-icons"; | |||
| 
 | |||
| library.add( | |||
|   faArrowDown, | |||
|   faArrowLeft, | |||
|   faArrowRight, | |||
|   faArrowRotateBackward, | |||
|   faArrowUpRightFromSquare, | |||
|   faArrowUp, | |||
|   faBan, | |||
|   faBitcoinSign, | |||
|   faBurst, | |||
|   faCalendar, | |||
|   faCamera, | |||
|   faCameraRotate, | |||
|   faCaretDown, | |||
|   faChair, | |||
|   faCheck, | |||
|   faChevronDown, | |||
|   faChevronLeft, | |||
|   faChevronRight, | |||
|   faChevronUp, | |||
|   faCircle, | |||
|   faCircleCheck, | |||
|   faCircleInfo, | |||
|   faCircleQuestion, | |||
|   faCircleUser, | |||
|   faClock, | |||
|   faCoins, | |||
|   faComment, | |||
|   faCopy, | |||
|   faDollar, | |||
|   faEllipsis, | |||
|   faEllipsisVertical, | |||
|   faEnvelopeOpenText, | |||
|   faEraser, | |||
|   faEye, | |||
|   faEyeSlash, | |||
|   faFileContract, | |||
|   faFileLines, | |||
|   faFilter, | |||
|   faFloppyDisk, | |||
|   faFolderOpen, | |||
|   faForward, | |||
|   faGift, | |||
|   faGlobe, | |||
|   faHammer, | |||
|   faHand, | |||
|   faHandHoldingDollar, | |||
|   faHandHoldingHeart, | |||
|   faHouseChimney, | |||
|   faImage, | |||
|   faImagePortrait, | |||
|   faLeftRight, | |||
|   faLightbulb, | |||
|   faLink, | |||
|   faLocationDot, | |||
|   faLongArrowAltLeft, | |||
|   faLongArrowAltRight, | |||
|   faMagnifyingGlass, | |||
|   faMessage, | |||
|   faMinus, | |||
|   faPen, | |||
|   faPersonCircleCheck, | |||
|   faPersonCircleQuestion, | |||
|   faPlus, | |||
|   faQrcode, | |||
|   faQuestion, | |||
|   faRotate, | |||
|   faRightFromBracket, | |||
|   faShareNodes, | |||
|   faSpinner, | |||
|   faSquare, | |||
|   faSquareCaretDown, | |||
|   faSquareCaretUp, | |||
|   faSquarePlus, | |||
|   faTrashCan, | |||
|   faTriangleExclamation, | |||
|   faUser, | |||
|   faUsers, | |||
|   faXmark, | |||
| ); | |||
| 
 | |||
| import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; | |||
| import Camera from "simple-vue-camera"; | |||
| import { logger } from "./utils/logger"; | |||
| 
 | |||
| // Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
 | |||
| function setupGlobalErrorHandler(app: VueApp) { | |||
|   // @ts-expect-error 'cause we cannot see why config is not defined
 | |||
|   app.config.errorHandler = ( | |||
|     err: Error, | |||
|     instance: ComponentPublicInstance | null, | |||
|     info: string, | |||
|   ) => { | |||
|     logger.error( | |||
|       "Ouch! Global Error Handler.", | |||
|       "Error:", | |||
|       err, | |||
|       "- Error toString:", | |||
|       err.toString(), | |||
|       "- Info:", | |||
|       info, | |||
|       "- Instance:", | |||
|       instance, | |||
|     ); | |||
|     // Want to show a nice notiwind notification but can't figure out how.
 | |||
|     alert( | |||
|       (err.message || "Something bad happened") + | |||
|         " - Try reloading or restarting the app.", | |||
|     ); | |||
|   }; | |||
| } | |||
| 
 | |||
| const app = createApp(App) | |||
|   .component("fa", FontAwesomeIcon) | |||
|   .component("camera", Camera) | |||
|   .use(createPinia()) | |||
|   .use(VueAxios, axios) | |||
|   .use(router) | |||
|   .use(Notifications); | |||
| 
 | |||
| setupGlobalErrorHandler(app); | |||
| 
 | |||
| app.mount("#app"); | |||
| @ -1,5 +1,37 @@ | |||
| import { initBackend } from "absurd-sql/dist/indexeddb-main-thread"; | |||
| import { initializeApp } from "./main.common"; | |||
| import "./registerServiceWorker"; // Web PWA support
 | |||
| import { logger } from "./utils/logger"; | |||
| 
 | |||
| const platform = process.env.VITE_PLATFORM; | |||
| const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; | |||
| 
 | |||
| logger.info("[Web] PWA enabled", { pwa_enabled }); | |||
| logger.info("[Web] Platform", { platform }); | |||
| 
 | |||
| // Only import service worker for web builds
 | |||
| if (platform !== "electron" && pwa_enabled) { | |||
|   import("./registerServiceWorker"); // Web PWA support
 | |||
| } | |||
| 
 | |||
| const app = initializeApp(); | |||
| 
 | |||
| function sqlInit() { | |||
|   // see https://github.com/jlongster/absurd-sql
 | |||
|   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
 | |||
|   initBackend(worker); | |||
| } | |||
| if (platform === "web" || platform === "development") { | |||
|   sqlInit(); | |||
| } else { | |||
|   logger.info("[Web] SQL not initialized for platform", { platform }); | |||
| } | |||
| 
 | |||
| app.mount("#app"); | |||
|  | |||
| @ -0,0 +1,6 @@ | |||
| import databaseService from "./services/AbsurdSqlDatabaseService"; | |||
| 
 | |||
| async function run() { | |||
|   await databaseService.initialize(); | |||
| } | |||
| run(); | |||
| @ -0,0 +1,29 @@ | |||
| import { DatabaseService } from "../interfaces/database"; | |||
| 
 | |||
| declare module "@jlongster/sql.js" { | |||
|   interface SQL { | |||
|     Database: unknown; | |||
|     FS: unknown; | |||
|     register_for_idb: (fs: unknown) => void; | |||
|   } | |||
| 
 | |||
|   function initSqlJs(config: { | |||
|     locateFile: (file: string) => string; | |||
|   }): Promise<SQL>; | |||
|   export default initSqlJs; | |||
| } | |||
| 
 | |||
| declare module "absurd-sql" { | |||
|   export class SQLiteFS { | |||
|     constructor(fs: unknown, backend: unknown); | |||
|   } | |||
| } | |||
| 
 | |||
| declare module "absurd-sql/dist/indexeddb-backend" { | |||
|   export default class IndexedDBBackend { | |||
|     constructor(); | |||
|   } | |||
| } | |||
| 
 | |||
| declare const databaseService: DatabaseService; | |||
| export default databaseService; | |||
| @ -0,0 +1,231 @@ | |||
| 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 { DatabaseService, QueryExecResult } from "../interfaces/database"; | |||
| import { logger } from "@/utils/logger"; | |||
| 
 | |||
| interface QueuedOperation { | |||
|   type: "run" | "query"; | |||
|   sql: string; | |||
|   params: unknown[]; | |||
|   resolve: (value: unknown) => void; | |||
|   reject: (reason: unknown) => void; | |||
| } | |||
| 
 | |||
| interface AbsurdSqlDatabase { | |||
|   exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>; | |||
|   run: ( | |||
|     sql: string, | |||
|     params?: unknown[], | |||
|   ) => Promise<{ changes: number; lastId?: number }>; | |||
| } | |||
| 
 | |||
| class AbsurdSqlDatabaseService implements DatabaseService { | |||
|   private static instance: AbsurdSqlDatabaseService | null = null; | |||
|   private db: AbsurdSqlDatabase | null; | |||
|   private initialized: boolean; | |||
|   private initializationPromise: Promise<void> | null = null; | |||
|   private operationQueue: Array<QueuedOperation> = []; | |||
|   private isProcessingQueue: boolean = false; | |||
| 
 | |||
|   private constructor() { | |||
|     this.db = null; | |||
|     this.initialized = false; | |||
|   } | |||
| 
 | |||
|   static getInstance(): AbsurdSqlDatabaseService { | |||
|     if (!AbsurdSqlDatabaseService.instance) { | |||
|       AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService(); | |||
|     } | |||
|     return AbsurdSqlDatabaseService.instance; | |||
|   } | |||
| 
 | |||
|   async initialize(): Promise<void> { | |||
|     // If already initialized, return immediately
 | |||
|     if (this.initialized) { | |||
|       return; | |||
|     } | |||
| 
 | |||
|     // If initialization is in progress, wait for it
 | |||
|     if (this.initializationPromise) { | |||
|       return this.initializationPromise; | |||
|     } | |||
| 
 | |||
|     // Start initialization
 | |||
|     this.initializationPromise = this._initialize(); | |||
|     try { | |||
|       await this.initializationPromise; | |||
|     } catch (error) { | |||
|       logger.error(`AbsurdSqlDatabaseService initialize method failed:`, error); | |||
|       this.initializationPromise = null; // Reset on failure
 | |||
|       throw error; | |||
|     } | |||
|   } | |||
| 
 | |||
|   private async _initialize(): Promise<void> { | |||
|     if (this.initialized) { | |||
|       return; | |||
|     } | |||
| 
 | |||
|     const SQL = await initSqlJs({ | |||
|       locateFile: (file: string) => { | |||
|         return new URL( | |||
|           `/node_modules/@jlongster/sql.js/dist/${file}`, | |||
|           import.meta.url, | |||
|         ).href; | |||
|       }, | |||
|     }); | |||
| 
 | |||
|     const sqlFS = new SQLiteFS(SQL.FS, new IndexedDBBackend()); | |||
|     SQL.register_for_idb(sqlFS); | |||
| 
 | |||
|     SQL.FS.mkdir("/sql"); | |||
|     SQL.FS.mount(sqlFS, {}, "/sql"); | |||
| 
 | |||
|     const path = "/sql/timesafari.absurd-sql"; | |||
|     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.", | |||
|       ); | |||
|     } | |||
| 
 | |||
|     // An error is thrown without this pragma: "File has invalid page size. (the first block of a new file must be written first)"
 | |||
|     await this.db.exec(`PRAGMA journal_mode=MEMORY;`); | |||
|     const sqlExec = this.db.run.bind(this.db); | |||
|     const sqlQuery = this.db.exec.bind(this.db); | |||
| 
 | |||
|     // Extract the migration names for the absurd-sql format
 | |||
|     const extractMigrationNames: (result: QueryExecResult[]) => Set<string> = ( | |||
|       result, | |||
|     ) => { | |||
|       // Even with the "select name" query, the QueryExecResult may be [] (which doesn't make sense to me).
 | |||
|       const names = result?.[0]?.values.map((row) => row[0] as string) || []; | |||
|       return new Set(names); | |||
|     }; | |||
| 
 | |||
|     // Run migrations
 | |||
|     await runMigrations(sqlExec, sqlQuery, extractMigrationNames); | |||
| 
 | |||
|     this.initialized = true; | |||
| 
 | |||
|     // Start processing the queue after initialization
 | |||
|     this.processQueue(); | |||
|   } | |||
| 
 | |||
|   private async processQueue(): Promise<void> { | |||
|     if (this.isProcessingQueue || !this.initialized || !this.db) { | |||
|       return; | |||
|     } | |||
| 
 | |||
|     this.isProcessingQueue = true; | |||
| 
 | |||
|     while (this.operationQueue.length > 0) { | |||
|       const operation = this.operationQueue.shift(); | |||
|       if (!operation) continue; | |||
| 
 | |||
|       try { | |||
|         let result: unknown; | |||
|         switch (operation.type) { | |||
|           case "run": | |||
|             result = await this.db.run(operation.sql, operation.params); | |||
|             break; | |||
|           case "query": | |||
|             result = await this.db.exec(operation.sql, operation.params); | |||
|             break; | |||
|         } | |||
|         operation.resolve(result); | |||
|       } catch (error) { | |||
|         logger.error( | |||
|           "Error while processing SQL queue:", | |||
|           error, | |||
|           " ... for sql:", | |||
|           operation.sql, | |||
|           " ... with params:", | |||
|           operation.params, | |||
|         ); | |||
|         operation.reject(error); | |||
|       } | |||
|     } | |||
| 
 | |||
|     this.isProcessingQueue = false; | |||
|   } | |||
| 
 | |||
|   private async queueOperation<R>( | |||
|     type: QueuedOperation["type"], | |||
|     sql: string, | |||
|     params: unknown[] = [], | |||
|   ): Promise<R> { | |||
|     return new Promise<R>((resolve, reject) => { | |||
|       const operation: QueuedOperation = { | |||
|         type, | |||
|         sql, | |||
|         params, | |||
|         resolve: (value: unknown) => resolve(value as R), | |||
|         reject, | |||
|       }; | |||
|       this.operationQueue.push(operation); | |||
| 
 | |||
|       // If we're already initialized, start processing the queue
 | |||
|       if (this.initialized && this.db) { | |||
|         this.processQueue(); | |||
|       } | |||
|     }); | |||
|   } | |||
| 
 | |||
|   private async waitForInitialization(): Promise<void> { | |||
|     // If we have an initialization promise, wait for it
 | |||
|     if (this.initializationPromise) { | |||
|       await this.initializationPromise; | |||
|       return; | |||
|     } | |||
| 
 | |||
|     // If not initialized and no promise, start initialization
 | |||
|     if (!this.initialized) { | |||
|       await this.initialize(); | |||
|       return; | |||
|     } | |||
| 
 | |||
|     // If initialized but no db, something went wrong
 | |||
|     if (!this.db) { | |||
|       logger.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: unknown[] = [], | |||
|   ): Promise<{ changes: number; lastId?: number }> { | |||
|     await this.waitForInitialization(); | |||
|     return this.queueOperation<{ changes: number; lastId?: number }>( | |||
|       "run", | |||
|       sql, | |||
|       params, | |||
|     ); | |||
|   } | |||
| 
 | |||
|   // Note that the resulting array may be empty if there are no results from the query
 | |||
|   async query(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> { | |||
|     await this.waitForInitialization(); | |||
|     return this.queueOperation<QueryExecResult[]>("query", sql, params); | |||
|   } | |||
| } | |||
| 
 | |||
| // Create a singleton instance
 | |||
| const databaseService = AbsurdSqlDatabaseService.getInstance(); | |||
| 
 | |||
| export default databaseService; | |||
| @ -0,0 +1,60 @@ | |||
| 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; | |||
|   } | |||
| 
 | |||
|   registerMigration(migration: Migration) { | |||
|     this.migrations.push(migration); | |||
|   } | |||
| 
 | |||
|   /** | |||
|    * @param sqlExec - A function that executes a SQL statement and returns some update result | |||
|    * @param sqlQuery - A function that executes a SQL query and returns the result in some format | |||
|    * @param extractMigrationNames - A function that extracts the names (string array) from a "select name from migrations" query | |||
|    */ | |||
|   async runMigrations<T>( | |||
|     // note that this does not take parameters because the Capacitor SQLite 'execute' is different
 | |||
|     sqlExec: (sql: string) => Promise<unknown>, | |||
|     sqlQuery: (sql: string) => Promise<T>, | |||
|     extractMigrationNames: (result: T) => Set<string>, | |||
|   ): 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 result1: T = await sqlQuery("SELECT name FROM migrations;"); | |||
|     const executedMigrations = extractMigrationNames(result1); | |||
| 
 | |||
|     // Run pending migrations in order
 | |||
|     for (const migration of this.migrations) { | |||
|       if (!executedMigrations.has(migration.name)) { | |||
|         await sqlExec(migration.sql); | |||
| 
 | |||
|         await sqlExec( | |||
|           `INSERT INTO migrations (name) VALUES ('${migration.name}')`, | |||
|         ); | |||
|       } | |||
|     } | |||
|   } | |||
| } | |||
| 
 | |||
| export default MigrationService.getInstance(); | |||
| @ -0,0 +1,45 @@ | |||
| declare module 'absurd-sql/dist/indexeddb-backend' { | |||
|   export default class IndexedDBBackend { | |||
|     constructor(options?: { | |||
|       dbName?: string; | |||
|       storeName?: string; | |||
|       onReady?: () => void; | |||
|       onError?: (error: Error) => void; | |||
|     }); | |||
|     init(): Promise<void>; | |||
|     exec(sql: string, params?: any[]): Promise<any>; | |||
|     close(): Promise<void>; | |||
|   } | |||
| } | |||
| 
 | |||
| declare module 'absurd-sql/dist/indexeddb-main-thread' { | |||
|   export function initBackend(worker: Worker): Promise<void>; | |||
| 
 | |||
|   export default class IndexedDBMainThread { | |||
|     constructor(options?: { | |||
|       dbName?: string; | |||
|       storeName?: string; | |||
|       onReady?: () => void; | |||
|       onError?: (error: Error) => void; | |||
|     }); | |||
|     init(): Promise<void>; | |||
|     exec(sql: string, params?: any[]): Promise<any>; | |||
|     close(): Promise<void>; | |||
|   } | |||
| } | |||
| 
 | |||
| declare module 'absurd-sql' { | |||
|   export class SQLiteFS { | |||
|     constructor(fs: unknown, backend: IndexedDBBackend); | |||
|     init(): Promise<void>; | |||
|     close(): Promise<void>; | |||
|     exec(sql: string, params?: any[]): Promise<any>; | |||
|     prepare(sql: string): Promise<any>; | |||
|     run(sql: string, params?: any[]): Promise<any>; | |||
|     get(sql: string, params?: any[]): Promise<any>; | |||
|     all(sql: string, params?: any[]): Promise<any[]>; | |||
|   } | |||
| 
 | |||
|   export * from 'absurd-sql/dist/indexeddb-backend'; | |||
|   export * from 'absurd-sql/dist/indexeddb-main-thread'; | |||
| }  | |||
| @ -0,0 +1,36 @@ | |||
| import type { QueryExecResult, SqlValue } from "./database"; | |||
| 
 | |||
| declare module '@jlongster/sql.js' { | |||
|   interface SQL { | |||
|     Database: new (path: string, options?: { filename: boolean }) => Database; | |||
|     FS: { | |||
|       mkdir: (path: string) => void; | |||
|       mount: (fs: any, options: any, path: string) => void; | |||
|       open: (path: string, flags: string) => any; | |||
|       close: (stream: any) => void; | |||
|     }; | |||
|     register_for_idb: (fs: any) => void; | |||
|   } | |||
| 
 | |||
|   interface Database { | |||
|     exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>; | |||
|     run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>; | |||
|     get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>; | |||
|     all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>; | |||
|     prepare: (sql: string) => Promise<Statement>; | |||
|     close: () => void; | |||
|   } | |||
| 
 | |||
|   interface Statement { | |||
|     run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>; | |||
|     get: (params?: unknown[]) => Promise<SqlValue[]>; | |||
|     all: (params?: unknown[]) => Promise<SqlValue[][]>; | |||
|     finalize: () => void; | |||
|   } | |||
| 
 | |||
|   const initSqlJs: (options?: { | |||
|     locateFile?: (file: string) => string; | |||
|   }) => Promise<SQL>; | |||
| 
 | |||
|   export default initSqlJs; | |||
| }  | |||
| @ -0,0 +1,67 @@ | |||
| import type { QueryExecResult, SqlValue } from "./database"; | |||
| 
 | |||
| declare module '@jlongster/sql.js' { | |||
|   interface SQL { | |||
|     Database: new (path: string, options?: { filename: boolean }) => Database; | |||
|     FS: { | |||
|       mkdir: (path: string) => void; | |||
|       mount: (fs: any, options: any, path: string) => void; | |||
|       open: (path: string, flags: string) => any; | |||
|       close: (stream: any) => void; | |||
|     }; | |||
|     register_for_idb: (fs: any) => void; | |||
|   } | |||
| 
 | |||
|   interface Database { | |||
|     exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>; | |||
|     run: (sql: string, params?: unknown[]) => Promise<{ changes: number; lastId?: number }>; | |||
|     get: (sql: string, params?: unknown[]) => Promise<SqlValue[]>; | |||
|     all: (sql: string, params?: unknown[]) => Promise<SqlValue[][]>; | |||
|     prepare: (sql: string) => Promise<Statement>; | |||
|     close: () => void; | |||
|   } | |||
| 
 | |||
|   interface Statement { | |||
|     run: (params?: unknown[]) => Promise<{ changes: number; lastId?: number }>; | |||
|     get: (params?: unknown[]) => Promise<SqlValue[]>; | |||
|     all: (params?: unknown[]) => Promise<SqlValue[][]>; | |||
|     finalize: () => void; | |||
|   } | |||
| 
 | |||
|   const initSqlJs: (options?: { | |||
|     locateFile?: (file: string) => string; | |||
|   }) => Promise<SQL>; | |||
| 
 | |||
|   export default initSqlJs; | |||
| } | |||
| 
 | |||
| declare module 'absurd-sql' { | |||
|   import type { SQL } from '@jlongster/sql.js'; | |||
|   export class SQLiteFS { | |||
|     constructor(fs: any, backend: any); | |||
|   } | |||
| } | |||
| 
 | |||
| declare module 'absurd-sql/dist/indexeddb-backend' { | |||
|   export default class IndexedDBBackend { | |||
|     constructor(); | |||
|   } | |||
| } | |||
| 
 | |||
| declare module 'absurd-sql/dist/indexeddb-main-thread' { | |||
|   import type { QueryExecResult } from './database'; | |||
|   export interface SQLiteOptions { | |||
|     filename?: string; | |||
|     autoLoad?: boolean; | |||
|     debug?: boolean; | |||
|   } | |||
| 
 | |||
|   export interface SQLiteDatabase { | |||
|     exec: (sql: string, params?: unknown[]) => Promise<QueryExecResult[]>; | |||
|     close: () => Promise<void>; | |||
|   } | |||
| 
 | |||
|   export function initSqlJs(options?: any): Promise<any>; | |||
|   export function createDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>; | |||
|   export function openDatabase(options?: SQLiteOptions): Promise<SQLiteDatabase>; | |||
| }  | |||
| @ -0,0 +1,57 @@ | |||
| /** | |||
|  * Type definitions for @jlongster/sql.js | |||
|  * @author Matthew Raymer | |||
|  * @description TypeScript declaration file for the SQL.js WASM module with filesystem support | |||
|  */ | |||
| 
 | |||
| declare module '@jlongster/sql.js' { | |||
|   export interface FileSystem { | |||
|     mkdir(path: string): void; | |||
|     mount(fs: any, opts: any, mountpoint: string): void; | |||
|     open(path: string, flags: string): FileStream; | |||
|     close(stream: FileStream): void; | |||
|   } | |||
| 
 | |||
|   export interface FileStream { | |||
|     node: { | |||
|       contents: { | |||
|         readIfFallback(): Promise<void>; | |||
|       }; | |||
|     }; | |||
|   } | |||
| 
 | |||
|   export interface Database { | |||
|     exec(sql: string, params?: any[]): Promise<QueryExecResult[]>; | |||
|     prepare(sql: string): Statement; | |||
|     run(sql: string, params?: any[]): Promise<{ changes: number; lastId?: number }>; | |||
|     close(): void; | |||
|   } | |||
| 
 | |||
|   export interface QueryExecResult { | |||
|     columns: string[]; | |||
|     values: any[][]; | |||
|   } | |||
| 
 | |||
|   export interface Statement { | |||
|     bind(params: any[]): void; | |||
|     step(): boolean; | |||
|     get(): any[]; | |||
|     getColumnNames(): string[]; | |||
|     reset(): void; | |||
|     free(): void; | |||
|   } | |||
| 
 | |||
|   export interface InitSqlJsStatic { | |||
|     (config?: { | |||
|       locateFile?: (file: string) => string; | |||
|       wasmBinary?: ArrayBuffer; | |||
|     }): Promise<{ | |||
|       Database: new (path?: string | Uint8Array, opts?: { filename?: boolean }) => Database; | |||
|       FS: FileSystem; | |||
|       register_for_idb: (fs: any) => void; | |||
|     }>; | |||
|   } | |||
| 
 | |||
|   const initSqlJs: InitSqlJsStatic; | |||
|   export default initSqlJs; | |||
| }  | |||
| @ -0,0 +1,2 @@ | |||
| // Empty module to satisfy Node.js built-in module imports
 | |||
| export default {}; | |||
| @ -0,0 +1,17 @@ | |||
| // Minimal crypto module implementation for browser using Web Crypto API
 | |||
| const crypto = { | |||
|   ...window.crypto, | |||
|   // Add any Node.js crypto methods that might be needed
 | |||
|   randomBytes: (size) => { | |||
|     const buffer = new Uint8Array(size); | |||
|     window.crypto.getRandomValues(buffer); | |||
|     return buffer; | |||
|   }, | |||
|   createHash: () => ({ | |||
|     update: () => ({ | |||
|       digest: () => new Uint8Array(32), // Return empty hash
 | |||
|     }), | |||
|   }), | |||
| }; | |||
| 
 | |||
| export default crypto; | |||
| @ -0,0 +1,18 @@ | |||
| // Minimal fs module implementation for browser
 | |||
| const fs = { | |||
|   readFileSync: () => { | |||
|     throw new Error("fs.readFileSync is not supported in browser"); | |||
|   }, | |||
|   writeFileSync: () => { | |||
|     throw new Error("fs.writeFileSync is not supported in browser"); | |||
|   }, | |||
|   existsSync: () => false, | |||
|   mkdirSync: () => {}, | |||
|   readdirSync: () => [], | |||
|   statSync: () => ({ | |||
|     isDirectory: () => false, | |||
|     isFile: () => false, | |||
|   }), | |||
| }; | |||
| 
 | |||
| export default fs; | |||
| @ -0,0 +1,13 @@ | |||
| // Minimal path module implementation for browser
 | |||
| const path = { | |||
|   resolve: (...parts) => parts.join("/"), | |||
|   join: (...parts) => parts.join("/"), | |||
|   dirname: (p) => p.split("/").slice(0, -1).join("/"), | |||
|   basename: (p) => p.split("/").pop(), | |||
|   extname: (p) => { | |||
|     const parts = p.split("."); | |||
|     return parts.length > 1 ? "." + parts.pop() : ""; | |||
|   }, | |||
| }; | |||
| 
 | |||
| export default path; | |||
Some files were not shown because too many files changed in this diff
					Loading…
					
					
				
		Reference in new issue