experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

start migration to multibackend

+111 -1827
+1
.gitignore
··· 7 7 # Build output 8 8 out/ 9 9 app/dist/ 10 + dist/ 10 11 11 12 # Claude Code local settings 12 13 .claude/
-28
app/datastore/config.js
··· 1 - // Datastore configuration 2 - 3 - export const id = 'datastore'; 4 - 5 - export const labels = { 6 - id: 'datastore', 7 - name: 'Datastore', 8 - description: 'Personal datastore for addresses, content, and metadata' 9 - }; 10 - 11 - export const storageKeys = { 12 - VERSION: 'version', 13 - SETTINGS: 'settings' 14 - }; 15 - 16 - export const defaults = { 17 - version: '1.0.0', 18 - settings: { 19 - autoSave: true, 20 - enableSync: false, 21 - persistenceType: 'indexeddb', // 'indexeddb', 'sqlite', 'memory' 22 - blobStoragePath: 'datastore/blobs', 23 - contentStoragePath: 'datastore/content', 24 - maxBlobSize: 100 * 1024 * 1024, // 100MB 25 - autoBackup: false, 26 - backupInterval: 24 * 60 * 60 * 1000 // 24 hours 27 - } 28 - };
-275
app/datastore/db.js
··· 1 - // Database module for direct SQLite access 2 - // Replaces TinyBase with better-sqlite3 3 - 4 - import Database from 'better-sqlite3'; 5 - import { createTableStatements, tableNames } from './schema-sql.js'; 6 - 7 - let db = null; 8 - 9 - /** 10 - * Initialize the SQLite database 11 - * @param {string} dbPath - Path to the database file 12 - * @returns {Database} The database instance 13 - */ 14 - export const initDatabase = (dbPath) => { 15 - console.log('main', 'initializing database at:', dbPath); 16 - 17 - db = new Database(dbPath); 18 - 19 - // Enable WAL mode for better concurrent access 20 - db.pragma('journal_mode = WAL'); 21 - 22 - // Create tables and indexes 23 - db.exec(createTableStatements); 24 - 25 - // Migrate from TinyBase if needed 26 - migrateTinyBaseData(); 27 - 28 - console.log('main', 'database initialized successfully'); 29 - return db; 30 - }; 31 - 32 - /** 33 - * One-time migration from TinyBase internal format to direct tables 34 - */ 35 - const migrateTinyBaseData = () => { 36 - // Check if tinybase table exists 37 - const tinybaseExists = db.prepare(` 38 - SELECT name FROM sqlite_master WHERE type='table' AND name='tinybase' 39 - `).get(); 40 - 41 - if (!tinybaseExists) { 42 - return; // No TinyBase data to migrate 43 - } 44 - 45 - // Check if we already migrated (addresses table has data) 46 - const existingData = db.prepare('SELECT COUNT(*) as count FROM addresses').get(); 47 - if (existingData.count > 0) { 48 - console.log('main', 'TinyBase data already migrated, skipping'); 49 - return; 50 - } 51 - 52 - console.log('main', 'Migrating TinyBase data to direct tables...'); 53 - 54 - try { 55 - // Read TinyBase data (stored as JSON in a single row) 56 - const tinybaseRow = db.prepare('SELECT * FROM tinybase').get(); 57 - if (!tinybaseRow) { 58 - console.log('main', 'No TinyBase data found'); 59 - return; 60 - } 61 - 62 - // TinyBase stores data in the second column as JSON array [tables, values] 63 - const rawData = Object.values(tinybaseRow)[1]; 64 - if (!rawData) { 65 - console.log('main', 'TinyBase data is empty'); 66 - return; 67 - } 68 - 69 - const [tables] = JSON.parse(rawData); 70 - if (!tables) { 71 - console.log('main', 'No tables in TinyBase data'); 72 - return; 73 - } 74 - 75 - // Migrate each table 76 - const tablesToMigrate = ['addresses', 'visits', 'tags', 'address_tags', 'extension_settings', 'extensions', 'content', 'blobs', 'scripts_data', 'feeds']; 77 - 78 - for (const tableName of tablesToMigrate) { 79 - const tableData = tables[tableName]; 80 - if (!tableData || typeof tableData !== 'object') continue; 81 - 82 - const entries = Object.entries(tableData); 83 - if (entries.length === 0) continue; 84 - 85 - console.log('main', ` Migrating ${entries.length} rows from ${tableName}`); 86 - 87 - for (const [id, row] of entries) { 88 - try { 89 - const fullRow = { id, ...row }; 90 - const columns = Object.keys(fullRow); 91 - const placeholders = columns.map(() => '?').join(', '); 92 - const values = columns.map(col => fullRow[col]); 93 - 94 - db.prepare(`INSERT OR IGNORE INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`).run(...values); 95 - } catch (err) { 96 - console.error('main', ` Error migrating row ${id} in ${tableName}:`, err.message); 97 - } 98 - } 99 - } 100 - 101 - // Drop the tinybase table after successful migration 102 - db.exec('DROP TABLE IF EXISTS tinybase'); 103 - console.log('main', 'TinyBase migration complete, removed tinybase table'); 104 - 105 - } catch (error) { 106 - console.error('main', 'TinyBase migration failed:', error.message); 107 - } 108 - }; 109 - 110 - /** 111 - * Get the database instance 112 - * @returns {Database|null} 113 - */ 114 - export const getDb = () => db; 115 - 116 - /** 117 - * Close the database connection 118 - */ 119 - export const closeDatabase = () => { 120 - if (db) { 121 - db.close(); 122 - db = null; 123 - console.log('main', 'database closed'); 124 - } 125 - }; 126 - 127 - // Helper functions 128 - 129 - /** 130 - * Generate a unique ID with optional prefix 131 - * @param {string} prefix - Prefix for the ID 132 - * @returns {string} 133 - */ 134 - export const generateId = (prefix = 'id') => { 135 - return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; 136 - }; 137 - 138 - /** 139 - * Get current timestamp in milliseconds 140 - * @returns {number} 141 - */ 142 - export const now = () => Date.now(); 143 - 144 - /** 145 - * Parse a URL into components 146 - * @param {string} uri - The URL to parse 147 - * @returns {{protocol: string, domain: string, path: string}} 148 - */ 149 - export const parseUrl = (uri) => { 150 - try { 151 - const url = new URL(uri); 152 - return { 153 - protocol: url.protocol.replace(':', ''), 154 - domain: url.hostname, 155 - path: url.pathname + url.search + url.hash 156 - }; 157 - } catch (e) { 158 - return { 159 - protocol: 'unknown', 160 - domain: uri, 161 - path: '' 162 - }; 163 - } 164 - }; 165 - 166 - /** 167 - * Normalize a URL for consistent storage 168 - * @param {string} uri - The URL to normalize 169 - * @returns {string} 170 - */ 171 - export const normalizeUrl = (uri) => { 172 - if (!uri) return uri; 173 - 174 - try { 175 - const url = new URL(uri); 176 - 177 - // Remove trailing slash from path (except for root) 178 - if (url.pathname !== '/' && url.pathname.endsWith('/')) { 179 - url.pathname = url.pathname.slice(0, -1); 180 - } 181 - 182 - // Remove default ports 183 - if ((url.protocol === 'http:' && url.port === '80') || 184 - (url.protocol === 'https:' && url.port === '443')) { 185 - url.port = ''; 186 - } 187 - 188 - // Sort query parameters for consistency 189 - if (url.search) { 190 - const params = new URLSearchParams(url.search); 191 - const sortedParams = new URLSearchParams([...params.entries()].sort()); 192 - url.search = sortedParams.toString(); 193 - } 194 - 195 - return url.toString(); 196 - } catch (e) { 197 - return uri; 198 - } 199 - }; 200 - 201 - /** 202 - * Check if a table name is valid 203 - * @param {string} tableName - The table name to validate 204 - * @returns {boolean} 205 - */ 206 - export const isValidTable = (tableName) => { 207 - return tableNames.includes(tableName); 208 - }; 209 - 210 - /** 211 - * Get all rows from a table as an object keyed by ID 212 - * @param {string} tableName - The table name 213 - * @returns {Object} 214 - */ 215 - export const getTableAsObject = (tableName) => { 216 - if (!isValidTable(tableName)) { 217 - throw new Error(`Invalid table name: ${tableName}`); 218 - } 219 - 220 - const rows = db.prepare(`SELECT * FROM ${tableName}`).all(); 221 - const result = {}; 222 - for (const row of rows) { 223 - result[row.id] = row; 224 - } 225 - return result; 226 - }; 227 - 228 - /** 229 - * Get a single row by ID 230 - * @param {string} tableName - The table name 231 - * @param {string} id - The row ID 232 - * @returns {Object|undefined} 233 - */ 234 - export const getRow = (tableName, id) => { 235 - if (!isValidTable(tableName)) { 236 - throw new Error(`Invalid table name: ${tableName}`); 237 - } 238 - 239 - return db.prepare(`SELECT * FROM ${tableName} WHERE id = ?`).get(id); 240 - }; 241 - 242 - /** 243 - * Insert or replace a row 244 - * @param {string} tableName - The table name 245 - * @param {string} id - The row ID 246 - * @param {Object} data - The row data 247 - */ 248 - export const setRow = (tableName, id, data) => { 249 - if (!isValidTable(tableName)) { 250 - throw new Error(`Invalid table name: ${tableName}`); 251 - } 252 - 253 - const row = { id, ...data }; 254 - const columns = Object.keys(row); 255 - const placeholders = columns.map(() => '?').join(', '); 256 - const values = columns.map(col => row[col]); 257 - 258 - const sql = `INSERT OR REPLACE INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`; 259 - db.prepare(sql).run(...values); 260 - }; 261 - 262 - /** 263 - * Delete a row by ID 264 - * @param {string} tableName - The table name 265 - * @param {string} id - The row ID 266 - */ 267 - export const deleteRow = (tableName, id) => { 268 - if (!isValidTable(tableName)) { 269 - throw new Error(`Invalid table name: ${tableName}`); 270 - } 271 - 272 - db.prepare(`DELETE FROM ${tableName} WHERE id = ?`).run(id); 273 - }; 274 - 275 - export { tableNames };
-511
app/datastore/index.js
··· 1 - // Datastore module - TinyBase implementation 2 - 3 - import { createStore, createIndexes, createRelationships, createMetrics } from 'tinybase'; 4 - import { schema, indexes, relationships, metrics } from './schema.js'; 5 - import { id, labels, defaults, storageKeys } from './config.js'; 6 - 7 - console.log('datastore', 'loading'); 8 - 9 - let store = null; 10 - let indexesInstance = null; 11 - let relationshipsInstance = null; 12 - let metricsInstance = null; 13 - 14 - // Generate unique ID 15 - const generateId = (prefix = 'id') => { 16 - return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; 17 - }; 18 - 19 - // Get current timestamp 20 - const now = () => Date.now(); 21 - 22 - // Parse URL to extract components 23 - const parseUrl = (uri) => { 24 - try { 25 - const url = new URL(uri); 26 - return { 27 - protocol: url.protocol.replace(':', ''), 28 - domain: url.hostname, 29 - path: url.pathname + url.search + url.hash 30 - }; 31 - } catch (error) { 32 - // Fallback for invalid URLs 33 - return { 34 - protocol: '', 35 - domain: '', 36 - path: uri 37 - }; 38 - } 39 - }; 40 - 41 - // Initialize the datastore 42 - const init = () => { 43 - console.log('datastore', 'initializing'); 44 - 45 - try { 46 - // Create the store with schema 47 - store = createStore(); 48 - 49 - // Set up the schema 50 - store.setTablesSchema(schema); 51 - 52 - // Create indexes 53 - indexesInstance = createIndexes(store); 54 - Object.entries(indexes).forEach(([indexName, indexConfig]) => { 55 - indexesInstance.setIndexDefinition( 56 - indexName, 57 - indexConfig.table, 58 - indexConfig.on 59 - ); 60 - }); 61 - 62 - // Create relationships 63 - relationshipsInstance = createRelationships(store); 64 - Object.entries(relationships).forEach(([relName, relConfig]) => { 65 - relationshipsInstance.setRelationshipDefinition( 66 - relName, 67 - relConfig.localTableId, 68 - relConfig.remoteTableId, 69 - relConfig.relationshipId 70 - ); 71 - }); 72 - 73 - // Create metrics 74 - metricsInstance = createMetrics(store); 75 - Object.entries(metrics).forEach(([metricName, metricConfig]) => { 76 - if (metricConfig.metric) { 77 - // Aggregate metric on specific cell 78 - metricsInstance.setMetricDefinition( 79 - metricName, 80 - metricConfig.table, 81 - metricConfig.aggregate, 82 - metricConfig.metric 83 - ); 84 - } else { 85 - // Simple count metric 86 - metricsInstance.setMetricDefinition( 87 - metricName, 88 - metricConfig.table, 89 - 'count' 90 - ); 91 - } 92 - }); 93 - 94 - console.log('datastore', 'initialized successfully'); 95 - return true; 96 - } catch (error) { 97 - console.error('datastore', 'initialization failed:', error); 98 - return false; 99 - } 100 - }; 101 - 102 - // Uninitialize (cleanup) 103 - const uninit = () => { 104 - console.log('datastore', 'uninitializing'); 105 - 106 - if (metricsInstance) { 107 - metricsInstance.destroy(); 108 - metricsInstance = null; 109 - } 110 - 111 - if (relationshipsInstance) { 112 - relationshipsInstance.destroy(); 113 - relationshipsInstance = null; 114 - } 115 - 116 - if (indexesInstance) { 117 - indexesInstance.destroy(); 118 - indexesInstance = null; 119 - } 120 - 121 - // Store doesn't have a destroy method in TinyBase, just clear references 122 - if (store) { 123 - store = null; 124 - } 125 - }; 126 - 127 - // ===== CRUD Operations ===== 128 - 129 - // --- Addresses --- 130 - 131 - const addAddress = (uri, data = {}) => { 132 - const parsed = parseUrl(uri); 133 - const addressId = generateId('addr'); 134 - const timestamp = now(); 135 - 136 - const row = { 137 - uri, 138 - protocol: data.protocol || parsed.protocol, 139 - domain: data.domain || parsed.domain, 140 - path: data.path || parsed.path, 141 - title: data.title || '', 142 - mimeType: data.mimeType || 'text/html', 143 - favicon: data.favicon || '', 144 - description: data.description || '', 145 - tags: data.tags || '', 146 - metadata: data.metadata || '{}', 147 - createdAt: timestamp, 148 - updatedAt: timestamp, 149 - lastVisitAt: data.lastVisitAt || 0, 150 - visitCount: data.visitCount || 0, 151 - starred: data.starred || 0, 152 - archived: data.archived || 0 153 - }; 154 - 155 - store.setRow('addresses', addressId, row); 156 - return addressId; 157 - }; 158 - 159 - const getAddress = (addressId) => { 160 - return store.getRow('addresses', addressId); 161 - }; 162 - 163 - const updateAddress = (addressId, data) => { 164 - const existing = getAddress(addressId); 165 - if (!existing) { 166 - throw new Error(`Address ${addressId} not found`); 167 - } 168 - 169 - const updated = { 170 - ...existing, 171 - ...data, 172 - updatedAt: now() 173 - }; 174 - 175 - store.setRow('addresses', addressId, updated); 176 - return updated; 177 - }; 178 - 179 - const deleteAddress = (addressId) => { 180 - store.delRow('addresses', addressId); 181 - }; 182 - 183 - const queryAddresses = (filter = {}) => { 184 - const table = store.getTable('addresses'); 185 - let results = Object.entries(table).map(([id, row]) => ({ id, ...row })); 186 - 187 - // Apply filters 188 - if (filter.domain) { 189 - results = results.filter(addr => addr.domain === filter.domain); 190 - } 191 - if (filter.protocol) { 192 - results = results.filter(addr => addr.protocol === filter.protocol); 193 - } 194 - if (filter.starred !== undefined) { 195 - results = results.filter(addr => addr.starred === filter.starred); 196 - } 197 - if (filter.tag) { 198 - results = results.filter(addr => addr.tags.includes(filter.tag)); 199 - } 200 - 201 - // Sort 202 - if (filter.sortBy === 'lastVisit') { 203 - results.sort((a, b) => b.lastVisitAt - a.lastVisitAt); 204 - } else if (filter.sortBy === 'visitCount') { 205 - results.sort((a, b) => b.visitCount - a.visitCount); 206 - } else if (filter.sortBy === 'created') { 207 - results.sort((a, b) => b.createdAt - a.createdAt); 208 - } 209 - 210 - // Limit 211 - if (filter.limit) { 212 - results = results.slice(0, filter.limit); 213 - } 214 - 215 - return results; 216 - }; 217 - 218 - // --- Visits --- 219 - 220 - const addVisit = (addressId, data = {}) => { 221 - const visitId = generateId('visit'); 222 - const timestamp = now(); 223 - 224 - const row = { 225 - addressId, 226 - timestamp: data.timestamp || timestamp, 227 - duration: data.duration || 0, 228 - source: data.source || 'direct', 229 - sourceId: data.sourceId || '', 230 - windowType: data.windowType || 'main', 231 - metadata: data.metadata || '{}', 232 - scrollDepth: data.scrollDepth || 0, 233 - interacted: data.interacted || 0 234 - }; 235 - 236 - store.setRow('visits', visitId, row); 237 - 238 - // Update address visit stats 239 - const address = getAddress(addressId); 240 - if (address) { 241 - updateAddress(addressId, { 242 - lastVisitAt: timestamp, 243 - visitCount: address.visitCount + 1 244 - }); 245 - } 246 - 247 - return visitId; 248 - }; 249 - 250 - const getVisit = (visitId) => { 251 - return store.getRow('visits', visitId); 252 - }; 253 - 254 - const queryVisits = (filter = {}) => { 255 - const table = store.getTable('visits'); 256 - let results = Object.entries(table).map(([id, row]) => ({ id, ...row })); 257 - 258 - // Apply filters 259 - if (filter.addressId) { 260 - results = results.filter(visit => visit.addressId === filter.addressId); 261 - } 262 - if (filter.source) { 263 - results = results.filter(visit => visit.source === filter.source); 264 - } 265 - if (filter.since) { 266 - const since = typeof filter.since === 'number' ? filter.since : now() - filter.since; 267 - results = results.filter(visit => visit.timestamp >= since); 268 - } 269 - 270 - // Sort by timestamp (most recent first) 271 - results.sort((a, b) => b.timestamp - a.timestamp); 272 - 273 - // Limit 274 - if (filter.limit) { 275 - results = results.slice(0, filter.limit); 276 - } 277 - 278 - return results; 279 - }; 280 - 281 - // --- Content --- 282 - 283 - const addContent = (data = {}) => { 284 - const contentId = generateId('content'); 285 - const timestamp = now(); 286 - 287 - const row = { 288 - title: data.title || 'Untitled', 289 - content: data.content || '', 290 - mimeType: data.mimeType || 'text/plain', 291 - contentType: data.contentType || 'plain', 292 - language: data.language || '', 293 - encoding: data.encoding || 'utf-8', 294 - tags: data.tags || '', 295 - addressRefs: data.addressRefs || '', 296 - parentId: data.parentId || '', 297 - metadata: data.metadata || '{}', 298 - createdAt: timestamp, 299 - updatedAt: timestamp, 300 - syncPath: data.syncPath || '', 301 - synced: data.synced || 0, 302 - starred: data.starred || 0, 303 - archived: data.archived || 0 304 - }; 305 - 306 - store.setRow('content', contentId, row); 307 - return contentId; 308 - }; 309 - 310 - const getContent = (contentId) => { 311 - return store.getRow('content', contentId); 312 - }; 313 - 314 - const updateContent = (contentId, data) => { 315 - const existing = getContent(contentId); 316 - if (!existing) { 317 - throw new Error(`Content ${contentId} not found`); 318 - } 319 - 320 - const updated = { 321 - ...existing, 322 - ...data, 323 - updatedAt: now() 324 - }; 325 - 326 - store.setRow('content', contentId, updated); 327 - return updated; 328 - }; 329 - 330 - const deleteContent = (contentId) => { 331 - store.delRow('content', contentId); 332 - }; 333 - 334 - const queryContent = (filter = {}) => { 335 - const table = store.getTable('content'); 336 - let results = Object.entries(table).map(([id, row]) => ({ id, ...row })); 337 - 338 - // Apply filters 339 - if (filter.contentType) { 340 - results = results.filter(item => item.contentType === filter.contentType); 341 - } 342 - if (filter.mimeType) { 343 - results = results.filter(item => item.mimeType === filter.mimeType); 344 - } 345 - if (filter.synced !== undefined) { 346 - results = results.filter(item => item.synced === filter.synced); 347 - } 348 - if (filter.starred !== undefined) { 349 - results = results.filter(item => item.starred === filter.starred); 350 - } 351 - if (filter.tag) { 352 - results = results.filter(item => item.tags.includes(filter.tag)); 353 - } 354 - 355 - // Sort 356 - if (filter.sortBy === 'updated') { 357 - results.sort((a, b) => b.updatedAt - a.updatedAt); 358 - } else if (filter.sortBy === 'created') { 359 - results.sort((a, b) => b.createdAt - a.createdAt); 360 - } 361 - 362 - // Limit 363 - if (filter.limit) { 364 - results = results.slice(0, filter.limit); 365 - } 366 - 367 - return results; 368 - }; 369 - 370 - // --- Tags --- 371 - 372 - const addTag = (name, data = {}) => { 373 - const tagId = generateId('tag'); 374 - const timestamp = now(); 375 - 376 - // Generate slug from name 377 - const slug = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''); 378 - 379 - const row = { 380 - name, 381 - slug, 382 - color: data.color || '#999999', 383 - parentId: data.parentId || '', 384 - description: data.description || '', 385 - metadata: data.metadata || '{}', 386 - createdAt: timestamp, 387 - updatedAt: timestamp, 388 - usageCount: data.usageCount || 0 389 - }; 390 - 391 - store.setRow('tags', tagId, row); 392 - return tagId; 393 - }; 394 - 395 - const getTag = (tagId) => { 396 - return store.getRow('tags', tagId); 397 - }; 398 - 399 - const getTagByName = (name) => { 400 - const table = store.getTable('tags'); 401 - const entry = Object.entries(table).find(([id, row]) => row.name === name); 402 - return entry ? { id: entry[0], ...entry[1] } : null; 403 - }; 404 - 405 - const updateTag = (tagId, data) => { 406 - const existing = getTag(tagId); 407 - if (!existing) { 408 - throw new Error(`Tag ${tagId} not found`); 409 - } 410 - 411 - const updated = { 412 - ...existing, 413 - ...data, 414 - updatedAt: now() 415 - }; 416 - 417 - store.setRow('tags', tagId, updated); 418 - return updated; 419 - }; 420 - 421 - const deleteTag = (tagId) => { 422 - store.delRow('tags', tagId); 423 - }; 424 - 425 - const queryTags = (filter = {}) => { 426 - const table = store.getTable('tags'); 427 - let results = Object.entries(table).map(([id, row]) => ({ id, ...row })); 428 - 429 - // Apply filters 430 - if (filter.parentId !== undefined) { 431 - results = results.filter(tag => tag.parentId === filter.parentId); 432 - } 433 - 434 - // Sort 435 - if (filter.sortBy === 'usage') { 436 - results.sort((a, b) => b.usageCount - a.usageCount); 437 - } else if (filter.sortBy === 'name') { 438 - results.sort((a, b) => a.name.localeCompare(b.name)); 439 - } 440 - 441 - return results; 442 - }; 443 - 444 - // --- Utility functions --- 445 - 446 - const getStats = () => { 447 - if (!metricsInstance) { 448 - return {}; 449 - } 450 - 451 - return { 452 - totalAddresses: metricsInstance.getMetric('totalAddresses'), 453 - totalVisits: metricsInstance.getMetric('totalVisits'), 454 - avgVisitDuration: metricsInstance.getMetric('avgVisitDuration'), 455 - totalBlobSize: metricsInstance.getMetric('totalBlobSize'), 456 - totalContent: metricsInstance.getMetric('totalContent'), 457 - syncedContent: metricsInstance.getMetric('syncedContent') 458 - }; 459 - }; 460 - 461 - const getStore = () => store; 462 - const getIndexes = () => indexesInstance; 463 - const getRelationships = () => relationshipsInstance; 464 - const getMetrics = () => metricsInstance; 465 - 466 - // Export datastore API 467 - export default { 468 - // Lifecycle 469 - init, 470 - uninit, 471 - 472 - // Addresses 473 - addAddress, 474 - getAddress, 475 - updateAddress, 476 - deleteAddress, 477 - queryAddresses, 478 - 479 - // Visits 480 - addVisit, 481 - getVisit, 482 - queryVisits, 483 - 484 - // Content 485 - addContent, 486 - getContent, 487 - updateContent, 488 - deleteContent, 489 - queryContent, 490 - 491 - // Tags 492 - addTag, 493 - getTag, 494 - getTagByName, 495 - updateTag, 496 - deleteTag, 497 - queryTags, 498 - 499 - // Stats & utilities 500 - getStats, 501 - getStore, 502 - getIndexes, 503 - getRelationships, 504 - getMetrics, 505 - 506 - // Config 507 - id, 508 - labels, 509 - defaults, 510 - storageKeys 511 - };
-224
app/datastore/schema-sql.js
··· 1 - // SQL Schema definitions for direct SQLite 2 - // Converted from TinyBase schema.js 3 - 4 - export const createTableStatements = ` 5 - -- Addresses: URLs with metadata 6 - CREATE TABLE IF NOT EXISTS addresses ( 7 - id TEXT PRIMARY KEY, 8 - uri TEXT NOT NULL, 9 - protocol TEXT DEFAULT 'https', 10 - domain TEXT, 11 - path TEXT DEFAULT '', 12 - title TEXT DEFAULT '', 13 - mimeType TEXT DEFAULT 'text/html', 14 - favicon TEXT DEFAULT '', 15 - description TEXT DEFAULT '', 16 - tags TEXT DEFAULT '', 17 - metadata TEXT DEFAULT '{}', 18 - createdAt INTEGER, 19 - updatedAt INTEGER, 20 - lastVisitAt INTEGER DEFAULT 0, 21 - visitCount INTEGER DEFAULT 0, 22 - starred INTEGER DEFAULT 0, 23 - archived INTEGER DEFAULT 0 24 - ); 25 - 26 - CREATE INDEX IF NOT EXISTS idx_addresses_uri ON addresses(uri); 27 - CREATE INDEX IF NOT EXISTS idx_addresses_domain ON addresses(domain); 28 - CREATE INDEX IF NOT EXISTS idx_addresses_protocol ON addresses(protocol); 29 - CREATE INDEX IF NOT EXISTS idx_addresses_lastVisitAt ON addresses(lastVisitAt); 30 - CREATE INDEX IF NOT EXISTS idx_addresses_visitCount ON addresses(visitCount); 31 - CREATE INDEX IF NOT EXISTS idx_addresses_starred ON addresses(starred); 32 - 33 - -- Visits: Navigation history linked to addresses 34 - CREATE TABLE IF NOT EXISTS visits ( 35 - id TEXT PRIMARY KEY, 36 - addressId TEXT, 37 - timestamp INTEGER, 38 - duration INTEGER DEFAULT 0, 39 - source TEXT DEFAULT 'direct', 40 - sourceId TEXT DEFAULT '', 41 - windowType TEXT DEFAULT 'main', 42 - metadata TEXT DEFAULT '{}', 43 - scrollDepth INTEGER DEFAULT 0, 44 - interacted INTEGER DEFAULT 0 45 - ); 46 - 47 - CREATE INDEX IF NOT EXISTS idx_visits_addressId ON visits(addressId); 48 - CREATE INDEX IF NOT EXISTS idx_visits_timestamp ON visits(timestamp); 49 - CREATE INDEX IF NOT EXISTS idx_visits_source ON visits(source); 50 - 51 - -- Content: User-created content (notes, etc.) 52 - CREATE TABLE IF NOT EXISTS content ( 53 - id TEXT PRIMARY KEY, 54 - title TEXT DEFAULT 'Untitled', 55 - content TEXT DEFAULT '', 56 - mimeType TEXT DEFAULT 'text/plain', 57 - contentType TEXT DEFAULT 'plain', 58 - language TEXT DEFAULT '', 59 - encoding TEXT DEFAULT 'utf-8', 60 - tags TEXT DEFAULT '', 61 - addressRefs TEXT DEFAULT '', 62 - parentId TEXT DEFAULT '', 63 - metadata TEXT DEFAULT '{}', 64 - createdAt INTEGER, 65 - updatedAt INTEGER, 66 - syncPath TEXT DEFAULT '', 67 - synced INTEGER DEFAULT 0, 68 - starred INTEGER DEFAULT 0, 69 - archived INTEGER DEFAULT 0 70 - ); 71 - 72 - CREATE INDEX IF NOT EXISTS idx_content_contentType ON content(contentType); 73 - CREATE INDEX IF NOT EXISTS idx_content_mimeType ON content(mimeType); 74 - CREATE INDEX IF NOT EXISTS idx_content_synced ON content(synced); 75 - CREATE INDEX IF NOT EXISTS idx_content_updatedAt ON content(updatedAt); 76 - 77 - -- Tags: Tag definitions with frecency tracking 78 - CREATE TABLE IF NOT EXISTS tags ( 79 - id TEXT PRIMARY KEY, 80 - name TEXT NOT NULL, 81 - slug TEXT, 82 - color TEXT DEFAULT '#999999', 83 - parentId TEXT DEFAULT '', 84 - description TEXT DEFAULT '', 85 - metadata TEXT DEFAULT '{}', 86 - createdAt INTEGER, 87 - updatedAt INTEGER, 88 - frequency INTEGER DEFAULT 0, 89 - lastUsedAt INTEGER DEFAULT 0, 90 - frecencyScore INTEGER DEFAULT 0 91 - ); 92 - 93 - CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name); 94 - CREATE INDEX IF NOT EXISTS idx_tags_slug ON tags(slug); 95 - CREATE INDEX IF NOT EXISTS idx_tags_parentId ON tags(parentId); 96 - CREATE INDEX IF NOT EXISTS idx_tags_frecencyScore ON tags(frecencyScore); 97 - 98 - -- Address-Tag join table 99 - CREATE TABLE IF NOT EXISTS address_tags ( 100 - id TEXT PRIMARY KEY, 101 - addressId TEXT NOT NULL, 102 - tagId TEXT NOT NULL, 103 - createdAt INTEGER 104 - ); 105 - 106 - CREATE INDEX IF NOT EXISTS idx_address_tags_addressId ON address_tags(addressId); 107 - CREATE INDEX IF NOT EXISTS idx_address_tags_tagId ON address_tags(tagId); 108 - CREATE UNIQUE INDEX IF NOT EXISTS idx_address_tags_unique ON address_tags(addressId, tagId); 109 - 110 - -- Blobs: Binary files/media 111 - CREATE TABLE IF NOT EXISTS blobs ( 112 - id TEXT PRIMARY KEY, 113 - filename TEXT, 114 - mimeType TEXT, 115 - mediaType TEXT, 116 - size INTEGER, 117 - hash TEXT, 118 - extension TEXT, 119 - path TEXT, 120 - addressId TEXT DEFAULT '', 121 - contentId TEXT DEFAULT '', 122 - tags TEXT DEFAULT '', 123 - metadata TEXT DEFAULT '{}', 124 - createdAt INTEGER, 125 - width INTEGER DEFAULT 0, 126 - height INTEGER DEFAULT 0, 127 - duration INTEGER DEFAULT 0, 128 - thumbnail TEXT DEFAULT '' 129 - ); 130 - 131 - CREATE INDEX IF NOT EXISTS idx_blobs_mediaType ON blobs(mediaType); 132 - CREATE INDEX IF NOT EXISTS idx_blobs_mimeType ON blobs(mimeType); 133 - CREATE INDEX IF NOT EXISTS idx_blobs_addressId ON blobs(addressId); 134 - CREATE INDEX IF NOT EXISTS idx_blobs_contentId ON blobs(contentId); 135 - 136 - -- Scripts data: Script execution results 137 - CREATE TABLE IF NOT EXISTS scripts_data ( 138 - id TEXT PRIMARY KEY, 139 - scriptId TEXT, 140 - scriptName TEXT, 141 - addressId TEXT, 142 - selector TEXT, 143 - content TEXT, 144 - contentType TEXT DEFAULT 'text', 145 - metadata TEXT DEFAULT '{}', 146 - extractedAt INTEGER, 147 - previousValue TEXT DEFAULT '', 148 - changed INTEGER DEFAULT 0 149 - ); 150 - 151 - CREATE INDEX IF NOT EXISTS idx_scripts_data_scriptId ON scripts_data(scriptId); 152 - CREATE INDEX IF NOT EXISTS idx_scripts_data_addressId ON scripts_data(addressId); 153 - CREATE INDEX IF NOT EXISTS idx_scripts_data_changed ON scripts_data(changed); 154 - 155 - -- Feeds: Feed definitions 156 - CREATE TABLE IF NOT EXISTS feeds ( 157 - id TEXT PRIMARY KEY, 158 - name TEXT, 159 - description TEXT DEFAULT '', 160 - type TEXT, 161 - query TEXT DEFAULT '', 162 - schedule TEXT DEFAULT '', 163 - source TEXT DEFAULT 'internal', 164 - tags TEXT DEFAULT '', 165 - metadata TEXT DEFAULT '{}', 166 - createdAt INTEGER, 167 - updatedAt INTEGER, 168 - lastFetchedAt INTEGER DEFAULT 0, 169 - enabled INTEGER DEFAULT 1 170 - ); 171 - 172 - CREATE INDEX IF NOT EXISTS idx_feeds_type ON feeds(type); 173 - CREATE INDEX IF NOT EXISTS idx_feeds_enabled ON feeds(enabled); 174 - 175 - -- Extensions: Extension registry 176 - CREATE TABLE IF NOT EXISTS extensions ( 177 - id TEXT PRIMARY KEY, 178 - name TEXT, 179 - description TEXT DEFAULT '', 180 - version TEXT DEFAULT '1.0.0', 181 - path TEXT, 182 - backgroundUrl TEXT DEFAULT '', 183 - settingsUrl TEXT DEFAULT '', 184 - iconPath TEXT DEFAULT '', 185 - builtin INTEGER DEFAULT 0, 186 - enabled INTEGER DEFAULT 1, 187 - status TEXT DEFAULT 'installed', 188 - installedAt INTEGER, 189 - updatedAt INTEGER, 190 - lastErrorAt INTEGER DEFAULT 0, 191 - lastError TEXT DEFAULT '', 192 - metadata TEXT DEFAULT '{}' 193 - ); 194 - 195 - CREATE INDEX IF NOT EXISTS idx_extensions_enabled ON extensions(enabled); 196 - CREATE INDEX IF NOT EXISTS idx_extensions_status ON extensions(status); 197 - CREATE INDEX IF NOT EXISTS idx_extensions_builtin ON extensions(builtin); 198 - 199 - -- Extension settings: Key-value storage for extension preferences 200 - CREATE TABLE IF NOT EXISTS extension_settings ( 201 - id TEXT PRIMARY KEY, 202 - extensionId TEXT NOT NULL, 203 - key TEXT NOT NULL, 204 - value TEXT, 205 - updatedAt INTEGER 206 - ); 207 - 208 - CREATE INDEX IF NOT EXISTS idx_extension_settings_extensionId ON extension_settings(extensionId); 209 - CREATE UNIQUE INDEX IF NOT EXISTS idx_extension_settings_unique ON extension_settings(extensionId, key); 210 - `; 211 - 212 - // List of all tables for validation and iteration 213 - export const tableNames = [ 214 - 'addresses', 215 - 'visits', 216 - 'content', 217 - 'tags', 218 - 'address_tags', 219 - 'blobs', 220 - 'scripts_data', 221 - 'feeds', 222 - 'extensions', 223 - 'extension_settings' 224 - ];
-394
app/datastore/schema.js
··· 1 - // Datastore schema definitions for TinyBase 2 - // 3 - // STORAGE NOTES (Dec 2024): 4 - // 5 - // Current state: 6 - // - TinyBase datastore: SQLite-backed via sqlite3 + createSqlite3Persister 7 - // - Stores: addresses, visits, content, tags, blobs, scripts_data, feeds 8 - // - Location: {userData}/{PROFILE}/datastore.sqlite 9 - // 10 - // - Feature settings: localStorage via openStore() in app/utils.js 11 - // - Stores: peeks, slides, scripts, cmd, groups configs 12 - // 13 - // - Adaptive matching (cmd): localStorage 14 - // - Stores: typed -> command selection feedback 15 - // 16 - // - App prefs: localStorage 17 - // - Stores: shortcuts, window size, startup feature, etc. 18 - // 19 - // Future consideration: 20 - // - May move away from TinyBase toward loosely-coupled SQLite everywhere 21 - // - Some storage should converge, but not all 22 - // - localStorage items that need to scale (like history) should move to SQLite 23 - // - Simple key-value prefs can stay in localStorage 24 - // 25 - // URL SOURCE TAXONOMY: 26 - // The visits table tracks where each URL navigation came from via source/sourceId fields: 27 - // 28 - // | Source | SourceId | Description | 29 - // |------------|---------------|------------------------------------------------| 30 - // | external | os | URL opened from another app via OS handler | 31 - // | external | cli | URL passed as CLI argument | 32 - // | cmd | open | User typed URL in cmd bar | 33 - // | cmd | history | User selected from history command | 34 - // | peek | <peek-id> | Opened from a peek | 35 - // | slide | <slide-id> | Opened from a slide | 36 - // | link | <source-url> | Link clicked in peek:// content | 37 - // | window | '' | Default fallback | 38 - 39 - export const schema = { 40 - addresses: { 41 - uri: { type: 'string' }, 42 - protocol: { type: 'string', default: 'https' }, 43 - domain: { type: 'string' }, 44 - path: { type: 'string', default: '' }, 45 - title: { type: 'string', default: '' }, 46 - mimeType: { type: 'string', default: 'text/html' }, 47 - favicon: { type: 'string', default: '' }, 48 - description: { type: 'string', default: '' }, 49 - tags: { type: 'string', default: '' }, 50 - metadata: { type: 'string', default: '{}' }, 51 - createdAt: { type: 'number' }, 52 - updatedAt: { type: 'number' }, 53 - lastVisitAt: { type: 'number', default: 0 }, 54 - visitCount: { type: 'number', default: 0 }, 55 - starred: { type: 'number', default: 0 }, 56 - archived: { type: 'number', default: 0 } 57 - }, 58 - 59 - visits: { 60 - addressId: { type: 'string' }, 61 - timestamp: { type: 'number' }, 62 - duration: { type: 'number', default: 0 }, 63 - source: { type: 'string', default: 'direct' }, 64 - sourceId: { type: 'string', default: '' }, 65 - windowType: { type: 'string', default: 'main' }, 66 - metadata: { type: 'string', default: '{}' }, 67 - scrollDepth: { type: 'number', default: 0 }, 68 - interacted: { type: 'number', default: 0 } 69 - }, 70 - 71 - content: { 72 - title: { type: 'string', default: 'Untitled' }, 73 - content: { type: 'string', default: '' }, 74 - mimeType: { type: 'string', default: 'text/plain' }, 75 - contentType: { type: 'string', default: 'plain' }, 76 - language: { type: 'string', default: '' }, 77 - encoding: { type: 'string', default: 'utf-8' }, 78 - tags: { type: 'string', default: '' }, 79 - addressRefs: { type: 'string', default: '' }, 80 - parentId: { type: 'string', default: '' }, 81 - metadata: { type: 'string', default: '{}' }, 82 - createdAt: { type: 'number' }, 83 - updatedAt: { type: 'number' }, 84 - syncPath: { type: 'string', default: '' }, 85 - synced: { type: 'number', default: 0 }, 86 - starred: { type: 'number', default: 0 }, 87 - archived: { type: 'number', default: 0 } 88 - }, 89 - 90 - tags: { 91 - name: { type: 'string' }, 92 - slug: { type: 'string' }, 93 - color: { type: 'string', default: '#999999' }, 94 - parentId: { type: 'string', default: '' }, 95 - description: { type: 'string', default: '' }, 96 - metadata: { type: 'string', default: '{}' }, 97 - createdAt: { type: 'number' }, 98 - updatedAt: { type: 'number' }, 99 - frequency: { type: 'number', default: 0 }, 100 - lastUsedAt: { type: 'number', default: 0 }, 101 - frecencyScore: { type: 'number', default: 0 } 102 - }, 103 - 104 - // Join table for address-tag relationships 105 - address_tags: { 106 - addressId: { type: 'string' }, 107 - tagId: { type: 'string' }, 108 - createdAt: { type: 'number' } 109 - }, 110 - 111 - blobs: { 112 - filename: { type: 'string' }, 113 - mimeType: { type: 'string' }, 114 - mediaType: { type: 'string' }, 115 - size: { type: 'number' }, 116 - hash: { type: 'string' }, 117 - extension: { type: 'string' }, 118 - path: { type: 'string' }, 119 - addressId: { type: 'string', default: '' }, 120 - contentId: { type: 'string', default: '' }, 121 - tags: { type: 'string', default: '' }, 122 - metadata: { type: 'string', default: '{}' }, 123 - createdAt: { type: 'number' }, 124 - width: { type: 'number', default: 0 }, 125 - height: { type: 'number', default: 0 }, 126 - duration: { type: 'number', default: 0 }, 127 - thumbnail: { type: 'string', default: '' } 128 - }, 129 - 130 - scripts_data: { 131 - scriptId: { type: 'string' }, 132 - scriptName: { type: 'string' }, 133 - addressId: { type: 'string' }, 134 - selector: { type: 'string' }, 135 - content: { type: 'string' }, 136 - contentType: { type: 'string', default: 'text' }, 137 - metadata: { type: 'string', default: '{}' }, 138 - extractedAt: { type: 'number' }, 139 - previousValue: { type: 'string', default: '' }, 140 - changed: { type: 'number', default: 0 } 141 - }, 142 - 143 - feeds: { 144 - name: { type: 'string' }, 145 - description: { type: 'string', default: '' }, 146 - type: { type: 'string' }, 147 - query: { type: 'string', default: '' }, 148 - schedule: { type: 'string', default: '' }, 149 - source: { type: 'string', default: 'internal' }, 150 - tags: { type: 'string', default: '' }, 151 - metadata: { type: 'string', default: '{}' }, 152 - createdAt: { type: 'number' }, 153 - updatedAt: { type: 'number' }, 154 - lastFetchedAt: { type: 'number', default: 0 }, 155 - enabled: { type: 'number', default: 1 } 156 - }, 157 - 158 - // Extensions registry 159 - extensions: { 160 - name: { type: 'string' }, 161 - description: { type: 'string', default: '' }, 162 - version: { type: 'string', default: '1.0.0' }, 163 - path: { type: 'string' }, // Filesystem path to extension folder 164 - backgroundUrl: { type: 'string', default: '' }, // peek://ext/{id}/background.js 165 - settingsUrl: { type: 'string', default: '' }, // peek://ext/{id}/settings.html 166 - iconPath: { type: 'string', default: '' }, 167 - builtin: { type: 'number', default: 0 }, // 1 if built-in extension 168 - enabled: { type: 'number', default: 1 }, 169 - status: { type: 'string', default: 'installed' }, // installed, running, suspended, error 170 - installedAt: { type: 'number' }, 171 - updatedAt: { type: 'number' }, 172 - lastErrorAt: { type: 'number', default: 0 }, 173 - lastError: { type: 'string', default: '' }, 174 - metadata: { type: 'string', default: '{}' } 175 - }, 176 - 177 - // Extension settings storage (replaces localStorage for cross-origin access) 178 - extension_settings: { 179 - extensionId: { type: 'string' }, // Extension ID (e.g., 'peeks', 'slides') 180 - key: { type: 'string' }, // Setting key (e.g., 'prefs', 'items') 181 - value: { type: 'string' }, // JSON stringified value 182 - updatedAt: { type: 'number' } 183 - } 184 - }; 185 - 186 - // Index definitions 187 - export const indexes = { 188 - // Address indexes 189 - addresses_byDomain: { 190 - table: 'addresses', 191 - on: 'domain' 192 - }, 193 - addresses_byProtocol: { 194 - table: 'addresses', 195 - on: 'protocol' 196 - }, 197 - addresses_byStarred: { 198 - table: 'addresses', 199 - on: 'starred' 200 - }, 201 - 202 - // Visit indexes 203 - visits_byAddress: { 204 - table: 'visits', 205 - on: 'addressId' 206 - }, 207 - visits_byTimestamp: { 208 - table: 'visits', 209 - on: 'timestamp' 210 - }, 211 - visits_bySource: { 212 - table: 'visits', 213 - on: 'source' 214 - }, 215 - 216 - // Content indexes 217 - content_byContentType: { 218 - table: 'content', 219 - on: 'contentType' 220 - }, 221 - content_byMimeType: { 222 - table: 'content', 223 - on: 'mimeType' 224 - }, 225 - content_bySynced: { 226 - table: 'content', 227 - on: 'synced' 228 - }, 229 - content_byUpdated: { 230 - table: 'content', 231 - on: 'updatedAt' 232 - }, 233 - 234 - // Tag indexes 235 - tags_byName: { 236 - table: 'tags', 237 - on: 'name' 238 - }, 239 - tags_byParent: { 240 - table: 'tags', 241 - on: 'parentId' 242 - }, 243 - tags_byFrecency: { 244 - table: 'tags', 245 - on: 'frecencyScore' 246 - }, 247 - 248 - // Address-tag join indexes 249 - address_tags_byAddress: { 250 - table: 'address_tags', 251 - on: 'addressId' 252 - }, 253 - address_tags_byTag: { 254 - table: 'address_tags', 255 - on: 'tagId' 256 - }, 257 - 258 - // Blob indexes 259 - blobs_byMediaType: { 260 - table: 'blobs', 261 - on: 'mediaType' 262 - }, 263 - blobs_byMimeType: { 264 - table: 'blobs', 265 - on: 'mimeType' 266 - }, 267 - 268 - // Scripts data indexes 269 - scripts_data_byScript: { 270 - table: 'scripts_data', 271 - on: 'scriptId' 272 - }, 273 - scripts_data_byChanged: { 274 - table: 'scripts_data', 275 - on: 'changed' 276 - }, 277 - 278 - // Feed indexes 279 - feeds_byType: { 280 - table: 'feeds', 281 - on: 'type' 282 - }, 283 - feeds_byEnabled: { 284 - table: 'feeds', 285 - on: 'enabled' 286 - }, 287 - 288 - // Extension indexes 289 - extensions_byEnabled: { 290 - table: 'extensions', 291 - on: 'enabled' 292 - }, 293 - extensions_byStatus: { 294 - table: 'extensions', 295 - on: 'status' 296 - }, 297 - extensions_byBuiltin: { 298 - table: 'extensions', 299 - on: 'builtin' 300 - }, 301 - 302 - // Extension settings indexes 303 - extension_settings_byExtension: { 304 - table: 'extension_settings', 305 - on: 'extensionId' 306 - } 307 - }; 308 - 309 - // Relationship definitions 310 - export const relationships = { 311 - // Visits to their addresses 312 - visitAddress: { 313 - localTableId: 'visits', 314 - remoteTableId: 'addresses', 315 - relationshipId: 'addressId' 316 - }, 317 - 318 - // Blobs to their source addresses 319 - blobAddress: { 320 - localTableId: 'blobs', 321 - remoteTableId: 'addresses', 322 - relationshipId: 'addressId' 323 - }, 324 - 325 - // Blobs to their content 326 - blobContent: { 327 - localTableId: 'blobs', 328 - remoteTableId: 'content', 329 - relationshipId: 'contentId' 330 - }, 331 - 332 - // Scripts data to addresses 333 - scriptDataAddress: { 334 - localTableId: 'scripts_data', 335 - remoteTableId: 'addresses', 336 - relationshipId: 'addressId' 337 - }, 338 - 339 - // Tag hierarchy (self-referential) 340 - childTags: { 341 - localTableId: 'tags', 342 - remoteTableId: 'tags', 343 - relationshipId: 'parentId' 344 - }, 345 - 346 - // Content hierarchy (self-referential) 347 - childContent: { 348 - localTableId: 'content', 349 - remoteTableId: 'content', 350 - relationshipId: 'parentId' 351 - } 352 - }; 353 - 354 - // Metric definitions 355 - export const metrics = { 356 - // Total addresses 357 - totalAddresses: { 358 - table: 'addresses', 359 - aggregate: 'count' 360 - }, 361 - 362 - // Total visits 363 - totalVisits: { 364 - table: 'visits', 365 - aggregate: 'count' 366 - }, 367 - 368 - // Average visit duration 369 - avgVisitDuration: { 370 - table: 'visits', 371 - metric: 'duration', 372 - aggregate: 'avg' 373 - }, 374 - 375 - // Total storage used by blobs 376 - totalBlobSize: { 377 - table: 'blobs', 378 - metric: 'size', 379 - aggregate: 'sum' 380 - }, 381 - 382 - // Number of content items 383 - totalContent: { 384 - table: 'content', 385 - aggregate: 'count' 386 - }, 387 - 388 - // Number of synced content items 389 - syncedContent: { 390 - table: 'content', 391 - where: { synced: 1 }, 392 - aggregate: 'count' 393 - } 394 - };
-145
app/datastore/test-ipc.html
··· 1 - <!DOCTYPE html> 2 - <html> 3 - <head> 4 - <meta charset="UTF-8"> 5 - <meta http-equiv="Content-Security-Policy" content="script-src 'self';"> 6 - <title>Datastore IPC Test</title> 7 - <style> 8 - body { 9 - font-family: system-ui, -apple-system, sans-serif; 10 - padding: 20px; 11 - background: #1e1e1e; 12 - color: #d4d4d4; 13 - } 14 - h1 { 15 - color: #4ec9b0; 16 - } 17 - .test { 18 - margin: 10px 0; 19 - padding: 10px; 20 - background: #252526; 21 - border-left: 3px solid #007acc; 22 - } 23 - .test.success { 24 - border-left-color: #4ec9b0; 25 - } 26 - .test.error { 27 - border-left-color: #f48771; 28 - } 29 - pre { 30 - background: #1e1e1e; 31 - padding: 10px; 32 - overflow-x: auto; 33 - font-size: 12px; 34 - } 35 - </style> 36 - </head> 37 - <body> 38 - <h1>Datastore IPC Test</h1> 39 - <div id="results"></div> 40 - 41 - <script type="module"> 42 - const results = document.getElementById('results'); 43 - 44 - function addResult(name, success, data) { 45 - const div = document.createElement('div'); 46 - div.className = `test ${success ? 'success' : 'error'}`; 47 - div.innerHTML = ` 48 - <strong>${success ? '✓' : '✗'} ${name}</strong> 49 - <pre>${JSON.stringify(data, null, 2)}</pre> 50 - `; 51 - results.appendChild(div); 52 - } 53 - 54 - async function runTests() { 55 - try { 56 - // Test 1: Check if api.datastore exists 57 - const hasDatastore = window.api && window.api.datastore; 58 - addResult('API available', hasDatastore, { 59 - hasApi: !!window.api, 60 - hasDatastore: hasDatastore, 61 - methods: hasDatastore ? Object.keys(window.api.datastore) : [] 62 - }); 63 - 64 - if (!hasDatastore) { 65 - throw new Error('api.datastore not available'); 66 - } 67 - 68 - // Test 2: Add an address 69 - const testUri = 'https://example.com/test'; 70 - const addResult = await window.api.datastore.addAddress(testUri, { 71 - title: 'Test Address', 72 - mimeType: 'text/html' 73 - }); 74 - addResult('Add address', addResult.success, addResult); 75 - 76 - const addressId = addResult.id; 77 - 78 - // Test 3: Get the address 79 - const getResult = await window.api.datastore.getAddress(addressId); 80 - addResult('Get address', getResult.success, getResult); 81 - 82 - // Test 4: Update the address 83 - const updateResult = await window.api.datastore.updateAddress(addressId, { 84 - title: 'Updated Test Address' 85 - }); 86 - addResult('Update address', updateResult.success, updateResult); 87 - 88 - // Test 5: Query addresses 89 - const queryResult = await window.api.datastore.queryAddresses({}); 90 - addResult('Query addresses', queryResult.success, { 91 - ...queryResult, 92 - count: queryResult.data ? queryResult.data.length : 0 93 - }); 94 - 95 - // Test 6: Add a visit 96 - const visitResult = await window.api.datastore.addVisit(addressId, { 97 - source: 'test', 98 - sourceId: 'test_ipc', 99 - windowType: 'modal', 100 - duration: 5000 101 - }); 102 - addResult('Add visit', visitResult.success, visitResult); 103 - 104 - // Test 7: Query visits 105 - const visitsResult = await window.api.datastore.queryVisits({}); 106 - addResult('Query visits', visitsResult.success, { 107 - ...visitsResult, 108 - count: visitsResult.data ? visitsResult.data.length : 0 109 - }); 110 - 111 - // Test 8: Get stats 112 - const statsResult = await window.api.datastore.getStats(); 113 - addResult('Get stats', statsResult.success, statsResult); 114 - 115 - // Test 9: Add content 116 - const contentResult = await window.api.datastore.addContent({ 117 - title: 'Test Note', 118 - content: 'This is a test note created via IPC', 119 - contentType: 'markdown', 120 - mimeType: 'text/markdown' 121 - }); 122 - addResult('Add content', contentResult.success, contentResult); 123 - 124 - // Test 10: Get table 125 - const tableResult = await window.api.datastore.getTable('addresses'); 126 - addResult('Get table', tableResult.success, { 127 - ...tableResult, 128 - rowCount: tableResult.data ? Object.keys(tableResult.data).length : 0 129 - }); 130 - 131 - console.log('All tests completed!'); 132 - 133 - } catch (error) { 134 - addResult('Test execution', false, { 135 - error: error.message, 136 - stack: error.stack 137 - }); 138 - } 139 - } 140 - 141 - // Run tests when page loads 142 - runTests(); 143 - </script> 144 - </body> 145 - </html>
-189
app/datastore/test.js
··· 1 - // Test file for datastore module 2 - 3 - import datastore from './index.js'; 4 - 5 - console.log('===== Datastore Test Suite =====\n'); 6 - 7 - // Initialize the datastore 8 - console.log('1. Initializing datastore...'); 9 - const initResult = datastore.init(); 10 - console.log(' Result:', initResult ? 'SUCCESS' : 'FAILED'); 11 - 12 - if (!initResult) { 13 - console.error('Failed to initialize datastore'); 14 - process.exit(1); 15 - } 16 - 17 - // Test Addresses 18 - console.log('\n2. Testing Addresses...'); 19 - const addr1 = datastore.addAddress('https://example.com/article', { 20 - title: 'Example Article', 21 - description: 'An example article', 22 - tags: 'tag_1,tag_2' 23 - }); 24 - console.log(' Added address:', addr1); 25 - 26 - const addr2 = datastore.addAddress('https://github.com/project', { 27 - title: 'GitHub Project', 28 - starred: 1 29 - }); 30 - console.log(' Added address:', addr2); 31 - 32 - const retrievedAddr = datastore.getAddress(addr1); 33 - console.log(' Retrieved address:', retrievedAddr); 34 - 35 - datastore.updateAddress(addr1, { 36 - description: 'Updated description', 37 - starred: 1 38 - }); 39 - console.log(' Updated address'); 40 - 41 - const allAddresses = datastore.queryAddresses(); 42 - console.log(' Total addresses:', allAddresses.length); 43 - 44 - const starredAddresses = datastore.queryAddresses({ starred: 1 }); 45 - console.log(' Starred addresses:', starredAddresses.length); 46 - 47 - // Test Visits 48 - console.log('\n3. Testing Visits...'); 49 - const visit1 = datastore.addVisit(addr1, { 50 - duration: 45000, 51 - source: 'peek', 52 - sourceId: 'peek_1', 53 - windowType: 'modal' 54 - }); 55 - console.log(' Added visit:', visit1); 56 - 57 - const visit2 = datastore.addVisit(addr1, { 58 - duration: 120000, 59 - source: 'direct' 60 - }); 61 - console.log(' Added visit:', visit2); 62 - 63 - const recentVisits = datastore.queryVisits({ limit: 10 }); 64 - console.log(' Recent visits:', recentVisits.length); 65 - 66 - const addr1Visits = datastore.queryVisits({ addressId: addr1 }); 67 - console.log(' Visits to address 1:', addr1Visits.length); 68 - 69 - // Verify address visit count was updated 70 - const updatedAddr1 = datastore.getAddress(addr1); 71 - console.log(' Address visit count:', updatedAddr1.visitCount); 72 - 73 - // Test Content 74 - console.log('\n4. Testing Content...'); 75 - const content1 = datastore.addContent({ 76 - title: 'My First Note', 77 - content: '# Hello World\n\nThis is my first note.', 78 - contentType: 'markdown', 79 - mimeType: 'text/markdown', 80 - tags: 'tag_1' 81 - }); 82 - console.log(' Added markdown content:', content1); 83 - 84 - const content2 = datastore.addContent({ 85 - title: 'Product Prices', 86 - content: 'product,price\nWidget,19.99\nGadget,29.99', 87 - contentType: 'csv', 88 - mimeType: 'text/csv', 89 - addressRefs: addr2 90 - }); 91 - console.log(' Added CSV content:', content2); 92 - 93 - const content3 = datastore.addContent({ 94 - title: 'Helper Function', 95 - content: 'function add(a, b) { return a + b; }', 96 - contentType: 'code', 97 - mimeType: 'text/javascript', 98 - language: 'javascript', 99 - starred: 1 100 - }); 101 - console.log(' Added code snippet:', content3); 102 - 103 - datastore.updateContent(content1, { 104 - content: '# Hello World\n\nThis is my updated note.', 105 - synced: 1, 106 - syncPath: 'notes/first-note.md' 107 - }); 108 - console.log(' Updated content with sync info'); 109 - 110 - const allContent = datastore.queryContent(); 111 - console.log(' Total content items:', allContent.length); 112 - 113 - const markdownContent = datastore.queryContent({ contentType: 'markdown' }); 114 - console.log(' Markdown content:', markdownContent.length); 115 - 116 - const syncedContent = datastore.queryContent({ synced: 1 }); 117 - console.log(' Synced content:', syncedContent.length); 118 - 119 - const starredContent = datastore.queryContent({ starred: 1 }); 120 - console.log(' Starred content:', starredContent.length); 121 - 122 - // Test Tags 123 - console.log('\n5. Testing Tags...'); 124 - const tag1 = datastore.addTag('Work', { 125 - color: '#3498db', 126 - description: 'Work-related content' 127 - }); 128 - console.log(' Added tag:', tag1); 129 - 130 - const tag2 = datastore.addTag('Personal', { 131 - color: '#e74c3c', 132 - description: 'Personal content' 133 - }); 134 - console.log(' Added tag:', tag2); 135 - 136 - const tag3 = datastore.addTag('Project Alpha', { 137 - color: '#2ecc71', 138 - parentId: tag1 139 - }); 140 - console.log(' Added child tag:', tag3); 141 - 142 - const workTag = datastore.getTagByName('Work'); 143 - console.log(' Retrieved tag by name:', workTag); 144 - 145 - const allTags = datastore.queryTags(); 146 - console.log(' Total tags:', allTags.length); 147 - 148 - const topLevelTags = datastore.queryTags({ parentId: '' }); 149 - console.log(' Top-level tags:', topLevelTags.length); 150 - 151 - const workChildTags = datastore.queryTags({ parentId: tag1 }); 152 - console.log(' Work child tags:', workChildTags.length); 153 - 154 - // Test Stats 155 - console.log('\n6. Testing Stats...'); 156 - const stats = datastore.getStats(); 157 - console.log(' Stats:', JSON.stringify(stats, null, 2)); 158 - 159 - // Test Store Access 160 - console.log('\n7. Testing Direct Store Access...'); 161 - const store = datastore.getStore(); 162 - const addressesTable = store.getTable('addresses'); 163 - console.log(' Direct table access - addresses:', Object.keys(addressesTable).length); 164 - 165 - // Test Cleanup 166 - console.log('\n8. Testing Cleanup...'); 167 - datastore.deleteContent(content2); 168 - console.log(' Deleted content item'); 169 - 170 - const remainingContent = datastore.queryContent(); 171 - console.log(' Remaining content:', remainingContent.length); 172 - 173 - // Final Stats 174 - console.log('\n9. Final Stats...'); 175 - const finalStats = datastore.getStats(); 176 - console.log(' Final stats:', JSON.stringify(finalStats, null, 2)); 177 - 178 - // Summary 179 - console.log('\n===== Test Summary ====='); 180 - console.log('Addresses created:', allAddresses.length); 181 - console.log('Visits recorded:', recentVisits.length); 182 - console.log('Content items created:', allContent.length); 183 - console.log('Tags created:', allTags.length); 184 - console.log('All tests completed successfully!'); 185 - 186 - // Uninitialize 187 - console.log('\n10. Uninitializing datastore...'); 188 - datastore.uninit(); 189 - console.log(' Done!');
+74 -58
index.js
··· 17 17 import fs from 'node:fs'; 18 18 import path from 'node:path'; 19 19 import { pathToFileURL } from 'url'; 20 + // Import from compiled TypeScript backend 20 21 import { 21 22 initDatabase, 22 23 closeDatabase, ··· 25 26 now, 26 27 parseUrl, 27 28 normalizeUrl, 28 - getTableAsObject, 29 - getRow, 30 - setRow, 31 - deleteRow, 32 29 isValidTable, 33 - tableNames 34 - } from './app/datastore/db.js'; 30 + calculateFrecency, 31 + } from './dist/backend/electron/index.js'; 35 32 import unhandled from 'electron-unhandled'; 36 33 37 34 // Catch unhandled errors and promise rejections without showing alert dialogs ··· 137 134 138 135 // ***** Datastore ***** 139 136 140 - // Database reference (set during init) 141 - let db = null; 137 + // Note: getDb, generateId, now, parseUrl, normalizeUrl, calculateFrecency, isValidTable 138 + // are imported directly from backend/electron 142 139 143 - const initDatastore = (userDataPath) => { 140 + const initDatastore = async (userDataPath) => { 144 141 const dbPath = path.join(userDataPath, 'datastore.sqlite'); 145 - db = initDatabase(dbPath); 146 - return db !== null; 147 - }; 148 - 149 - // Calculate frecency score: frequency * 10 * decay_factor 150 - // decay_factor = 1 / (1 + days_since_use / 7) 151 - const calculateFrecency = (frequency, lastUsedAt) => { 152 - const currentTime = Date.now(); 153 - const daysSinceUse = (currentTime - lastUsedAt) / (1000 * 60 * 60 * 24); 154 - const decayFactor = 1 / (1 + daysSinceUse / 7); 155 - return Math.round(frequency * 10 * decayFactor); 142 + return initDatabase(dbPath); 156 143 }; 157 144 158 145 // ***** Features / Strings ***** ··· 418 405 if (builtinPath) return builtinPath; 419 406 420 407 // Check datastore for external extensions 421 - if (db) { 408 + try { 409 + const db = getDb(); 422 410 const ext = db.prepare('SELECT * FROM extensions WHERE id = ?').get(id); 423 411 if (ext && ext.path) { 424 412 return ext.path; ··· 436 424 // Ignore JSON parse errors 437 425 } 438 426 } 427 + } catch { 428 + // Database not initialized yet 439 429 } 440 430 441 431 return null; ··· 616 606 const builtinExtIds = Array.from(extensionPaths.keys()); 617 607 618 608 // Check which are enabled from datastore/localStorage 609 + const db = getDb(); 619 610 for (const extId of builtinExtIds) { 620 611 // Check if enabled in extension_settings or extensions table 621 612 let enabled = true; // Default to enabled for builtins 622 613 623 - if (db) { 624 - // Check extension_settings for enabled state 625 - const setting = db.prepare('SELECT * FROM extension_settings WHERE extensionId = ? AND key = ?').get(extId, 'enabled'); 626 - if (setting) { 627 - try { 628 - enabled = JSON.parse(setting.value) !== false; 629 - } catch (e) { 630 - enabled = true; 631 - } 614 + // Check extension_settings for enabled state 615 + const setting = db.prepare('SELECT * FROM extension_settings WHERE extensionId = ? AND key = ?').get(extId, 'enabled'); 616 + if (setting) { 617 + try { 618 + enabled = JSON.parse(setting.value) !== false; 619 + } catch (e) { 620 + enabled = true; 632 621 } 633 622 } 634 623 ··· 641 630 } 642 631 643 632 // Load external extensions from datastore 644 - if (db) { 645 - const externalExts = db.prepare('SELECT * FROM extensions').all(); 646 - for (const extData of externalExts) { 647 - const extId = extData.id; 648 - // Skip if already loaded (shouldn't happen but be safe) 649 - if (extensionWindows.has(extId)) continue; 650 - 651 - // Skip if not enabled 652 - if (extData.enabled !== 1) { 653 - console.log(`[ext:win] Skipping disabled external extension: ${extId}`); 654 - continue; 655 - } 633 + const externalExts = db.prepare('SELECT * FROM extensions').all(); 634 + for (const extData of externalExts) { 635 + const extId = extData.id; 636 + // Skip if already loaded (shouldn't happen but be safe) 637 + if (extensionWindows.has(extId)) continue; 656 638 657 - // Need a path to load from 658 - if (!extData.path) { 659 - console.log(`[ext:win] Skipping external extension without path: ${extId}`); 660 - continue; 661 - } 639 + // Skip if not enabled 640 + if (extData.enabled !== 1) { 641 + console.log(`[ext:win] Skipping disabled external extension: ${extId}`); 642 + continue; 643 + } 662 644 663 - console.log(`[ext:win] Loading enabled external extension: ${extId}`); 664 - await createExtensionWindow(extId); 645 + // Need a path to load from 646 + if (!extData.path) { 647 + console.log(`[ext:win] Skipping external extension without path: ${extId}`); 648 + continue; 665 649 } 650 + 651 + console.log(`[ext:win] Loading enabled external extension: ${extId}`); 652 + await createExtensionWindow(extId); 666 653 } 667 654 668 655 console.log(`[ext:win] Loaded ${extensionWindows.size} extensions`); ··· 1546 1533 const parsed = parseUrl(normalizedUri); 1547 1534 const addressId = generateId('addr'); 1548 1535 const timestamp = now(); 1536 + const db = getDb(); 1549 1537 1550 1538 const stmt = db.prepare(` 1551 1539 INSERT INTO addresses (id, uri, protocol, domain, path, title, mimeType, favicon, description, tags, metadata, createdAt, updatedAt, lastVisitAt, visitCount, starred, archived) ··· 1582 1570 ipcMain.handle('datastore-get-address', async (ev, data) => { 1583 1571 try { 1584 1572 const { id } = data; 1573 + const db = getDb(); 1585 1574 const row = db.prepare('SELECT * FROM addresses WHERE id = ?').get(id); 1586 1575 return { success: true, data: row || {} }; 1587 1576 } catch (error) { ··· 1593 1582 ipcMain.handle('datastore-update-address', async (ev, data) => { 1594 1583 try { 1595 1584 const { id, updates } = data; 1585 + const db = getDb(); 1596 1586 const existing = db.prepare('SELECT * FROM addresses WHERE id = ?').get(id); 1597 1587 if (!existing) { 1598 1588 return { success: false, error: 'Address not found' }; ··· 1614 1604 ipcMain.handle('datastore-query-addresses', async (ev, data) => { 1615 1605 try { 1616 1606 const { filter = {} } = data; 1607 + const db = getDb(); 1617 1608 1618 1609 let sql = 'SELECT * FROM addresses WHERE 1=1'; 1619 1610 const params = []; ··· 1662 1653 const { addressId, options = {} } = data; 1663 1654 const visitId = generateId('visit'); 1664 1655 const timestamp = now(); 1656 + const db = getDb(); 1665 1657 1666 1658 db.prepare(` 1667 1659 INSERT INTO visits (id, addressId, timestamp, duration, source, sourceId, windowType, metadata, scrollDepth, interacted) ··· 1695 1687 ipcMain.handle('datastore-query-visits', async (ev, data) => { 1696 1688 try { 1697 1689 const { filter = {} } = data; 1690 + const db = getDb(); 1698 1691 1699 1692 let sql = 'SELECT * FROM visits WHERE 1=1'; 1700 1693 const params = []; ··· 1733 1726 const { options = {} } = data; 1734 1727 const contentId = generateId('content'); 1735 1728 const timestamp = now(); 1729 + const db = getDb(); 1736 1730 1737 1731 db.prepare(` 1738 1732 INSERT INTO content (id, title, content, mimeType, contentType, language, encoding, tags, addressRefs, parentId, metadata, createdAt, updatedAt, syncPath, synced, starred, archived) ··· 1766 1760 ipcMain.handle('datastore-query-content', async (ev, data) => { 1767 1761 try { 1768 1762 const { filter = {} } = data; 1763 + const db = getDb(); 1769 1764 1770 1765 let sql = 'SELECT * FROM content WHERE 1=1'; 1771 1766 const params = []; ··· 1814 1809 ipcMain.handle('datastore-get-table', async (ev, data) => { 1815 1810 try { 1816 1811 const { tableName } = data; 1817 - if (!isValidTable(tableName)) { 1812 + const db = getDb(); 1813 + // Validate table name against known tables 1814 + const validTables = ['addresses', 'visits', 'content', 'tags', 'address_tags', 'blobs', 'scripts_data', 'feeds', 'extensions', 'extension_settings']; 1815 + if (!validTables.includes(tableName)) { 1818 1816 return { success: false, error: `Invalid table name: ${tableName}` }; 1819 1817 } 1820 1818 const rows = db.prepare(`SELECT * FROM ${tableName}`).all(); ··· 1833 1831 ipcMain.handle('datastore-set-row', async (ev, data) => { 1834 1832 try { 1835 1833 const { tableName, rowId, rowData } = data; 1836 - if (!isValidTable(tableName)) { 1834 + const db = getDb(); 1835 + // Validate table name against known tables 1836 + const validTables = ['addresses', 'visits', 'content', 'tags', 'address_tags', 'blobs', 'scripts_data', 'feeds', 'extensions', 'extension_settings']; 1837 + if (!validTables.includes(tableName)) { 1837 1838 return { success: false, error: `Invalid table name: ${tableName}` }; 1838 1839 } 1839 1840 const row = { id: rowId, ...rowData }; ··· 1851 1852 1852 1853 ipcMain.handle('datastore-get-stats', async () => { 1853 1854 try { 1855 + const db = getDb(); 1854 1856 const stats = { 1855 1857 totalAddresses: db.prepare('SELECT COUNT(*) as count FROM addresses').get().count, 1856 1858 totalVisits: db.prepare('SELECT COUNT(*) as count FROM visits').get().count, ··· 1874 1876 console.log('datastore-get-or-create-tag:', name); 1875 1877 const slug = name.toLowerCase().trim().replace(/\s+/g, '-'); 1876 1878 const timestamp = now(); 1879 + const db = getDb(); 1877 1880 1878 1881 // Look for existing tag by name (case-insensitive) 1879 1882 const existingTag = db.prepare('SELECT * FROM tags WHERE LOWER(name) = LOWER(?)').get(name); ··· 1905 1908 const { addressId, tagId } = data; 1906 1909 console.log('datastore-tag-address:', { addressId, tagId }); 1907 1910 const timestamp = now(); 1911 + const db = getDb(); 1908 1912 1909 1913 // Check if link already exists 1910 1914 const existingLink = db.prepare('SELECT * FROM address_tags WHERE addressId = ? AND tagId = ?').get(addressId, tagId); ··· 1937 1941 ipcMain.handle('datastore-untag-address', async (ev, data) => { 1938 1942 try { 1939 1943 const { addressId, tagId } = data; 1944 + const db = getDb(); 1940 1945 1941 1946 const result = db.prepare('DELETE FROM address_tags WHERE addressId = ? AND tagId = ?').run(addressId, tagId); 1942 1947 return { success: true, removed: result.changes > 0 }; ··· 1950 1955 ipcMain.handle('datastore-get-tags-by-frecency', async (ev, data = {}) => { 1951 1956 try { 1952 1957 const { domain } = data || {}; 1958 + const db = getDb(); 1953 1959 let tags = db.prepare('SELECT * FROM tags').all(); 1954 1960 console.log('datastore-get-tags-by-frecency: tags table has', tags.length, 'tags'); 1955 1961 ··· 1991 1997 ipcMain.handle('datastore-get-address-tags', async (ev, data) => { 1992 1998 try { 1993 1999 const { addressId } = data; 2000 + const db = getDb(); 1994 2001 1995 2002 // Use JOIN to get tags directly 1996 2003 const tags = db.prepare(` ··· 2010 2017 ipcMain.handle('datastore-get-addresses-by-tag', async (ev, data) => { 2011 2018 try { 2012 2019 const { tagId } = data; 2020 + const db = getDb(); 2013 2021 2014 2022 // Use JOIN to get addresses directly 2015 2023 const addresses = db.prepare(` ··· 2028 2036 // Get addresses that have no tags 2029 2037 ipcMain.handle('datastore-get-untagged-addresses', async (ev, data) => { 2030 2038 try { 2039 + const db = getDb(); 2031 2040 // Use LEFT JOIN + NULL check to find untagged addresses 2032 2041 const addresses = db.prepare(` 2033 2042 SELECT a.* FROM addresses a ··· 2133 2142 try { 2134 2143 const timestamp = now(); 2135 2144 const id = manifest?.id || `ext-${timestamp}`; 2145 + const db = getDb(); 2136 2146 2137 2147 // Check if extension with this ID already exists 2138 2148 const existing = db.prepare('SELECT * FROM extensions WHERE id = ?').get(id); ··· 2176 2186 const { id } = data; 2177 2187 2178 2188 try { 2189 + const db = getDb(); 2179 2190 const existing = db.prepare('SELECT * FROM extensions WHERE id = ?').get(id); 2180 2191 if (!existing) { 2181 2192 return { success: false, error: `Extension '${id}' not found` }; ··· 2200 2211 const { id, updates } = data; 2201 2212 2202 2213 try { 2214 + const db = getDb(); 2203 2215 const existing = db.prepare('SELECT * FROM extensions WHERE id = ?').get(id); 2204 2216 if (!existing) { 2205 2217 return { success: false, error: `Extension '${id}' not found` }; ··· 2224 2236 // Get all extensions from datastore 2225 2237 ipcMain.handle('extension-get-all', async (ev) => { 2226 2238 try { 2239 + const db = getDb(); 2227 2240 const extensions = db.prepare('SELECT * FROM extensions').all(); 2228 2241 return { success: true, data: extensions }; 2229 2242 } catch (error) { ··· 2237 2250 const { id } = data; 2238 2251 2239 2252 try { 2253 + const db = getDb(); 2240 2254 const row = db.prepare('SELECT * FROM extensions WHERE id = ?').get(id); 2241 2255 if (!row) { 2242 2256 return { success: false, error: `Extension '${id}' not found` }; ··· 2339 2353 const { extId } = data; 2340 2354 2341 2355 try { 2356 + const db = getDb(); 2342 2357 const rows = db.prepare('SELECT * FROM extension_settings WHERE extensionId = ?').all(extId); 2343 2358 const settings = {}; 2344 2359 ··· 2363 2378 2364 2379 try { 2365 2380 const timestamp = now(); 2381 + const db = getDb(); 2366 2382 2367 2383 for (const [key, value] of Object.entries(settings)) { 2368 2384 const rowId = `${extId}:${key}`; ··· 2384 2400 const { extId, key } = data; 2385 2401 2386 2402 try { 2403 + const db = getDb(); 2387 2404 const row = db.prepare('SELECT * FROM extension_settings WHERE extensionId = ? AND key = ?').get(extId, key); 2388 2405 2389 2406 if (!row) { ··· 2406 2423 const { extId, key, value } = data; 2407 2424 2408 2425 try { 2426 + const db = getDb(); 2409 2427 const rowId = `${extId}:${key}`; 2410 2428 db.prepare(` 2411 2429 INSERT OR REPLACE INTO extension_settings (id, extensionId, key, value, updatedAt) ··· 2990 3008 }); 2991 3009 2992 3010 // Close SQLite database 2993 - if (db) { 2994 - try { 2995 - closeDatabase(); 2996 - console.log('SQLite database closed'); 2997 - } catch (error) { 2998 - console.error('Error closing SQLite database:', error); 2999 - } 3011 + try { 3012 + closeDatabase(); 3013 + console.log('SQLite database closed'); 3014 + } catch (error) { 3015 + console.error('Error closing SQLite database:', error); 3000 3016 } 3001 3017 3002 3018 // Give windows a moment to clean up before forcing quit
+6 -1
package.json
··· 26 26 "start": "electron .", 27 27 "debug": "DEBUG=1 electron .", 28 28 "hot-debug": "nodemon --exec 'DEBUG=1 electron .'", 29 + "build:backend": "tsc -p backend/tsconfig.json", 30 + "build:backend:watch": "tsc -p backend/tsconfig.json --watch", 29 31 "package": "electron-builder --dir", 30 32 "package:install": "electron-builder --dir && rm -rf /Applications/Peek.app && cp -R out/mac-arm64/Peek.app /Applications/", 31 33 "make": "electron-builder", ··· 40 42 "devDependencies": { 41 43 "@electron/fuses": "^1.8.0", 42 44 "@electron/rebuild": "^4.0.2", 45 + "@types/better-sqlite3": "^7.6.13", 46 + "@types/node": "^25.0.3", 43 47 "electron": "^35.7.5", 44 - "electron-builder": "26.0.12" 48 + "electron-builder": "26.0.12", 49 + "typescript": "^5.9.3" 45 50 }, 46 51 "resolutions": { 47 52 "tmp": "^0.2.5",
+30 -2
yarn.lock
··· 346 346 languageName: node 347 347 linkType: hard 348 348 349 + "@types/better-sqlite3@npm:^7.6.13": 350 + version: 7.6.13 351 + resolution: "@types/better-sqlite3@npm:7.6.13" 352 + dependencies: 353 + "@types/node": "npm:*" 354 + checksum: 10c0/c4336e7b92343eb0e988ded007c53fa9887b98a38d61175226e86124a1a2c28b1a4e3892873c5041e350b7bfa2901f85c82db1542c4f0eed1d3a899682c92106 355 + languageName: node 356 + linkType: hard 357 + 349 358 "@types/cacheable-request@npm:^6.0.1": 350 359 version: 6.0.3 351 360 resolution: "@types/cacheable-request@npm:6.0.3" ··· 408 417 languageName: node 409 418 linkType: hard 410 419 420 + "@types/node@npm:^25.0.3": 421 + version: 25.0.3 422 + resolution: "@types/node@npm:25.0.3" 423 + dependencies: 424 + undici-types: "npm:~7.16.0" 425 + checksum: 10c0/b7568f0d765d9469621615e2bb257c7fd1953d95e9acbdb58dffb6627a2c4150d405a4600aa1ad8a40182a94fe5f903cafd3c0a2f5132814debd0e3bfd61f835 426 + languageName: node 427 + linkType: hard 428 + 411 429 "@types/plist@npm:^3.0.1": 412 430 version: 3.0.5 413 431 resolution: "@types/plist@npm:3.0.5" ··· 456 474 dependencies: 457 475 "@electron/fuses": "npm:^1.8.0" 458 476 "@electron/rebuild": "npm:^4.0.2" 477 + "@types/better-sqlite3": "npm:^7.6.13" 478 + "@types/node": "npm:^25.0.3" 459 479 better-sqlite3: "npm:^12.5.0" 460 480 electron: "npm:^35.7.5" 461 481 electron-builder: "npm:26.0.12" 462 482 electron-unhandled: "npm:^5.0.0" 463 483 lil-gui: "npm:^0.19.2" 484 + typescript: "npm:^5.9.3" 464 485 languageName: unknown 465 486 linkType: soft 466 487 ··· 3658 3679 languageName: node 3659 3680 linkType: hard 3660 3681 3661 - "typescript@npm:^5.4.3": 3682 + "typescript@npm:^5.4.3, typescript@npm:^5.9.3": 3662 3683 version: 5.9.3 3663 3684 resolution: "typescript@npm:5.9.3" 3664 3685 bin: ··· 3668 3689 languageName: node 3669 3690 linkType: hard 3670 3691 3671 - "typescript@patch:typescript@npm%3A^5.4.3#optional!builtin<compat/typescript>": 3692 + "typescript@patch:typescript@npm%3A^5.4.3#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.9.3#optional!builtin<compat/typescript>": 3672 3693 version: 5.9.3 3673 3694 resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin<compat/typescript>::version=5.9.3&hash=5786d5" 3674 3695 bin: ··· 3682 3703 version: 6.20.0 3683 3704 resolution: "undici-types@npm:6.20.0" 3684 3705 checksum: 10c0/68e659a98898d6a836a9a59e6adf14a5d799707f5ea629433e025ac90d239f75e408e2e5ff086afc3cace26f8b26ee52155293564593fbb4a2f666af57fc59bf 3706 + languageName: node 3707 + linkType: hard 3708 + 3709 + "undici-types@npm:~7.16.0": 3710 + version: 7.16.0 3711 + resolution: "undici-types@npm:7.16.0" 3712 + checksum: 10c0/3033e2f2b5c9f1504bdc5934646cb54e37ecaca0f9249c983f7b1fc2e87c6d18399ebb05dc7fd5419e02b2e915f734d872a65da2e3eeed1813951c427d33cc9a 3685 3713 languageName: node 3686 3714 linkType: hard 3687 3715