Compare commits
6 Commits
master
...
sql-absurd
Author | SHA1 | Date |
---|---|---|
|
d9ce884513 | 3 months ago |
|
a1a1543ae1 | 3 months ago |
|
93591a5815 | 3 months ago |
|
b30c4c8b30 | 3 months ago |
|
1f9db0ba94 | 3 months ago |
|
bdc2d71d3c | 3 months ago |
43 changed files with 6972 additions and 2266 deletions
@ -0,0 +1,172 @@ |
|||||
|
--- |
||||
|
description: |
||||
|
globs: |
||||
|
alwaysApply: true |
||||
|
--- |
||||
|
# @capacitor-community/sqlite MDC Ruleset |
||||
|
|
||||
|
## Project Overview |
||||
|
This ruleset is for the `@capacitor-community/sqlite` plugin, a Capacitor community plugin that provides native and Electron SQLite database functionality with encryption support. |
||||
|
|
||||
|
## Key Features |
||||
|
- Native SQLite database support for iOS, Android, and Electron |
||||
|
- Database encryption support using SQLCipher (Native) and better-sqlite3-multiple-ciphers (Electron) |
||||
|
- Biometric authentication support |
||||
|
- Cross-platform database operations |
||||
|
- JSON import/export capabilities |
||||
|
- Database migration support |
||||
|
- Sync table functionality |
||||
|
|
||||
|
## Platform Support Matrix |
||||
|
|
||||
|
### Core Database Operations |
||||
|
| Operation | Android | iOS | Electron | Web | |
||||
|
|-----------|---------|-----|----------|-----| |
||||
|
| Create Connection (RW) | ✅ | ✅ | ✅ | ✅ | |
||||
|
| Create Connection (RO) | ✅ | ✅ | ✅ | ❌ | |
||||
|
| Open DB (non-encrypted) | ✅ | ✅ | ✅ | ✅ | |
||||
|
| Open DB (encrypted) | ✅ | ✅ | ✅ | ❌ | |
||||
|
| Execute/Query | ✅ | ✅ | ✅ | ✅ | |
||||
|
| Import/Export JSON | ✅ | ✅ | ✅ | ✅ | |
||||
|
|
||||
|
### Security Features |
||||
|
| Feature | Android | iOS | Electron | Web | |
||||
|
|---------|---------|-----|----------|-----| |
||||
|
| Encryption | ✅ | ✅ | ✅ | ❌ | |
||||
|
| Biometric Auth | ✅ | ✅ | ✅ | ❌ | |
||||
|
| Secret Management | ✅ | ✅ | ✅ | ❌ | |
||||
|
|
||||
|
## Configuration Requirements |
||||
|
|
||||
|
### Base Configuration |
||||
|
```typescript |
||||
|
// capacitor.config.ts |
||||
|
{ |
||||
|
plugins: { |
||||
|
CapacitorSQLite: { |
||||
|
iosDatabaseLocation: 'Library/CapacitorDatabase', |
||||
|
iosIsEncryption: true, |
||||
|
iosKeychainPrefix: 'your-app-prefix', |
||||
|
androidIsEncryption: true, |
||||
|
electronIsEncryption: true |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Platform-Specific Requirements |
||||
|
|
||||
|
#### Android |
||||
|
- Minimum SDK: 23 |
||||
|
- Target SDK: 35 |
||||
|
- Required Gradle JDK: 21 |
||||
|
- Required Android Gradle Plugin: 8.7.2 |
||||
|
- Required manifest settings for backup prevention |
||||
|
- Required data extraction rules |
||||
|
|
||||
|
#### iOS |
||||
|
- No additional configuration needed beyond base setup |
||||
|
- Supports biometric authentication |
||||
|
- Uses keychain for encryption |
||||
|
|
||||
|
#### Electron |
||||
|
Required dependencies: |
||||
|
```json |
||||
|
{ |
||||
|
"dependencies": { |
||||
|
"better-sqlite3-multiple-ciphers": "latest", |
||||
|
"electron-json-storage": "latest", |
||||
|
"jszip": "latest", |
||||
|
"node-fetch": "2.6.7", |
||||
|
"crypto": "latest", |
||||
|
"crypto-js": "latest" |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
#### Web |
||||
|
- Requires `sql.js` and `jeep-sqlite` |
||||
|
- Manual copy of `sql-wasm.wasm` to assets folder |
||||
|
- Framework-specific asset placement: |
||||
|
- Angular: `src/assets/` |
||||
|
- Vue/React: `public/assets/` |
||||
|
|
||||
|
## Best Practices |
||||
|
|
||||
|
### Database Operations |
||||
|
1. Always close connections after use |
||||
|
2. Use transactions for multiple operations |
||||
|
3. Implement proper error handling |
||||
|
4. Use prepared statements for queries |
||||
|
5. Implement proper database versioning |
||||
|
|
||||
|
### Security |
||||
|
1. Always use encryption for sensitive data |
||||
|
2. Implement proper secret management |
||||
|
3. Use biometric authentication when available |
||||
|
4. Follow platform-specific security guidelines |
||||
|
|
||||
|
### Performance |
||||
|
1. Use appropriate indexes |
||||
|
2. Implement connection pooling |
||||
|
3. Use transactions for bulk operations |
||||
|
4. Implement proper database cleanup |
||||
|
|
||||
|
## Common Issues and Solutions |
||||
|
|
||||
|
### Android |
||||
|
- Build data properties conflict: Add to `app/build.gradle`: |
||||
|
```gradle |
||||
|
packagingOptions { |
||||
|
exclude 'build-data.properties' |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
### Electron |
||||
|
- Node-fetch version must be ≤2.6.7 |
||||
|
- For Capacitor Electron v5: |
||||
|
- Use Electron@25.8.4 |
||||
|
- Add `"skipLibCheck": true` to tsconfig.json |
||||
|
|
||||
|
### Web |
||||
|
- Ensure proper WASM file placement |
||||
|
- Handle browser compatibility |
||||
|
- Implement proper fallbacks |
||||
|
|
||||
|
## Version Compatibility |
||||
|
- Requires Node.js ≥16.0.0 |
||||
|
- Compatible with Capacitor ≥7.0.0 |
||||
|
- Supports TypeScript 4.1.5+ |
||||
|
|
||||
|
## Testing Requirements |
||||
|
- Unit tests for database operations |
||||
|
- Platform-specific integration tests |
||||
|
- Encryption/decryption tests |
||||
|
- Biometric authentication tests |
||||
|
- Migration tests |
||||
|
- Sync functionality tests |
||||
|
|
||||
|
## Documentation |
||||
|
- API Documentation: `/docs/API.md` |
||||
|
- Connection API: `/docs/APIConnection.md` |
||||
|
- DB Connection API: `/docs/APIDBConnection.md` |
||||
|
- Release Notes: `/docs/info_releases.md` |
||||
|
- Changelog: `CHANGELOG.md` |
||||
|
|
||||
|
## Contributing Guidelines |
||||
|
- Follow Ionic coding standards |
||||
|
- Use provided linting and formatting tools |
||||
|
- Maintain platform compatibility |
||||
|
- Update documentation |
||||
|
- Add appropriate tests |
||||
|
- Follow semantic versioning |
||||
|
|
||||
|
## Maintenance |
||||
|
- Regular security updates |
||||
|
- Platform compatibility checks |
||||
|
- Performance optimization |
||||
|
- Documentation updates |
||||
|
- Dependency updates |
||||
|
|
||||
|
## License |
||||
|
MIT License - See LICENSE file for details |
File diff suppressed because it is too large
File diff suppressed because it is too large
@ -1,4 +1,15 @@ |
|||||
import { initializeApp } from "./main.common"; |
import { initializeApp } from "./main.common"; |
||||
|
|
||||
const app = initializeApp(); |
// Initialize and mount the app
|
||||
app.mount("#app"); |
initializeApp().then((app) => { |
||||
|
app.mount("#app"); |
||||
|
}).catch((error) => { |
||||
|
console.error("Failed to initialize app:", error); |
||||
|
document.body.innerHTML = ` |
||||
|
<div style="color: red; padding: 20px; font-family: sans-serif;"> |
||||
|
<h1>Failed to initialize app</h1> |
||||
|
<p>${error instanceof Error ? error.message : "Unknown error"}</p> |
||||
|
<p>Please try restarting the app or contact support if the problem persists.</p> |
||||
|
</div> |
||||
|
`;
|
||||
|
}); |
||||
|
@ -1,4 +1,15 @@ |
|||||
import { initializeApp } from "./main.common"; |
import { initializeApp } from "./main.common"; |
||||
|
|
||||
const app = initializeApp(); |
// Initialize and mount the app
|
||||
app.mount("#app"); |
initializeApp().then((app) => { |
||||
|
app.mount("#app"); |
||||
|
}).catch((error) => { |
||||
|
console.error("Failed to initialize app:", error); |
||||
|
document.body.innerHTML = ` |
||||
|
<div style="color: red; padding: 20px; font-family: sans-serif;"> |
||||
|
<h1>Failed to initialize app</h1> |
||||
|
<p>${error instanceof Error ? error.message : "Unknown error"}</p> |
||||
|
<p>Please try restarting the app or contact support if the problem persists.</p> |
||||
|
</div> |
||||
|
`;
|
||||
|
}); |
||||
|
@ -0,0 +1,370 @@ |
|||||
|
import { |
||||
|
PlatformService, |
||||
|
PlatformCapabilities, |
||||
|
SQLiteOperations, |
||||
|
SQLiteConfig, |
||||
|
PreparedStatement, |
||||
|
SQLiteResult, |
||||
|
ImageResult, |
||||
|
} from "./PlatformService"; |
||||
|
import { BaseSQLiteService } from "./sqlite/BaseSQLiteService"; |
||||
|
import { app } from "electron"; |
||||
|
import { dialog } from "electron"; |
||||
|
import fs from "fs"; |
||||
|
import path from "path"; |
||||
|
import sqlite3 from "sqlite3"; |
||||
|
import { open, Database } from "sqlite"; |
||||
|
import { logger } from "../utils/logger"; |
||||
|
import { Settings } from "../db/tables/settings"; |
||||
|
import { Account } from "../db/tables/accounts"; |
||||
|
import { Contact } from "../db/tables/contacts"; |
||||
|
import { db } from "../db"; |
||||
|
import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; |
||||
|
import { accountsDBPromise } from "../db"; |
||||
|
import { accessToken } from "../libs/crypto"; |
||||
|
import { getPlanFromCache as getPlanFromCacheImpl } from "../libs/endorserServer"; |
||||
|
import { PlanSummaryRecord } from "../interfaces/records"; |
||||
|
import { Axios } from "axios"; |
||||
|
|
||||
|
interface SQLiteDatabase extends Database { |
||||
|
changes: number; |
||||
|
} |
||||
|
|
||||
|
// Create Promise-based versions of fs functions
|
||||
|
const readFileAsync = (filePath: string, encoding: BufferEncoding): Promise<string> => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
fs.readFile(filePath, { encoding }, (err: NodeJS.ErrnoException | null, data: string) => { |
||||
|
if (err) reject(err); |
||||
|
else resolve(data); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const readFileBufferAsync = (filePath: string): Promise<Buffer> => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
fs.readFile(filePath, (err: NodeJS.ErrnoException | null, data: Buffer) => { |
||||
|
if (err) reject(err); |
||||
|
else resolve(data); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const writeFileAsync = (filePath: string, data: string, encoding: BufferEncoding): Promise<void> => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
fs.writeFile(filePath, data, { encoding }, (err: NodeJS.ErrnoException | null) => { |
||||
|
if (err) reject(err); |
||||
|
else resolve(); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const unlinkAsync = (filePath: string): Promise<void> => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
fs.unlink(filePath, (err: NodeJS.ErrnoException | null) => { |
||||
|
if (err) reject(err); |
||||
|
else resolve(); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const readdirAsync = (dirPath: string): Promise<string[]> => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
fs.readdir(dirPath, (err: NodeJS.ErrnoException | null, files: string[]) => { |
||||
|
if (err) reject(err); |
||||
|
else resolve(files); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
const statAsync = (filePath: string): Promise<fs.Stats> => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
fs.stat(filePath, (err: NodeJS.ErrnoException | null, stats: fs.Stats) => { |
||||
|
if (err) reject(err); |
||||
|
else resolve(stats); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* SQLite implementation for Electron using native sqlite3 |
||||
|
*/ |
||||
|
class ElectronSQLiteService extends BaseSQLiteService { |
||||
|
private db: SQLiteDatabase | null = null; |
||||
|
private config: SQLiteConfig | null = null; |
||||
|
|
||||
|
async initialize(config: SQLiteConfig): Promise<void> { |
||||
|
if (this.initialized) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
this.config = config; |
||||
|
const dbPath = path.join(app.getPath("userData"), `${config.name}.db`); |
||||
|
|
||||
|
this.db = await open({ |
||||
|
filename: dbPath, |
||||
|
driver: sqlite3.Database, |
||||
|
}); |
||||
|
|
||||
|
// Configure database settings
|
||||
|
if (config.useWAL) { |
||||
|
await this.execute("PRAGMA journal_mode = WAL"); |
||||
|
this.stats.walMode = true; |
||||
|
} |
||||
|
|
||||
|
// Set other pragmas for performance
|
||||
|
await this.execute("PRAGMA synchronous = NORMAL"); |
||||
|
await this.execute("PRAGMA temp_store = MEMORY"); |
||||
|
await this.execute("PRAGMA cache_size = -2000"); // Use 2MB of cache
|
||||
|
|
||||
|
this.initialized = true; |
||||
|
await this.updateStats(); |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to initialize Electron SQLite:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async close(): Promise<void> { |
||||
|
if (!this.initialized || !this.db) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
await this.db.close(); |
||||
|
this.db = null; |
||||
|
this.initialized = false; |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to close Electron SQLite connection:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected async _executeQuery<T>( |
||||
|
sql: string, |
||||
|
params: unknown[] = [], |
||||
|
operation: "query" | "execute" = "query", |
||||
|
): Promise<SQLiteResult<T>> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
if (operation === "query") { |
||||
|
const rows = await this.db.all<T[]>(sql, params); |
||||
|
const result = await this.db.run("SELECT last_insert_rowid() as id"); |
||||
|
return { |
||||
|
rows, |
||||
|
rowsAffected: this.db.changes, |
||||
|
lastInsertId: result.lastID, |
||||
|
executionTime: 0, // Will be set by base class
|
||||
|
}; |
||||
|
} else { |
||||
|
const result = await this.db.run(sql, params); |
||||
|
return { |
||||
|
rows: [], |
||||
|
rowsAffected: this.db.changes, |
||||
|
lastInsertId: result.lastID, |
||||
|
executionTime: 0, // Will be set by base class
|
||||
|
}; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("Electron SQLite query failed:", { |
||||
|
sql, |
||||
|
params, |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
}); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected async _beginTransaction(): Promise<void> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
await this.db.run("BEGIN TRANSACTION"); |
||||
|
} |
||||
|
|
||||
|
protected async _commitTransaction(): Promise<void> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
await this.db.run("COMMIT"); |
||||
|
} |
||||
|
|
||||
|
protected async _rollbackTransaction(): Promise<void> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
await this.db.run("ROLLBACK"); |
||||
|
} |
||||
|
|
||||
|
protected async _prepareStatement<T>( |
||||
|
sql: string, |
||||
|
): Promise<PreparedStatement<T>> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
const stmt = await this.db.prepare(sql); |
||||
|
return { |
||||
|
execute: async (params: unknown[] = []) => { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
const rows = await stmt.all<T>(params); |
||||
|
return { |
||||
|
rows, |
||||
|
rowsAffected: this.db.changes, |
||||
|
lastInsertId: (await this.db.run("SELECT last_insert_rowid() as id")) |
||||
|
.lastID, |
||||
|
executionTime: 0, // Will be set by base class
|
||||
|
}; |
||||
|
}, |
||||
|
finalize: async () => { |
||||
|
await stmt.finalize(); |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
protected async _finalizeStatement(_sql: string): Promise<void> { |
||||
|
// Statements are finalized when the PreparedStatement is finalized
|
||||
|
} |
||||
|
|
||||
|
async getDatabaseSize(): Promise<number> { |
||||
|
if (!this.db || !this.config) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const dbPath = path.join(app.getPath("userData"), `${this.config.name}.db`); |
||||
|
const stats = await statAsync(dbPath); |
||||
|
return stats.size; |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to get database size:", error); |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Only import Electron-specific code in Electron environment
|
||||
|
let ElectronPlatformServiceImpl: typeof import("./platforms/ElectronPlatformService").ElectronPlatformService; |
||||
|
|
||||
|
async function initializeElectronPlatformService() { |
||||
|
if (process.env.ELECTRON) { |
||||
|
// Dynamic import for Electron environment
|
||||
|
const { ElectronPlatformService } = await import("./platforms/ElectronPlatformService"); |
||||
|
ElectronPlatformServiceImpl = ElectronPlatformService; |
||||
|
} else { |
||||
|
// Stub implementation for non-Electron environments
|
||||
|
class StubElectronPlatformService implements PlatformService { |
||||
|
#sqliteService: SQLiteOperations | null = null; |
||||
|
|
||||
|
getCapabilities(): PlatformCapabilities { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async getSQLite(): Promise<SQLiteOperations> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async readFile(path: string): Promise<string> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async writeFile(path: string, content: string): Promise<void> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async deleteFile(path: string): Promise<void> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async listFiles(directory: string): Promise<string[]> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async takePicture(): Promise<ImageResult> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async pickImage(): Promise<ImageResult> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async handleDeepLink(url: string): Promise<void> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async getAccounts(): Promise<Account[]> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async getAccount(did: string): Promise<Account | undefined> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async addAccount(account: Account): Promise<void> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async getContacts(): Promise<Contact[]> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async getAllContacts(): Promise<Contact[]> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async updateMasterSettings(settingsChanges: Partial<Settings>): Promise<void> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async getActiveAccountSettings(): Promise<Settings> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async updateAccountSettings(accountDid: string, settingsChanges: Partial<Settings>): Promise<void> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async getHeaders(did?: string): Promise<Record<string, string>> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
async getPlanFromCache( |
||||
|
handleId: string | undefined, |
||||
|
axios: Axios, |
||||
|
apiServer: string, |
||||
|
requesterDid?: string, |
||||
|
): Promise<PlanSummaryRecord | undefined> { |
||||
|
throw new Error("Electron platform service is not available in this environment"); |
||||
|
} |
||||
|
|
||||
|
isCapacitor(): boolean { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
isElectron(): boolean { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
isPyWebView(): boolean { |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
isWeb(): boolean { |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
ElectronPlatformServiceImpl = StubElectronPlatformService; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Initialize the service
|
||||
|
initializeElectronPlatformService().catch(error => { |
||||
|
logger.error("Failed to initialize Electron platform service:", error); |
||||
|
}); |
||||
|
|
||||
|
export class ElectronPlatformService extends ElectronPlatformServiceImpl {} |
@ -1,111 +1,102 @@ |
|||||
import { |
import { |
||||
ImageResult, |
|
||||
PlatformService, |
PlatformService, |
||||
PlatformCapabilities, |
PlatformCapabilities, |
||||
|
SQLiteOperations, |
||||
|
SQLiteConfig, |
||||
|
PreparedStatement, |
||||
|
SQLiteResult, |
||||
|
ImageResult, |
||||
} from "../PlatformService"; |
} from "../PlatformService"; |
||||
|
import { BaseSQLiteService } from "../sqlite/BaseSQLiteService"; |
||||
|
import { app } from "electron"; |
||||
|
import { dialog } from "electron"; |
||||
|
import fs from "fs"; |
||||
|
import path from "path"; |
||||
|
import sqlite3 from "sqlite3"; |
||||
|
import { open, Database } from "sqlite"; |
||||
import { logger } from "../../utils/logger"; |
import { logger } from "../../utils/logger"; |
||||
|
import { Settings } from "../../db/tables/settings"; |
||||
|
import { Account } from "../../db/tables/accounts"; |
||||
|
import { Contact } from "../../db/tables/contacts"; |
||||
|
import { db } from "../../db"; |
||||
|
import { MASTER_SETTINGS_KEY } from "../../db/tables/settings"; |
||||
|
import { accountsDBPromise } from "../../db"; |
||||
|
import { accessToken } from "../../libs/crypto"; |
||||
|
import { getPlanFromCache as getPlanFromCacheImpl } from "../../libs/endorserServer"; |
||||
|
import { PlanSummaryRecord } from "../../interfaces/records"; |
||||
|
import { Axios } from "axios"; |
||||
|
|
||||
/** |
// Create Promise-based versions of fs functions
|
||||
* Platform service implementation for Electron (desktop) platform. |
const readFileAsync = (filePath: string, encoding: BufferEncoding): Promise<string> => { |
||||
* Note: This is a placeholder implementation with most methods currently unimplemented. |
return new Promise((resolve, reject) => { |
||||
* Implements the PlatformService interface but throws "Not implemented" errors for most operations. |
fs.readFile(filePath, { encoding }, (err: NodeJS.ErrnoException | null, data: string) => { |
||||
* |
if (err) reject(err); |
||||
* @remarks |
else resolve(data); |
||||
* This service is intended for desktop application functionality through Electron. |
}); |
||||
* Future implementations should provide: |
}); |
||||
* - Native file system access |
}; |
||||
* - Desktop camera integration |
|
||||
* - System-level features |
const readFileBufferAsync = (filePath: string): Promise<Buffer> => { |
||||
*/ |
return new Promise((resolve, reject) => { |
||||
export class ElectronPlatformService implements PlatformService { |
fs.readFile(filePath, (err: NodeJS.ErrnoException | null, data: Buffer) => { |
||||
/** |
if (err) reject(err); |
||||
* Gets the capabilities of the Electron platform |
else resolve(data); |
||||
* @returns Platform capabilities object |
}); |
||||
*/ |
}); |
||||
getCapabilities(): PlatformCapabilities { |
}; |
||||
return { |
|
||||
hasFileSystem: false, // Not implemented yet
|
|
||||
hasCamera: false, // Not implemented yet
|
|
||||
isMobile: false, |
|
||||
isIOS: false, |
|
||||
hasFileDownload: false, // Not implemented yet
|
|
||||
needsFileHandlingInstructions: false, |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
/** |
const writeFileAsync = (filePath: string, data: string, encoding: BufferEncoding): Promise<void> => { |
||||
* Reads a file from the filesystem. |
return new Promise((resolve, reject) => { |
||||
* @param _path - Path to the file to read |
fs.writeFile(filePath, data, { encoding }, (err: NodeJS.ErrnoException | null) => { |
||||
* @returns Promise that should resolve to file contents |
if (err) reject(err); |
||||
* @throws Error with "Not implemented" message |
else resolve(); |
||||
* @todo Implement file reading using Electron's file system API |
}); |
||||
*/ |
}); |
||||
async readFile(_path: string): Promise<string> { |
}; |
||||
throw new Error("Not implemented"); |
|
||||
} |
|
||||
|
|
||||
/** |
const unlinkAsync = (filePath: string): Promise<void> => { |
||||
* Writes content to a file. |
return new Promise((resolve, reject) => { |
||||
* @param _path - Path where to write the file |
fs.unlink(filePath, (err: NodeJS.ErrnoException | null) => { |
||||
* @param _content - Content to write to the file |
if (err) reject(err); |
||||
* @throws Error with "Not implemented" message |
else resolve(); |
||||
* @todo Implement file writing using Electron's file system API |
}); |
||||
*/ |
}); |
||||
async writeFile(_path: string, _content: string): Promise<void> { |
}; |
||||
throw new Error("Not implemented"); |
|
||||
} |
|
||||
|
|
||||
/** |
const readdirAsync = (dirPath: string): Promise<string[]> => { |
||||
* Deletes a file from the filesystem. |
return new Promise((resolve, reject) => { |
||||
* @param _path - Path to the file to delete |
fs.readdir(dirPath, (err: NodeJS.ErrnoException | null, files: string[]) => { |
||||
* @throws Error with "Not implemented" message |
if (err) reject(err); |
||||
* @todo Implement file deletion using Electron's file system API |
else resolve(files); |
||||
*/ |
}); |
||||
async deleteFile(_path: string): Promise<void> { |
}); |
||||
throw new Error("Not implemented"); |
}; |
||||
} |
|
||||
|
|
||||
/** |
const statAsync = (filePath: string): Promise<fs.Stats> => { |
||||
* Lists files in the specified directory. |
return new Promise((resolve, reject) => { |
||||
* @param _directory - Path to the directory to list |
fs.stat(filePath, (err: NodeJS.ErrnoException | null, stats: fs.Stats) => { |
||||
* @returns Promise that should resolve to array of filenames |
if (err) reject(err); |
||||
* @throws Error with "Not implemented" message |
else resolve(stats); |
||||
* @todo Implement directory listing using Electron's file system API |
}); |
||||
*/ |
}); |
||||
async listFiles(_directory: string): Promise<string[]> { |
}; |
||||
throw new Error("Not implemented"); |
|
||||
} |
|
||||
|
|
||||
/** |
interface SQLiteDatabase extends Database { |
||||
* Should open system camera to take a picture. |
changes: number; |
||||
* @returns Promise that should resolve to captured image data |
} |
||||
* @throws Error with "Not implemented" message |
|
||||
* @todo Implement camera access using Electron's media APIs |
|
||||
*/ |
|
||||
async takePicture(): Promise<ImageResult> { |
|
||||
logger.error("takePicture not implemented in Electron platform"); |
|
||||
throw new Error("Not implemented"); |
|
||||
} |
|
||||
|
|
||||
/** |
/** |
||||
* Should open system file picker for selecting an image. |
* SQLite implementation for Electron using native sqlite3 |
||||
* @returns Promise that should resolve to selected image data |
*/ |
||||
* @throws Error with "Not implemented" message |
class ElectronSQLiteService extends BaseSQLiteService { |
||||
* @todo Implement file picker using Electron's dialog API |
private db: SQLiteDatabase | null = null; |
||||
*/ |
private config: SQLiteConfig | null = null; |
||||
async pickImage(): Promise<ImageResult> { |
|
||||
logger.error("pickImage not implemented in Electron platform"); |
// ... rest of the ElectronSQLiteService implementation ...
|
||||
throw new Error("Not implemented"); |
} |
||||
} |
|
||||
|
export class ElectronPlatformService implements PlatformService { |
||||
|
private sqliteService: ElectronSQLiteService | null = null; |
||||
|
|
||||
/** |
// ... rest of the ElectronPlatformService implementation ...
|
||||
* Should handle deep link URLs for the desktop application. |
|
||||
* @param _url - The deep link URL to handle |
|
||||
* @throws Error with "Not implemented" message |
|
||||
* @todo Implement deep link handling using Electron's protocol handler |
|
||||
*/ |
|
||||
async handleDeepLink(_url: string): Promise<void> { |
|
||||
logger.error("handleDeepLink not implemented in Electron platform"); |
|
||||
throw new Error("Not implemented"); |
|
||||
} |
|
||||
} |
} |
@ -0,0 +1,248 @@ |
|||||
|
import initSqlJs, { Database } from "@jlongster/sql.js"; |
||||
|
import { SQLiteFS } from "absurd-sql"; |
||||
|
import { IndexedDBBackend } from "absurd-sql/dist/indexeddb-backend"; |
||||
|
import { BaseSQLiteService } from "./BaseSQLiteService"; |
||||
|
import { |
||||
|
SQLiteConfig, |
||||
|
SQLiteResult, |
||||
|
PreparedStatement, |
||||
|
} from "../PlatformService"; |
||||
|
import { logger } from "../../utils/logger"; |
||||
|
|
||||
|
/** |
||||
|
* SQLite implementation using absurd-sql for web browsers. |
||||
|
* Provides SQLite access in the browser using Web Workers and IndexedDB. |
||||
|
*/ |
||||
|
export class AbsurdSQLService extends BaseSQLiteService { |
||||
|
private db: Database | null = null; |
||||
|
private worker: Worker | null = null; |
||||
|
private config: SQLiteConfig | null = null; |
||||
|
|
||||
|
async initialize(config: SQLiteConfig): Promise<void> { |
||||
|
if (this.initialized) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
this.config = config; |
||||
|
const SQL = await initSqlJs({ |
||||
|
locateFile: (file) => `/sql-wasm/${file}`, |
||||
|
}); |
||||
|
|
||||
|
// Initialize the virtual file system
|
||||
|
const backend = new IndexedDBBackend(this.config.name); |
||||
|
const fs = new SQLiteFS(SQL.FS, backend); |
||||
|
SQL.register_for_idb(fs); |
||||
|
|
||||
|
// Create and initialize the database
|
||||
|
this.db = new SQL.Database(this.config.name, { |
||||
|
filename: true, |
||||
|
}); |
||||
|
|
||||
|
// Configure database settings
|
||||
|
if (this.config.useWAL) { |
||||
|
await this.execute("PRAGMA journal_mode = WAL"); |
||||
|
this.stats.walMode = true; |
||||
|
} |
||||
|
|
||||
|
if (this.config.useMMap) { |
||||
|
const mmapSize = this.config.mmapSize ?? 30000000000; |
||||
|
await this.execute(`PRAGMA mmap_size = ${mmapSize}`); |
||||
|
this.stats.mmapActive = true; |
||||
|
} |
||||
|
|
||||
|
// Set other pragmas for performance
|
||||
|
await this.execute("PRAGMA synchronous = NORMAL"); |
||||
|
await this.execute("PRAGMA temp_store = MEMORY"); |
||||
|
await this.execute("PRAGMA cache_size = -2000"); // Use 2MB of cache
|
||||
|
|
||||
|
// Start the Web Worker for async operations
|
||||
|
this.worker = new Worker(new URL("./sqlite.worker.ts", import.meta.url), { |
||||
|
type: "module", |
||||
|
}); |
||||
|
|
||||
|
this.initialized = true; |
||||
|
await this.updateStats(); |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to initialize Absurd SQL:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async close(): Promise<void> { |
||||
|
if (!this.initialized || !this.db) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
// Finalize all prepared statements
|
||||
|
for (const [_sql, stmt] of this.preparedStatements) { |
||||
|
logger.debug("finalizing statement", _sql); |
||||
|
await stmt.finalize(); |
||||
|
} |
||||
|
this.preparedStatements.clear(); |
||||
|
|
||||
|
// Close the database
|
||||
|
this.db.close(); |
||||
|
this.db = null; |
||||
|
|
||||
|
// Terminate the worker
|
||||
|
if (this.worker) { |
||||
|
this.worker.terminate(); |
||||
|
this.worker = null; |
||||
|
} |
||||
|
|
||||
|
this.initialized = false; |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to close Absurd SQL connection:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected async _executeQuery<T>( |
||||
|
sql: string, |
||||
|
params: unknown[] = [], |
||||
|
operation: "query" | "execute" = "query", |
||||
|
): Promise<SQLiteResult<T>> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
let lastInsertId: number | undefined = undefined; |
||||
|
|
||||
|
if (operation === "query") { |
||||
|
const stmt = this.db.prepare(sql); |
||||
|
const rows: T[] = []; |
||||
|
|
||||
|
try { |
||||
|
while (stmt.step()) { |
||||
|
rows.push(stmt.getAsObject() as T); |
||||
|
} |
||||
|
} finally { |
||||
|
stmt.free(); |
||||
|
} |
||||
|
|
||||
|
// Get last insert ID safely
|
||||
|
const result = this.db.exec("SELECT last_insert_rowid() AS id"); |
||||
|
lastInsertId = |
||||
|
(result?.[0]?.values?.[0]?.[0] as number | undefined) ?? undefined; |
||||
|
|
||||
|
return { |
||||
|
rows, |
||||
|
rowsAffected: this.db.getRowsModified(), |
||||
|
lastInsertId, |
||||
|
executionTime: 0, // Will be set by base class
|
||||
|
}; |
||||
|
} else { |
||||
|
this.db.run(sql, params); |
||||
|
|
||||
|
// Get last insert ID after execute
|
||||
|
const result = this.db.exec("SELECT last_insert_rowid() AS id"); |
||||
|
lastInsertId = |
||||
|
(result?.[0]?.values?.[0]?.[0] as number | undefined) ?? undefined; |
||||
|
|
||||
|
return { |
||||
|
rows: [], |
||||
|
rowsAffected: this.db.getRowsModified(), |
||||
|
lastInsertId, |
||||
|
executionTime: 0, |
||||
|
}; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("Absurd SQL query failed:", { |
||||
|
sql, |
||||
|
params, |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
}); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected async _beginTransaction(): Promise<void> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
this.db.exec("BEGIN TRANSACTION"); |
||||
|
} |
||||
|
|
||||
|
protected async _commitTransaction(): Promise<void> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
this.db.exec("COMMIT"); |
||||
|
} |
||||
|
|
||||
|
protected async _rollbackTransaction(): Promise<void> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
this.db.exec("ROLLBACK"); |
||||
|
} |
||||
|
|
||||
|
protected async _prepareStatement<T>( |
||||
|
_sql: string, |
||||
|
): Promise<PreparedStatement<T>> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
const stmt = this.db.prepare(_sql); |
||||
|
return { |
||||
|
execute: async (params: unknown[] = []) => { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const rows: T[] = []; |
||||
|
stmt.bind(params); |
||||
|
while (stmt.step()) { |
||||
|
rows.push(stmt.getAsObject() as T); |
||||
|
} |
||||
|
|
||||
|
// Safely extract lastInsertId
|
||||
|
const result = this.db.exec("SELECT last_insert_rowid()"); |
||||
|
const rawId = result?.[0]?.values?.[0]?.[0]; |
||||
|
const lastInsertId = typeof rawId === "number" ? rawId : undefined; |
||||
|
|
||||
|
return { |
||||
|
rows, |
||||
|
rowsAffected: this.db.getRowsModified(), |
||||
|
lastInsertId, |
||||
|
executionTime: 0, // Will be set by base class
|
||||
|
}; |
||||
|
} finally { |
||||
|
stmt.reset(); |
||||
|
} |
||||
|
}, |
||||
|
finalize: async () => { |
||||
|
stmt.free(); |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
protected async _finalizeStatement(_sql: string): Promise<void> { |
||||
|
// Statements are finalized when the PreparedStatement is finalized
|
||||
|
} |
||||
|
|
||||
|
async getDatabaseSize(): Promise<number> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const result = this.db.exec( |
||||
|
"SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()", |
||||
|
); |
||||
|
|
||||
|
const rawSize = result?.[0]?.values?.[0]?.[0]; |
||||
|
const size = typeof rawSize === "number" ? rawSize : 0; |
||||
|
|
||||
|
return size; |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to get database size:", error); |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,383 @@ |
|||||
|
import { |
||||
|
SQLiteOperations, |
||||
|
SQLiteConfig, |
||||
|
SQLiteResult, |
||||
|
PreparedStatement, |
||||
|
SQLiteStats, |
||||
|
} from "../PlatformService"; |
||||
|
import { Settings, MASTER_SETTINGS_KEY } from "../../db/tables/settings"; |
||||
|
import { logger } from "../../utils/logger"; |
||||
|
|
||||
|
/** |
||||
|
* Base class for SQLite implementations across different platforms. |
||||
|
* Provides common functionality and error handling. |
||||
|
*/ |
||||
|
export abstract class BaseSQLiteService implements SQLiteOperations { |
||||
|
protected initialized = false; |
||||
|
protected stats: SQLiteStats = { |
||||
|
totalQueries: 0, |
||||
|
avgExecutionTime: 0, |
||||
|
preparedStatements: 0, |
||||
|
databaseSize: 0, |
||||
|
walMode: false, |
||||
|
mmapActive: false, |
||||
|
}; |
||||
|
protected preparedStatements: Map<string, PreparedStatement<unknown>> = |
||||
|
new Map(); |
||||
|
|
||||
|
abstract initialize(config: SQLiteConfig): Promise<void>; |
||||
|
abstract close(): Promise<void>; |
||||
|
abstract getDatabaseSize(): Promise<number>; |
||||
|
|
||||
|
protected async executeQuery<T>( |
||||
|
sql: string, |
||||
|
params: unknown[] = [], |
||||
|
operation: "query" | "execute" = "query", |
||||
|
): Promise<SQLiteResult<T>> { |
||||
|
if (!this.initialized) { |
||||
|
throw new Error("SQLite database not initialized"); |
||||
|
} |
||||
|
|
||||
|
const startTime = performance.now(); |
||||
|
try { |
||||
|
const result = await this._executeQuery<T>(sql, params, operation); |
||||
|
const executionTime = performance.now() - startTime; |
||||
|
|
||||
|
// Update stats
|
||||
|
this.stats.totalQueries++; |
||||
|
this.stats.avgExecutionTime = |
||||
|
(this.stats.avgExecutionTime * (this.stats.totalQueries - 1) + |
||||
|
executionTime) / |
||||
|
this.stats.totalQueries; |
||||
|
|
||||
|
return { |
||||
|
...result, |
||||
|
executionTime, |
||||
|
}; |
||||
|
} catch (error) { |
||||
|
logger.error("SQLite query failed:", { |
||||
|
sql, |
||||
|
params, |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
}); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract _executeQuery<T>( |
||||
|
sql: string, |
||||
|
params: unknown[], |
||||
|
operation: "query" | "execute", |
||||
|
): Promise<SQLiteResult<T>>; |
||||
|
|
||||
|
async query<T>( |
||||
|
sql: string, |
||||
|
params: unknown[] = [], |
||||
|
): Promise<SQLiteResult<T>> { |
||||
|
return this.executeQuery<T>(sql, params, "query"); |
||||
|
} |
||||
|
|
||||
|
async execute(sql: string, params: unknown[] = []): Promise<number> { |
||||
|
const result = await this.executeQuery<unknown>(sql, params, "execute"); |
||||
|
return result.rowsAffected; |
||||
|
} |
||||
|
|
||||
|
async transaction( |
||||
|
statements: { sql: string; params?: unknown[] }[], |
||||
|
): Promise<void> { |
||||
|
if (!this.initialized) { |
||||
|
throw new Error("SQLite database not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
await this._beginTransaction(); |
||||
|
for (const { sql, params = [] } of statements) { |
||||
|
await this.executeQuery(sql, params, "execute"); |
||||
|
} |
||||
|
await this._commitTransaction(); |
||||
|
} catch (error) { |
||||
|
await this._rollbackTransaction(); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract _beginTransaction(): Promise<void>; |
||||
|
protected abstract _commitTransaction(): Promise<void>; |
||||
|
protected abstract _rollbackTransaction(): Promise<void>; |
||||
|
|
||||
|
async getMaxValue<T>( |
||||
|
table: string, |
||||
|
column: string, |
||||
|
where?: string, |
||||
|
params: unknown[] = [], |
||||
|
): Promise<T | null> { |
||||
|
const sql = `SELECT MAX(${column}) as max_value FROM ${table}${where ? ` WHERE ${where}` : ""}`; |
||||
|
const result = await this.query<{ max_value: T }>(sql, params); |
||||
|
return result.rows[0]?.max_value ?? null; |
||||
|
} |
||||
|
|
||||
|
async prepare<T>(sql: string): Promise<PreparedStatement<T>> { |
||||
|
if (!this.initialized) { |
||||
|
throw new Error("SQLite database not initialized"); |
||||
|
} |
||||
|
|
||||
|
const stmt = await this._prepareStatement<T>(sql); |
||||
|
this.stats.preparedStatements++; |
||||
|
this.preparedStatements.set(sql, stmt); |
||||
|
|
||||
|
return { |
||||
|
execute: async (params: unknown[] = []) => { |
||||
|
return this.executeQuery<T>(sql, params, "query"); |
||||
|
}, |
||||
|
finalize: async () => { |
||||
|
await this._finalizeStatement(sql); |
||||
|
this.preparedStatements.delete(sql); |
||||
|
this.stats.preparedStatements--; |
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
protected abstract _prepareStatement<T>( |
||||
|
sql: string, |
||||
|
): Promise<PreparedStatement<T>>; |
||||
|
protected abstract _finalizeStatement(sql: string): Promise<void>; |
||||
|
|
||||
|
async getStats(): Promise<SQLiteStats> { |
||||
|
return { |
||||
|
...this.stats, |
||||
|
databaseSize: await this.getDatabaseSize(), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
protected async updateStats(): Promise<void> { |
||||
|
this.stats.databaseSize = await this.getDatabaseSize(); |
||||
|
// Platform-specific stats updates can be implemented in subclasses
|
||||
|
} |
||||
|
|
||||
|
protected async setupSchema(): Promise<void> { |
||||
|
await this.execute(` |
||||
|
CREATE TABLE IF NOT EXISTS settings ( |
||||
|
id INTEGER PRIMARY KEY, |
||||
|
accountDid TEXT, |
||||
|
activeDid TEXT, |
||||
|
apiServer TEXT, |
||||
|
filterFeedByNearby INTEGER, |
||||
|
filterFeedByVisible INTEGER, |
||||
|
finishedOnboarding INTEGER, |
||||
|
firstName TEXT, |
||||
|
hideRegisterPromptOnNewContact INTEGER, |
||||
|
isRegistered INTEGER, |
||||
|
lastName TEXT, |
||||
|
lastAckedOfferToUserJwtId TEXT, |
||||
|
lastAckedOfferToUserProjectsJwtId TEXT, |
||||
|
lastNotifiedClaimId TEXT, |
||||
|
lastViewedClaimId TEXT, |
||||
|
notifyingNewActivityTime TEXT, |
||||
|
notifyingReminderMessage TEXT, |
||||
|
notifyingReminderTime TEXT, |
||||
|
partnerApiServer TEXT, |
||||
|
passkeyExpirationMinutes INTEGER, |
||||
|
profileImageUrl TEXT, |
||||
|
searchBoxes TEXT, |
||||
|
showContactGivesInline INTEGER, |
||||
|
showGeneralAdvanced INTEGER, |
||||
|
showShortcutBvc INTEGER, |
||||
|
vapid TEXT, |
||||
|
warnIfProdServer INTEGER, |
||||
|
warnIfTestServer INTEGER, |
||||
|
webPushServer TEXT |
||||
|
) |
||||
|
`);
|
||||
|
} |
||||
|
|
||||
|
protected async settingsToRow( |
||||
|
settings: Partial<Settings>, |
||||
|
): Promise<Record<string, unknown>> { |
||||
|
const row: Record<string, unknown> = {}; |
||||
|
|
||||
|
// Convert boolean values to integers for SQLite
|
||||
|
if ("filterFeedByNearby" in settings) |
||||
|
row.filterFeedByNearby = settings.filterFeedByNearby ? 1 : 0; |
||||
|
if ("filterFeedByVisible" in settings) |
||||
|
row.filterFeedByVisible = settings.filterFeedByVisible ? 1 : 0; |
||||
|
if ("finishedOnboarding" in settings) |
||||
|
row.finishedOnboarding = settings.finishedOnboarding ? 1 : 0; |
||||
|
if ("hideRegisterPromptOnNewContact" in settings) |
||||
|
row.hideRegisterPromptOnNewContact = |
||||
|
settings.hideRegisterPromptOnNewContact ? 1 : 0; |
||||
|
if ("isRegistered" in settings) |
||||
|
row.isRegistered = settings.isRegistered ? 1 : 0; |
||||
|
if ("showContactGivesInline" in settings) |
||||
|
row.showContactGivesInline = settings.showContactGivesInline ? 1 : 0; |
||||
|
if ("showGeneralAdvanced" in settings) |
||||
|
row.showGeneralAdvanced = settings.showGeneralAdvanced ? 1 : 0; |
||||
|
if ("showShortcutBvc" in settings) |
||||
|
row.showShortcutBvc = settings.showShortcutBvc ? 1 : 0; |
||||
|
if ("warnIfProdServer" in settings) |
||||
|
row.warnIfProdServer = settings.warnIfProdServer ? 1 : 0; |
||||
|
if ("warnIfTestServer" in settings) |
||||
|
row.warnIfTestServer = settings.warnIfTestServer ? 1 : 0; |
||||
|
|
||||
|
// Handle JSON fields
|
||||
|
if ("searchBoxes" in settings) |
||||
|
row.searchBoxes = JSON.stringify(settings.searchBoxes); |
||||
|
|
||||
|
// Copy all other fields as is
|
||||
|
Object.entries(settings).forEach(([key, value]) => { |
||||
|
if (!(key in row)) { |
||||
|
row[key] = value; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return row; |
||||
|
} |
||||
|
|
||||
|
protected async rowToSettings( |
||||
|
row: Record<string, unknown>, |
||||
|
): Promise<Settings> { |
||||
|
const settings: Settings = {}; |
||||
|
|
||||
|
// Convert integer values back to booleans
|
||||
|
if ("filterFeedByNearby" in row) |
||||
|
settings.filterFeedByNearby = !!row.filterFeedByNearby; |
||||
|
if ("filterFeedByVisible" in row) |
||||
|
settings.filterFeedByVisible = !!row.filterFeedByVisible; |
||||
|
if ("finishedOnboarding" in row) |
||||
|
settings.finishedOnboarding = !!row.finishedOnboarding; |
||||
|
if ("hideRegisterPromptOnNewContact" in row) |
||||
|
settings.hideRegisterPromptOnNewContact = |
||||
|
!!row.hideRegisterPromptOnNewContact; |
||||
|
if ("isRegistered" in row) settings.isRegistered = !!row.isRegistered; |
||||
|
if ("showContactGivesInline" in row) |
||||
|
settings.showContactGivesInline = !!row.showContactGivesInline; |
||||
|
if ("showGeneralAdvanced" in row) |
||||
|
settings.showGeneralAdvanced = !!row.showGeneralAdvanced; |
||||
|
if ("showShortcutBvc" in row) |
||||
|
settings.showShortcutBvc = !!row.showShortcutBvc; |
||||
|
if ("warnIfProdServer" in row) |
||||
|
settings.warnIfProdServer = !!row.warnIfProdServer; |
||||
|
if ("warnIfTestServer" in row) |
||||
|
settings.warnIfTestServer = !!row.warnIfTestServer; |
||||
|
|
||||
|
// Parse JSON fields
|
||||
|
if ("searchBoxes" in row && row.searchBoxes) { |
||||
|
try { |
||||
|
settings.searchBoxes = JSON.parse(row.searchBoxes); |
||||
|
} catch (error) { |
||||
|
logger.error("Error parsing searchBoxes JSON:", error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Copy all other fields as is
|
||||
|
Object.entries(row).forEach(([key, value]) => { |
||||
|
if (!(key in settings)) { |
||||
|
(settings as Record<string, unknown>)[key] = value; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return settings; |
||||
|
} |
||||
|
|
||||
|
async updateMasterSettings( |
||||
|
settingsChanges: Partial<Settings>, |
||||
|
): Promise<void> { |
||||
|
try { |
||||
|
const row = await this.settingsToRow(settingsChanges); |
||||
|
row.id = MASTER_SETTINGS_KEY; |
||||
|
delete row.accountDid; |
||||
|
|
||||
|
const result = await this.execute( |
||||
|
`UPDATE settings SET ${Object.keys(row) |
||||
|
.map((k) => `${k} = ?`) |
||||
|
.join(", ")} WHERE id = ?`,
|
||||
|
[...Object.values(row), MASTER_SETTINGS_KEY], |
||||
|
); |
||||
|
|
||||
|
if (result === 0) { |
||||
|
// If no record was updated, create a new one
|
||||
|
await this.execute( |
||||
|
`INSERT INTO settings (${Object.keys(row).join(", ")}) VALUES (${Object.keys( |
||||
|
row, |
||||
|
) |
||||
|
.map(() => "?") |
||||
|
.join(", ")})`,
|
||||
|
Object.values(row), |
||||
|
); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("Error updating master settings:", error); |
||||
|
throw new Error("Failed to update settings"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async getActiveAccountSettings(): Promise<Settings> { |
||||
|
try { |
||||
|
const defaultSettings = await this.query<Record<string, unknown>>( |
||||
|
"SELECT * FROM settings WHERE id = ?", |
||||
|
[MASTER_SETTINGS_KEY], |
||||
|
); |
||||
|
|
||||
|
if (!defaultSettings.rows.length) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
const settings = await this.rowToSettings(defaultSettings.rows[0]); |
||||
|
|
||||
|
if (!settings.activeDid) { |
||||
|
return settings; |
||||
|
} |
||||
|
|
||||
|
const overrideSettings = await this.query<Record<string, unknown>>( |
||||
|
"SELECT * FROM settings WHERE accountDid = ?", |
||||
|
[settings.activeDid], |
||||
|
); |
||||
|
|
||||
|
if (!overrideSettings.rows.length) { |
||||
|
return settings; |
||||
|
} |
||||
|
|
||||
|
const override = await this.rowToSettings(overrideSettings.rows[0]); |
||||
|
return { ...settings, ...override }; |
||||
|
} catch (error) { |
||||
|
logger.error("Error getting active account settings:", error); |
||||
|
throw new Error("Failed to get settings"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async updateAccountSettings( |
||||
|
accountDid: string, |
||||
|
settingsChanges: Partial<Settings>, |
||||
|
): Promise<void> { |
||||
|
try { |
||||
|
const row = await this.settingsToRow(settingsChanges); |
||||
|
row.accountDid = accountDid; |
||||
|
|
||||
|
const result = await this.execute( |
||||
|
`UPDATE settings SET ${Object.keys(row) |
||||
|
.map((k) => `${k} = ?`) |
||||
|
.join(", ")} WHERE accountDid = ?`,
|
||||
|
[...Object.values(row), accountDid], |
||||
|
); |
||||
|
|
||||
|
if (result === 0) { |
||||
|
// If no record was updated, create a new one
|
||||
|
const idResult = await this.query<{ max: number }>( |
||||
|
"SELECT MAX(id) as max FROM settings", |
||||
|
); |
||||
|
row.id = (idResult.rows[0]?.max || 0) + 1; |
||||
|
|
||||
|
await this.execute( |
||||
|
`INSERT INTO settings (${Object.keys(row).join(", ")}) VALUES (${Object.keys( |
||||
|
row, |
||||
|
) |
||||
|
.map(() => "?") |
||||
|
.join(", ")})`,
|
||||
|
Object.values(row), |
||||
|
); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("Error updating account settings:", error); |
||||
|
throw new Error("Failed to update settings"); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,176 @@ |
|||||
|
import { |
||||
|
CapacitorSQLite, |
||||
|
SQLiteConnection, |
||||
|
SQLiteDBConnection, |
||||
|
} from "@capacitor-community/sqlite"; |
||||
|
import { BaseSQLiteService } from "./BaseSQLiteService"; |
||||
|
import { |
||||
|
SQLiteConfig, |
||||
|
SQLiteResult, |
||||
|
PreparedStatement, |
||||
|
} from "../PlatformService"; |
||||
|
import { logger } from "../../utils/logger"; |
||||
|
|
||||
|
/** |
||||
|
* SQLite implementation using the Capacitor SQLite plugin. |
||||
|
* Provides native SQLite access on mobile platforms. |
||||
|
*/ |
||||
|
export class CapacitorSQLiteService extends BaseSQLiteService { |
||||
|
private connection: SQLiteDBConnection | null = null; |
||||
|
private sqlite: SQLiteConnection | null = null; |
||||
|
|
||||
|
async initialize(config: SQLiteConfig): Promise<void> { |
||||
|
if (this.initialized) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
this.sqlite = new SQLiteConnection(CapacitorSQLite); |
||||
|
const db = await this.sqlite.createConnection( |
||||
|
config.name, |
||||
|
config.useWAL ?? false, |
||||
|
"no-encryption", |
||||
|
1, |
||||
|
false, |
||||
|
); |
||||
|
|
||||
|
await db.open(); |
||||
|
this.connection = db; |
||||
|
|
||||
|
// Configure database settings
|
||||
|
if (config.useWAL) { |
||||
|
await this.execute("PRAGMA journal_mode = WAL"); |
||||
|
this.stats.walMode = true; |
||||
|
} |
||||
|
|
||||
|
// Set other pragmas for performance
|
||||
|
await this.execute("PRAGMA synchronous = NORMAL"); |
||||
|
await this.execute("PRAGMA temp_store = MEMORY"); |
||||
|
await this.execute("PRAGMA mmap_size = 30000000000"); |
||||
|
this.stats.mmapActive = true; |
||||
|
|
||||
|
// Set up database schema
|
||||
|
await this.setupSchema(); |
||||
|
|
||||
|
this.initialized = true; |
||||
|
await this.updateStats(); |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to initialize Capacitor SQLite:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async close(): Promise<void> { |
||||
|
if (!this.initialized || !this.connection || !this.sqlite) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
await this.connection.close(); |
||||
|
await this.sqlite.closeConnection(this.connection); |
||||
|
this.connection = null; |
||||
|
this.sqlite = null; |
||||
|
this.initialized = false; |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to close Capacitor SQLite connection:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected async _executeQuery<T>( |
||||
|
sql: string, |
||||
|
params: unknown[] = [], |
||||
|
operation: "query" | "execute" = "query", |
||||
|
): Promise<SQLiteResult<T>> { |
||||
|
if (!this.connection) { |
||||
|
throw new Error("Database connection not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
if (operation === "query") { |
||||
|
const result = await this.connection.query(sql, params); |
||||
|
return { |
||||
|
rows: result.values as T[], |
||||
|
rowsAffected: result.changes?.changes ?? 0, |
||||
|
lastInsertId: result.changes?.lastId, |
||||
|
executionTime: 0, // Will be set by base class
|
||||
|
}; |
||||
|
} else { |
||||
|
const result = await this.connection.run(sql, params); |
||||
|
return { |
||||
|
rows: [], |
||||
|
rowsAffected: result.changes?.changes ?? 0, |
||||
|
lastInsertId: result.changes?.lastId, |
||||
|
executionTime: 0, // Will be set by base class
|
||||
|
}; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("Capacitor SQLite query failed:", { |
||||
|
sql, |
||||
|
params, |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
}); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected async _beginTransaction(): Promise<void> { |
||||
|
if (!this.connection) { |
||||
|
throw new Error("Database connection not initialized"); |
||||
|
} |
||||
|
await this.connection.execute("BEGIN TRANSACTION"); |
||||
|
} |
||||
|
|
||||
|
protected async _commitTransaction(): Promise<void> { |
||||
|
if (!this.connection) { |
||||
|
throw new Error("Database connection not initialized"); |
||||
|
} |
||||
|
await this.connection.execute("COMMIT"); |
||||
|
} |
||||
|
|
||||
|
protected async _rollbackTransaction(): Promise<void> { |
||||
|
if (!this.connection) { |
||||
|
throw new Error("Database connection not initialized"); |
||||
|
} |
||||
|
await this.connection.execute("ROLLBACK"); |
||||
|
} |
||||
|
|
||||
|
protected async _prepareStatement<T>( |
||||
|
sql: string, |
||||
|
): Promise<PreparedStatement<T>> { |
||||
|
if (!this.connection) { |
||||
|
throw new Error("Database connection not initialized"); |
||||
|
} |
||||
|
|
||||
|
// Capacitor SQLite doesn't support prepared statements directly,
|
||||
|
// so we'll simulate it by storing the SQL
|
||||
|
return { |
||||
|
execute: async (params: unknown[] = []) => { |
||||
|
return this.executeQuery<T>(sql, params, "query"); |
||||
|
}, |
||||
|
finalize: async () => { |
||||
|
// No cleanup needed for Capacitor SQLite
|
||||
|
}, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
protected async _finalizeStatement(_sql: string): Promise<void> { |
||||
|
// No cleanup needed for Capacitor SQLite
|
||||
|
} |
||||
|
|
||||
|
async getDatabaseSize(): Promise<number> { |
||||
|
if (!this.connection) { |
||||
|
throw new Error("Database connection not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
const result = await this.connection.query( |
||||
|
"SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()", |
||||
|
); |
||||
|
return result.values?.[0]?.size ?? 0; |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to get database size:", error); |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,170 @@ |
|||||
|
import { BaseSQLiteService } from "./BaseSQLiteService"; |
||||
|
import { SQLiteConfig, SQLiteOperations, SQLiteResult, PreparedStatement, SQLiteStats } from "../PlatformService"; |
||||
|
import { logger } from "../../utils/logger"; |
||||
|
import initSqlJs, { Database } from "@jlongster/sql.js"; |
||||
|
import { SQLiteFS } from "absurd-sql"; |
||||
|
import IndexedDBBackend from "absurd-sql/dist/indexeddb-backend"; |
||||
|
|
||||
|
/** |
||||
|
* SQLite implementation for web platform using absurd-sql |
||||
|
*/ |
||||
|
export class WebSQLiteService extends BaseSQLiteService { |
||||
|
private db: Database | null = null; |
||||
|
private config: SQLiteConfig | null = null; |
||||
|
private worker: Worker | null = null; |
||||
|
|
||||
|
async initialize(config: SQLiteConfig): Promise<void> { |
||||
|
if (this.initialized) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
this.config = config; |
||||
|
|
||||
|
// Initialize SQL.js
|
||||
|
const SQL = await initSqlJs({ |
||||
|
locateFile: (file) => `/sql-wasm.wasm`, |
||||
|
}); |
||||
|
|
||||
|
// Create a worker for SQLite operations
|
||||
|
this.worker = new Worker("/sql-worker.js"); |
||||
|
|
||||
|
// Initialize SQLiteFS with IndexedDB backend
|
||||
|
const backend = new IndexedDBBackend(); |
||||
|
const fs = new SQLiteFS(backend, this.worker); |
||||
|
|
||||
|
// Create database file
|
||||
|
const dbPath = `/${config.name}.db`; |
||||
|
if (!(await fs.exists(dbPath))) { |
||||
|
await fs.writeFile(dbPath, new Uint8Array(0)); |
||||
|
} |
||||
|
|
||||
|
// Open database
|
||||
|
this.db = new SQL.Database(dbPath, { filename: true }); |
||||
|
|
||||
|
// Configure database settings
|
||||
|
if (config.useWAL) { |
||||
|
await this.execute("PRAGMA journal_mode = WAL"); |
||||
|
this.stats.walMode = true; |
||||
|
} |
||||
|
|
||||
|
// Set other pragmas for performance
|
||||
|
await this.execute("PRAGMA synchronous = NORMAL"); |
||||
|
await this.execute("PRAGMA temp_store = MEMORY"); |
||||
|
await this.execute("PRAGMA cache_size = -2000"); // Use 2MB of cache
|
||||
|
|
||||
|
this.initialized = true; |
||||
|
await this.updateStats(); |
||||
|
} catch (error) { |
||||
|
logger.error("Failed to initialize Web SQLite:", error); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected async _executeQuery<T>( |
||||
|
sql: string, |
||||
|
params: unknown[] = [], |
||||
|
operation: "query" | "execute" = "query", |
||||
|
): Promise<SQLiteResult<T>> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
if (operation === "query") { |
||||
|
const stmt = this.db.prepare(sql); |
||||
|
const results = stmt.get(params) as T[]; |
||||
|
stmt.free(); |
||||
|
return { results }; |
||||
|
} else { |
||||
|
const stmt = this.db.prepare(sql); |
||||
|
stmt.run(params); |
||||
|
const changes = this.db.getRowsModified(); |
||||
|
stmt.free(); |
||||
|
return { changes }; |
||||
|
} |
||||
|
} catch (error) { |
||||
|
logger.error("SQLite query failed:", { |
||||
|
sql, |
||||
|
params, |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
}); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async close(): Promise<void> { |
||||
|
if (this.db) { |
||||
|
this.db.close(); |
||||
|
this.db = null; |
||||
|
} |
||||
|
if (this.worker) { |
||||
|
this.worker.terminate(); |
||||
|
this.worker = null; |
||||
|
} |
||||
|
this.initialized = false; |
||||
|
} |
||||
|
|
||||
|
async getDatabaseSize(): Promise<number> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
const result = await this.query<{ size: number }>("SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()"); |
||||
|
return result.results[0]?.size || 0; |
||||
|
} |
||||
|
|
||||
|
async prepare<T>(sql: string): Promise<PreparedStatement<T>> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
const stmt = this.db.prepare(sql); |
||||
|
const key = sql; |
||||
|
|
||||
|
const preparedStmt: PreparedStatement<T> = { |
||||
|
execute: async (params: unknown[] = []) => { |
||||
|
try { |
||||
|
const results = stmt.get(params) as T[]; |
||||
|
return { results }; |
||||
|
} catch (error) { |
||||
|
logger.error("Prepared statement execution failed:", { |
||||
|
sql, |
||||
|
params, |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
}); |
||||
|
throw error; |
||||
|
} |
||||
|
}, |
||||
|
finalize: () => { |
||||
|
stmt.free(); |
||||
|
this.preparedStatements.delete(key); |
||||
|
this.stats.preparedStatements--; |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
this.preparedStatements.set(key, preparedStmt); |
||||
|
this.stats.preparedStatements++; |
||||
|
|
||||
|
return preparedStmt; |
||||
|
} |
||||
|
|
||||
|
async getStats(): Promise<SQLiteStats> { |
||||
|
await this.updateStats(); |
||||
|
return this.stats; |
||||
|
} |
||||
|
|
||||
|
private async updateStats(): Promise<void> { |
||||
|
if (!this.db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
const size = await this.getDatabaseSize(); |
||||
|
this.stats.databaseSize = size; |
||||
|
|
||||
|
const walResult = await this.query<{ journal_mode: string }>("PRAGMA journal_mode"); |
||||
|
this.stats.walMode = walResult.results[0]?.journal_mode === "wal"; |
||||
|
|
||||
|
const mmapResult = await this.query<{ mmap_size: number }>("PRAGMA mmap_size"); |
||||
|
this.stats.mmapActive = mmapResult.results[0]?.mmap_size > 0; |
||||
|
} |
||||
|
} |
@ -0,0 +1,150 @@ |
|||||
|
import initSqlJs, { Database } from "@jlongster/sql.js"; |
||||
|
import { SQLiteFS } from "absurd-sql"; |
||||
|
import { IndexedDBBackend } from "absurd-sql/dist/indexeddb-backend"; |
||||
|
|
||||
|
interface WorkerMessage { |
||||
|
type: "init" | "query" | "execute" | "transaction" | "close"; |
||||
|
id: string; |
||||
|
dbName?: string; |
||||
|
sql?: string; |
||||
|
params?: unknown[]; |
||||
|
statements?: { sql: string; params?: unknown[] }[]; |
||||
|
} |
||||
|
|
||||
|
interface WorkerResponse { |
||||
|
id: string; |
||||
|
error?: string; |
||||
|
result?: unknown; |
||||
|
} |
||||
|
|
||||
|
let db: Database | null = null; |
||||
|
|
||||
|
async function initialize(dbName: string): Promise<void> { |
||||
|
if (db) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const SQL = await initSqlJs({ |
||||
|
locateFile: (file: string) => `/sql-wasm/${file}`, |
||||
|
}); |
||||
|
|
||||
|
// Initialize the virtual file system
|
||||
|
const backend = new IndexedDBBackend(dbName); |
||||
|
const fs = new SQLiteFS(SQL.FS, backend); |
||||
|
SQL.register_for_idb(fs); |
||||
|
|
||||
|
// Create and initialize the database
|
||||
|
db = new SQL.Database(dbName, { |
||||
|
filename: true, |
||||
|
}); |
||||
|
|
||||
|
// Configure database settings
|
||||
|
db.exec("PRAGMA synchronous = NORMAL"); |
||||
|
db.exec("PRAGMA temp_store = MEMORY"); |
||||
|
db.exec("PRAGMA cache_size = -2000"); // Use 2MB of cache
|
||||
|
} |
||||
|
|
||||
|
async function executeQuery( |
||||
|
sql: string, |
||||
|
params: unknown[] = [], |
||||
|
): Promise<unknown> { |
||||
|
if (!db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
const stmt = db.prepare(sql); |
||||
|
try { |
||||
|
const rows: unknown[] = []; |
||||
|
stmt.bind(params); |
||||
|
while (stmt.step()) { |
||||
|
rows.push(stmt.getAsObject()); |
||||
|
} |
||||
|
return { |
||||
|
rows, |
||||
|
rowsAffected: db.getRowsModified(), |
||||
|
lastInsertId: db.exec("SELECT last_insert_rowid()")[0]?.values[0]?.[0], |
||||
|
}; |
||||
|
} finally { |
||||
|
stmt.free(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function executeTransaction( |
||||
|
statements: { sql: string; params?: unknown[] }[], |
||||
|
): Promise<void> { |
||||
|
if (!db) { |
||||
|
throw new Error("Database not initialized"); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
db.exec("BEGIN TRANSACTION"); |
||||
|
for (const { sql, params = [] } of statements) { |
||||
|
const stmt = db.prepare(sql); |
||||
|
try { |
||||
|
stmt.bind(params); |
||||
|
stmt.step(); |
||||
|
} finally { |
||||
|
stmt.free(); |
||||
|
} |
||||
|
} |
||||
|
db.exec("COMMIT"); |
||||
|
} catch (error) { |
||||
|
db.exec("ROLLBACK"); |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function close(): Promise<void> { |
||||
|
if (db) { |
||||
|
db.close(); |
||||
|
db = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
self.onmessage = async (event: MessageEvent<WorkerMessage>) => { |
||||
|
const { type, id, dbName, sql, params, statements } = event.data; |
||||
|
const response: WorkerResponse = { id }; |
||||
|
|
||||
|
try { |
||||
|
switch (type) { |
||||
|
case "init": |
||||
|
if (!dbName) { |
||||
|
throw new Error("Database name is required for initialization"); |
||||
|
} |
||||
|
await initialize(dbName); |
||||
|
break; |
||||
|
|
||||
|
case "query": |
||||
|
if (!sql) { |
||||
|
throw new Error("SQL query is required"); |
||||
|
} |
||||
|
response.result = await executeQuery(sql, params); |
||||
|
break; |
||||
|
|
||||
|
case "execute": |
||||
|
if (!sql) { |
||||
|
throw new Error("SQL statement is required"); |
||||
|
} |
||||
|
response.result = await executeQuery(sql, params); |
||||
|
break; |
||||
|
|
||||
|
case "transaction": |
||||
|
if (!statements?.length) { |
||||
|
throw new Error("Transaction statements are required"); |
||||
|
} |
||||
|
await executeTransaction(statements); |
||||
|
break; |
||||
|
|
||||
|
case "close": |
||||
|
await close(); |
||||
|
break; |
||||
|
|
||||
|
default: |
||||
|
throw new Error(`Unknown message type: ${type}`); |
||||
|
} |
||||
|
} catch (error) { |
||||
|
response.error = error instanceof Error ? error.message : String(error); |
||||
|
} |
||||
|
|
||||
|
self.postMessage(response); |
||||
|
}; |
@ -0,0 +1,45 @@ |
|||||
|
declare module "@jlongster/sql.js" { |
||||
|
export interface Database { |
||||
|
exec( |
||||
|
sql: string, |
||||
|
params?: unknown[], |
||||
|
): { columns: string[]; values: unknown[][] }[]; |
||||
|
prepare(sql: string): Statement; |
||||
|
run(sql: string, params?: unknown[]): void; |
||||
|
getRowsModified(): number; |
||||
|
close(): void; |
||||
|
} |
||||
|
|
||||
|
export interface Statement { |
||||
|
step(): boolean; |
||||
|
getAsObject(): Record<string, unknown>; |
||||
|
bind(params: unknown[]): void; |
||||
|
reset(): void; |
||||
|
free(): void; |
||||
|
} |
||||
|
|
||||
|
export interface InitSqlJsStatic { |
||||
|
Database: new ( |
||||
|
filename?: string, |
||||
|
options?: { filename: boolean }, |
||||
|
) => Database; |
||||
|
FS: unknown; |
||||
|
register_for_idb(fs: unknown): void; |
||||
|
} |
||||
|
|
||||
|
export default function initSqlJs(options?: { |
||||
|
locateFile?: (file: string) => string; |
||||
|
}): Promise<InitSqlJsStatic>; |
||||
|
} |
||||
|
|
||||
|
declare module "absurd-sql" { |
||||
|
export class SQLiteFS { |
||||
|
constructor(fs: unknown, backend: unknown); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
declare module "absurd-sql/dist/indexeddb-backend" { |
||||
|
export class IndexedDBBackend { |
||||
|
constructor(dbName: string); |
||||
|
} |
||||
|
} |
@ -1,18 +1,18 @@ |
|||||
// Minimal fs module implementation for browser
|
// Minimal fs module implementation for browser
|
||||
const fs = { |
const fs = { |
||||
readFileSync: () => { |
readFileSync: () => { |
||||
throw new Error('fs.readFileSync is not supported in browser'); |
throw new Error("fs.readFileSync is not supported in browser"); |
||||
}, |
}, |
||||
writeFileSync: () => { |
writeFileSync: () => { |
||||
throw new Error('fs.writeFileSync is not supported in browser'); |
throw new Error("fs.writeFileSync is not supported in browser"); |
||||
}, |
}, |
||||
existsSync: () => false, |
existsSync: () => false, |
||||
mkdirSync: () => {}, |
mkdirSync: () => {}, |
||||
readdirSync: () => [], |
readdirSync: () => [], |
||||
statSync: () => ({ |
statSync: () => ({ |
||||
isDirectory: () => false, |
isDirectory: () => false, |
||||
isFile: () => false |
isFile: () => false, |
||||
}) |
}), |
||||
}; |
}; |
||||
|
|
||||
export default fs; |
export default fs; |
@ -1,13 +1,13 @@ |
|||||
// Minimal path module implementation for browser
|
// Minimal path module implementation for browser
|
||||
const path = { |
const path = { |
||||
resolve: (...parts) => parts.join('/'), |
resolve: (...parts) => parts.join("/"), |
||||
join: (...parts) => parts.join('/'), |
join: (...parts) => parts.join("/"), |
||||
dirname: (p) => p.split('/').slice(0, -1).join('/'), |
dirname: (p) => p.split("/").slice(0, -1).join("/"), |
||||
basename: (p) => p.split('/').pop(), |
basename: (p) => p.split("/").pop(), |
||||
extname: (p) => { |
extname: (p) => { |
||||
const parts = p.split('.'); |
const parts = p.split("."); |
||||
return parts.length > 1 ? '.' + parts.pop() : ''; |
return parts.length > 1 ? "." + parts.pop() : ""; |
||||
} |
}, |
||||
}; |
}; |
||||
|
|
||||
export default path; |
export default path; |
@ -1,4 +1,12 @@ |
|||||
import { defineConfig } from "vite"; |
import { defineConfig } from "vite"; |
||||
import { createBuildConfig } from "./vite.config.common.mts"; |
import { createBuildConfig } from "./vite.config.common.mts"; |
||||
|
|
||||
export default defineConfig(async () => createBuildConfig('capacitor')); |
export default defineConfig( |
||||
|
async () => { |
||||
|
const baseConfig = await createBuildConfig('capacitor'); |
||||
|
return mergeConfig(baseConfig, { |
||||
|
optimizeDeps: { |
||||
|
include: ['@capacitor-community/sqlite'] |
||||
|
} |
||||
|
}); |
||||
|
}); |
Loading…
Reference in new issue