fss-link 1.0.51 → 1.0.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +42 -2
  2. package/dist/package.json +1 -1
  3. package/dist/src/config/auth.js +5 -8
  4. package/dist/src/config/auth.js.map +1 -1
  5. package/dist/src/config/config.d.ts +1 -0
  6. package/dist/src/config/config.js +5 -0
  7. package/dist/src/config/config.js.map +1 -1
  8. package/dist/src/config/database-utils.d.ts +185 -0
  9. package/dist/src/config/database-utils.js +295 -0
  10. package/dist/src/config/database-utils.js.map +1 -0
  11. package/dist/src/config/database.d.ts +197 -1
  12. package/dist/src/config/database.js +499 -160
  13. package/dist/src/config/database.js.map +1 -1
  14. package/dist/src/config/databaseHealthMonitor.d.ts +86 -0
  15. package/dist/src/config/databaseHealthMonitor.js +180 -0
  16. package/dist/src/config/databaseHealthMonitor.js.map +1 -0
  17. package/dist/src/config/databaseMetrics.d.ts +147 -0
  18. package/dist/src/config/databaseMetrics.js +369 -0
  19. package/dist/src/config/databaseMetrics.js.map +1 -0
  20. package/dist/src/config/databaseMigrations.js +40 -2
  21. package/dist/src/config/databaseMigrations.js.map +1 -1
  22. package/dist/src/config/databaseSchemaValidator.d.ts +114 -0
  23. package/dist/src/config/databaseSchemaValidator.js +499 -0
  24. package/dist/src/config/databaseSchemaValidator.js.map +1 -0
  25. package/dist/src/config/providerPersistence.d.ts +7 -0
  26. package/dist/src/config/providerPersistence.js +14 -7
  27. package/dist/src/config/providerPersistence.js.map +1 -1
  28. package/dist/src/gemini.js +35 -2
  29. package/dist/src/gemini.js.map +1 -1
  30. package/dist/src/generated/git-commit.d.ts +2 -2
  31. package/dist/src/generated/git-commit.js +2 -2
  32. package/dist/src/ui/utils/updateCheck.js +1 -1
  33. package/dist/src/ui/utils/updateCheck.js.map +1 -1
  34. package/dist/src/utils/installationInfo.js +16 -0
  35. package/dist/src/utils/installationInfo.js.map +1 -1
  36. package/dist/src/utils/installationInfo.test.js +6 -6
  37. package/dist/src/utils/installationInfo.test.js.map +1 -1
  38. package/dist/src/validateNonInterActiveAuth.js +18 -4
  39. package/dist/src/validateNonInterActiveAuth.js.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +1 -1
@@ -12,6 +12,10 @@ import { getDatabasePool } from './databasePool.js';
12
12
  import { MigrationManager } from './databaseMigrations.js';
13
13
  import { DatabaseBackupManager } from './databaseBackup.js';
14
14
  import { QueryOptimizer } from './queryOptimizer.js';
15
+ import { DatabaseMetricsCollector } from './databaseMetrics.js';
16
+ import { DatabaseHealthMonitor } from './databaseHealthMonitor.js';
17
+ import { DatabaseSchemaValidator } from './databaseSchemaValidator.js';
18
+ import { safeQueryFirst, safeQuery, safeExec, safeTransaction, getLastInsertId } from './database-utils.js';
15
19
  /**
16
20
  * FSS Link Database Manager
17
21
  * Handles all persistent state for model configurations and user preferences
@@ -27,6 +31,9 @@ export class FSSLinkDatabase {
27
31
  migrationManager;
28
32
  backupManager;
29
33
  queryOptimizer;
34
+ metricsCollector;
35
+ healthMonitor;
36
+ schemaValidator;
30
37
  constructor() {
31
38
  // Ensure FSS Link settings directory exists
32
39
  if (!fs.existsSync(USER_SETTINGS_DIR)) {
@@ -38,6 +45,9 @@ export class FSSLinkDatabase {
38
45
  this.migrationManager = new MigrationManager();
39
46
  this.backupManager = new DatabaseBackupManager();
40
47
  this.queryOptimizer = new QueryOptimizer();
48
+ this.metricsCollector = new DatabaseMetricsCollector();
49
+ this.healthMonitor = new DatabaseHealthMonitor();
50
+ this.schemaValidator = new DatabaseSchemaValidator();
41
51
  }
42
52
  /**
43
53
  * Derive encryption key from machine-specific data
@@ -55,7 +65,7 @@ export class FSSLinkDatabase {
55
65
  if (!apiKey)
56
66
  return '';
57
67
  const iv = crypto.randomBytes(16);
58
- const cipher = crypto.createCipher('aes-256-cbc', this.encryptionKey);
68
+ const cipher = crypto.createCipheriv('aes-256-cbc', this.encryptionKey, iv);
59
69
  let encrypted = cipher.update(apiKey, 'utf8', 'hex');
60
70
  encrypted += cipher.final('hex');
61
71
  return `enc:${iv.toString('hex')}:${encrypted}`;
@@ -71,8 +81,9 @@ export class FSSLinkDatabase {
71
81
  const parts = encryptedData.substring(4).split(':'); // Remove 'enc:' prefix
72
82
  if (parts.length !== 2)
73
83
  return encryptedData;
74
- const [_ivHex, encrypted] = parts;
75
- const decipher = crypto.createDecipher('aes-256-cbc', this.encryptionKey);
84
+ const [ivHex, encrypted] = parts;
85
+ const iv = Buffer.from(ivHex, 'hex');
86
+ const decipher = crypto.createDecipheriv('aes-256-cbc', this.encryptionKey, iv);
76
87
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
77
88
  decrypted += decipher.final('utf8');
78
89
  return decrypted;
@@ -88,35 +99,67 @@ export class FSSLinkDatabase {
88
99
  async initialize() {
89
100
  if (this.initialized)
90
101
  return;
91
- // Initialize the connection pool
92
- await this.pool.initialize();
93
- // Use a connection from the pool to run migrations and initialize schema
94
- await this.pool.withConnection(async (db) => {
95
- this.db = db; // Keep reference for compatibility
96
- // Create auto backup before migrations if database exists
97
- const needsMigration = this.migrationManager.needsMigration(db);
98
- if (needsMigration && fs.existsSync(this.dbPath)) {
99
- try {
100
- console.log('Creating backup before migration...');
101
- await this.backupManager.createAutoBackup(db);
102
+ debugLog('Starting FSS Link database initialization...');
103
+ try {
104
+ // Initialize the connection pool
105
+ debugLog('Initializing database connection pool...');
106
+ await this.pool.initialize();
107
+ debugLog('Database connection pool initialized successfully');
108
+ // Use a connection from the pool to run migrations and initialize schema
109
+ await this.pool.withConnection(async (db) => {
110
+ this.db = db; // Keep reference for compatibility
111
+ debugLog('Database connection acquired, checking migration status...');
112
+ // Create auto backup before migrations if database exists
113
+ const needsMigration = this.migrationManager.needsMigration(db);
114
+ debugLog(`Migration needed: ${needsMigration}`);
115
+ if (needsMigration && fs.existsSync(this.dbPath)) {
116
+ try {
117
+ console.log('Creating backup before migration...');
118
+ await this.backupManager.createAutoBackup(db);
119
+ console.log('Pre-migration backup created successfully');
120
+ }
121
+ catch (error) {
122
+ console.warn('Failed to create pre-migration backup:', error);
123
+ }
102
124
  }
103
- catch (error) {
104
- console.warn('Failed to create pre-migration backup:', error);
125
+ // Run database migrations if needed
126
+ if (needsMigration) {
127
+ debugLog('Running database migrations...');
128
+ await this.migrationManager.migrate(db);
129
+ debugLog('Database migrations completed successfully');
105
130
  }
106
- }
107
- // Run database migrations if needed
108
- if (needsMigration) {
109
- console.log('Running database migrations...');
110
- await this.migrationManager.migrate(db);
111
- }
112
- // Validate schema integrity
113
- if (!this.migrationManager.validateSchema(db)) {
114
- throw new Error('Database schema validation failed');
115
- }
116
- // Run legacy schema initialization for any missed items
117
- await this.initializeSchema();
118
- });
119
- this.initialized = true;
131
+ else {
132
+ debugLog('Database is up to date, no migrations needed');
133
+ }
134
+ // Validate schema integrity
135
+ debugLog('Validating database schema...');
136
+ if (!this.migrationManager.validateSchema(db)) {
137
+ throw new Error('Database schema validation failed');
138
+ }
139
+ debugLog('Database schema validation passed');
140
+ // Run legacy schema initialization for any missed items
141
+ debugLog('Running legacy schema initialization...');
142
+ await this.initializeSchema();
143
+ debugLog('Legacy schema initialization completed');
144
+ // Final verification that critical tables exist
145
+ debugLog('Performing final table existence check...');
146
+ const criticalTables = ['model_configs', 'provider_usage_stats', 'user_preferences'];
147
+ for (const tableName of criticalTables) {
148
+ const tableCheck = db.exec(`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`);
149
+ if (tableCheck.length === 0) {
150
+ throw new Error(`Critical table '${tableName}' missing after initialization`);
151
+ }
152
+ debugLog(`✓ Table '${tableName}' exists`);
153
+ }
154
+ });
155
+ this.initialized = true;
156
+ debugLog('FSS Link database initialization completed successfully');
157
+ }
158
+ catch (error) {
159
+ console.error('Database initialization failed:', error);
160
+ this.initialized = false;
161
+ throw error;
162
+ }
120
163
  }
121
164
  /**
122
165
  * Ensure database is initialized
@@ -258,6 +301,18 @@ export class FSSLinkDatabase {
258
301
  UNIQUE(provider_id, setting_key)
259
302
  )
260
303
  `);
304
+ // Provider usage stats table (safety fallback if migrations failed)
305
+ debugLog('Creating provider_usage_stats table (legacy schema fallback)...');
306
+ this.db.exec(`
307
+ CREATE TABLE IF NOT EXISTS provider_usage_stats (
308
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
309
+ provider_id INTEGER NOT NULL,
310
+ tokens_used INTEGER DEFAULT 0,
311
+ session_duration_seconds INTEGER DEFAULT 0,
312
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
313
+ )
314
+ `);
315
+ debugLog('provider_usage_stats table exists (legacy schema)');
261
316
  // Create indexes for performance
262
317
  this.db.exec(`
263
318
  CREATE INDEX IF NOT EXISTS idx_model_configs_active ON model_configs(is_active);
@@ -265,7 +320,7 @@ export class FSSLinkDatabase {
265
320
  CREATE INDEX IF NOT EXISTS idx_model_configs_last_used ON model_configs(last_used DESC);
266
321
  CREATE INDEX IF NOT EXISTS idx_model_configs_auth_type ON model_configs(auth_type);
267
322
  CREATE INDEX IF NOT EXISTS idx_custom_endpoints_provider ON custom_endpoints(provider_id);
268
- CREATE INDEX IF NOT EXISTS idx_provider_usage_date ON provider_usage(session_date DESC);
323
+ CREATE INDEX IF NOT EXISTS idx_provider_usage_stats_aggregation ON provider_usage_stats(provider_id, created_at DESC);
269
324
  CREATE INDEX IF NOT EXISTS idx_provider_settings_key ON provider_settings(provider_id, setting_key);
270
325
  `);
271
326
  // Insert default configurations if none exist
@@ -279,10 +334,9 @@ export class FSSLinkDatabase {
279
334
  async ensureDefaultConfigurations() {
280
335
  if (!this.db)
281
336
  return;
282
- // Check if any configurations exist
283
- const countStmt = this.db.prepare('SELECT COUNT(*) as count FROM model_configs');
284
- const countResult = countStmt.getAsObject({});
285
- if (countResult.count === 0) {
337
+ // MIGRATED: Check if any configurations exist
338
+ const countResult = safeQueryFirst(this.db, 'SELECT COUNT(*) as count FROM model_configs');
339
+ if ((countResult?.count || 0) === 0) {
286
340
  // Insert default Ollama configuration for new users (no API key needed)
287
341
  const defaultOllamaConfig = {
288
342
  auth_type: 'ollama',
@@ -295,13 +349,13 @@ export class FSSLinkDatabase {
295
349
  last_used: null,
296
350
  created_at: new Date().toISOString()
297
351
  };
298
- const insertStmt = this.db.prepare(`
352
+ // MIGRATED: Insert default Ollama configuration
353
+ safeExec(this.db, `
299
354
  INSERT INTO model_configs (
300
355
  auth_type, model_name, endpoint_url, api_key, display_name,
301
356
  is_favorite, is_active, last_used, created_at
302
357
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
303
- `);
304
- insertStmt.run([
358
+ `, [
305
359
  defaultOllamaConfig.auth_type,
306
360
  defaultOllamaConfig.model_name,
307
361
  defaultOllamaConfig.endpoint_url,
@@ -312,10 +366,8 @@ export class FSSLinkDatabase {
312
366
  defaultOllamaConfig.last_used,
313
367
  defaultOllamaConfig.created_at
314
368
  ]);
315
- // Get the inserted Ollama config ID to add provider settings
316
- const idStmt = this.db.prepare('SELECT last_insert_rowid() as id');
317
- const idResult = idStmt.getAsObject({});
318
- const ollamaId = idResult.id;
369
+ // MIGRATED: Get the inserted Ollama config ID
370
+ const ollamaId = getLastInsertId(this.db);
319
371
  // Also insert a default Gemini configuration (inactive) for when users want to configure API keys later
320
372
  const defaultGeminiConfig = {
321
373
  auth_type: 'gemini-api-key',
@@ -328,7 +380,13 @@ export class FSSLinkDatabase {
328
380
  last_used: null,
329
381
  created_at: new Date().toISOString()
330
382
  };
331
- insertStmt.run([
383
+ // MIGRATED: Insert default Gemini configuration
384
+ safeExec(this.db, `
385
+ INSERT INTO model_configs (
386
+ auth_type, model_name, endpoint_url, api_key, display_name,
387
+ is_favorite, is_active, last_used, created_at
388
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
389
+ `, [
332
390
  defaultGeminiConfig.auth_type,
333
391
  defaultGeminiConfig.model_name,
334
392
  defaultGeminiConfig.endpoint_url,
@@ -351,7 +409,13 @@ export class FSSLinkDatabase {
351
409
  last_used: null,
352
410
  created_at: new Date().toISOString()
353
411
  };
354
- insertStmt.run([
412
+ // MIGRATED: Insert default LM Studio configuration
413
+ safeExec(this.db, `
414
+ INSERT INTO model_configs (
415
+ auth_type, model_name, endpoint_url, api_key, display_name,
416
+ is_favorite, is_active, last_used, created_at
417
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
418
+ `, [
355
419
  defaultLMStudioConfig.auth_type,
356
420
  defaultLMStudioConfig.model_name,
357
421
  defaultLMStudioConfig.endpoint_url,
@@ -374,7 +438,13 @@ export class FSSLinkDatabase {
374
438
  last_used: null,
375
439
  created_at: new Date().toISOString()
376
440
  };
377
- insertStmt.run([
441
+ // MIGRATED: Insert default OpenAI configuration
442
+ safeExec(this.db, `
443
+ INSERT INTO model_configs (
444
+ auth_type, model_name, endpoint_url, api_key, display_name,
445
+ is_favorite, is_active, last_used, created_at
446
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
447
+ `, [
378
448
  defaultOpenAIConfig.auth_type,
379
449
  defaultOpenAIConfig.model_name,
380
450
  defaultOpenAIConfig.endpoint_url,
@@ -385,30 +455,35 @@ export class FSSLinkDatabase {
385
455
  defaultOpenAIConfig.last_used,
386
456
  defaultOpenAIConfig.created_at
387
457
  ]);
388
- // Get the LM Studio ID
389
- const lmStudioStmt = this.db.prepare('SELECT last_insert_rowid() as id');
390
- const lmStudioResult = lmStudioStmt.getAsObject({});
391
- const lmStudioId = lmStudioResult.id;
392
- // Insert default provider settings for optimal context windows
393
- const settingsStmt = this.db.prepare(`
394
- INSERT INTO provider_settings (provider_id, setting_key, setting_value, updated_at)
395
- VALUES (?, ?, ?, ?)
396
- `);
458
+ // MIGRATED: Get the LM Studio ID
459
+ const lmStudioId = getLastInsertId(this.db);
460
+ // MIGRATED: Insert default provider settings for optimal context windows
397
461
  const now = new Date().toISOString();
398
462
  // Ollama default settings - 32K context window
399
- settingsStmt.run([ollamaId, 'num_ctx', '32768', now]);
400
- settingsStmt.run([ollamaId, 'temperature', '0.0', now]);
463
+ safeExec(this.db, `
464
+ INSERT INTO provider_settings (provider_id, setting_key, setting_value, updated_at)
465
+ VALUES (?, ?, ?, ?)
466
+ `, [ollamaId, 'num_ctx', '32768', now]);
467
+ safeExec(this.db, `
468
+ INSERT INTO provider_settings (provider_id, setting_key, setting_value, updated_at)
469
+ VALUES (?, ?, ?, ?)
470
+ `, [ollamaId, 'temperature', '0.0', now]);
401
471
  // LM Studio default settings - 32K context window
402
- settingsStmt.run([lmStudioId, 'num_ctx', '32768', now]);
403
- settingsStmt.run([lmStudioId, 'temperature', '0.0', now]);
472
+ safeExec(this.db, `
473
+ INSERT INTO provider_settings (provider_id, setting_key, setting_value, updated_at)
474
+ VALUES (?, ?, ?, ?)
475
+ `, [lmStudioId, 'num_ctx', '32768', now]);
476
+ safeExec(this.db, `
477
+ INSERT INTO provider_settings (provider_id, setting_key, setting_value, updated_at)
478
+ VALUES (?, ?, ?, ?)
479
+ `, [lmStudioId, 'temperature', '0.0', now]);
404
480
  }
405
- // CRITICAL: ALWAYS ensure there's an active model (even if configs already exist)
406
- const activeStmt = this.db.prepare('SELECT COUNT(*) as count FROM model_configs WHERE is_active = 1');
407
- const activeResult = activeStmt.getAsObject({});
408
- if (activeResult.count === 0) {
409
- // No active model found - activate the best available model
481
+ // MIGRATED: ALWAYS ensure there's an active model (even if configs already exist)
482
+ const activeResult = safeQueryFirst(this.db, 'SELECT COUNT(*) as count FROM model_configs WHERE is_active = 1');
483
+ if ((activeResult?.count || 0) === 0) {
484
+ // MIGRATED: No active model found - activate the best available model
410
485
  // Priority: Ollama > LM Studio > Gemini > OpenAI
411
- const findBestStmt = this.db.prepare(`
486
+ const bestModel = safeQueryFirst(this.db, `
412
487
  SELECT id FROM model_configs
413
488
  WHERE auth_type IN ('ollama', 'lmstudio', 'gemini-api-key', 'openai-api-key')
414
489
  ORDER BY
@@ -422,56 +497,51 @@ export class FSSLinkDatabase {
422
497
  created_at ASC
423
498
  LIMIT 1
424
499
  `);
425
- if (findBestStmt.step()) {
426
- const bestModel = findBestStmt.getAsObject();
427
- this.db.run('UPDATE model_configs SET is_active = 1 WHERE id = ?', [bestModel.id]);
428
- // Add default provider settings if they don't exist
429
- const settingsCheckStmt = this.db.prepare('SELECT COUNT(*) as count FROM provider_settings WHERE provider_id = ?');
430
- settingsCheckStmt.bind([bestModel.id]);
431
- if (settingsCheckStmt.step()) {
432
- const settingsCheck = settingsCheckStmt.getAsObject();
433
- if (settingsCheck.count === 0) {
434
- const now = new Date().toISOString();
435
- const addSettingsStmt = this.db.prepare('INSERT INTO provider_settings (provider_id, setting_key, setting_value, updated_at) VALUES (?, ?, ?, ?)');
436
- addSettingsStmt.run([bestModel.id, 'num_ctx', '32768', now]);
437
- addSettingsStmt.run([bestModel.id, 'temperature', '0.0', now]);
438
- }
500
+ if (bestModel) {
501
+ safeExec(this.db, 'UPDATE model_configs SET is_active = 1 WHERE id = ?', [bestModel.id]);
502
+ // MIGRATED: Add default provider settings if they don't exist
503
+ const settingsCheck = safeQueryFirst(this.db, 'SELECT COUNT(*) as count FROM provider_settings WHERE provider_id = ?', [bestModel.id]);
504
+ if ((settingsCheck?.count || 0) === 0) {
505
+ const now = new Date().toISOString();
506
+ safeExec(this.db, 'INSERT INTO provider_settings (provider_id, setting_key, setting_value, updated_at) VALUES (?, ?, ?, ?)', [bestModel.id, 'num_ctx', '32768', now]);
507
+ safeExec(this.db, 'INSERT INTO provider_settings (provider_id, setting_key, setting_value, updated_at) VALUES (?, ?, ?, ?)', [bestModel.id, 'temperature', '0.0', now]);
439
508
  }
440
509
  }
441
510
  }
442
511
  }
443
512
  /**
444
513
  * Get provider settings for a specific provider
514
+ * @todo UPGRADE: Move to ProviderSettingsRepository.findByProviderId()
445
515
  */
446
516
  async getProviderSettings(providerId) {
447
517
  await this.ensureInitialized();
448
518
  if (!this.db)
449
519
  return {};
450
- const stmt = this.db.prepare(`
520
+ // MIGRATED: Using safeQuery wrapper to eliminate manual .free() calls
521
+ const rows = safeQuery(this.db, `
451
522
  SELECT setting_key, setting_value
452
523
  FROM provider_settings
453
524
  WHERE provider_id = ?
454
- `);
455
- stmt.bind([providerId]);
525
+ `, [providerId]);
456
526
  const settings = {};
457
- while (stmt.step()) {
458
- const row = stmt.getAsObject();
527
+ for (const row of rows) {
459
528
  settings[row.setting_key] = row.setting_value;
460
529
  }
461
530
  return settings;
462
531
  }
463
532
  /**
464
533
  * Save a provider setting
534
+ * @todo UPGRADE: Move to ProviderSettingsRepository.upsert()
465
535
  */
466
536
  async saveProviderSetting(providerId, key, value) {
467
537
  await this.ensureInitialized();
468
538
  if (!this.db)
469
539
  return;
470
- const stmt = this.db.prepare(`
540
+ // MIGRATED: Using safeExec wrapper to eliminate manual .free() calls
541
+ safeExec(this.db, `
471
542
  INSERT OR REPLACE INTO provider_settings (provider_id, setting_key, setting_value, updated_at)
472
543
  VALUES (?, ?, ?, ?)
473
- `);
474
- stmt.run([providerId, key, String(value), new Date().toISOString()]);
544
+ `, [providerId, key, String(value), new Date().toISOString()]);
475
545
  }
476
546
  /**
477
547
  * Get the currently active model configuration
@@ -509,6 +579,7 @@ export class FSSLinkDatabase {
509
579
  }
510
580
  /**
511
581
  * Add or update a model configuration
582
+ * @todo UPGRADE: Move to ModelRepository.upsert()
512
583
  */
513
584
  async upsertModelConfig(config) {
514
585
  await this.ensureInitialized();
@@ -516,12 +587,12 @@ export class FSSLinkDatabase {
516
587
  return -1;
517
588
  // Encrypt API key if provided
518
589
  const encryptedApiKey = config.apiKey ? this.encryptApiKey(config.apiKey) : null;
519
- const stmt = this.db.prepare(`
590
+ // MIGRATED: Using safeExec wrapper to eliminate manual .free() calls
591
+ safeExec(this.db, `
520
592
  INSERT OR REPLACE INTO model_configs
521
593
  (auth_type, model_name, endpoint_url, api_key, display_name, is_favorite, is_active, last_used, source)
522
594
  VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)
523
- `);
524
- stmt.run([
595
+ `, [
525
596
  config.authType,
526
597
  config.modelName,
527
598
  config.endpointUrl || null,
@@ -532,9 +603,8 @@ export class FSSLinkDatabase {
532
603
  config.source || 'db'
533
604
  ]);
534
605
  this.save();
535
- // Get the last inserted row ID
536
- const result = this.db.exec('SELECT last_insert_rowid() as id');
537
- return result[0]?.values[0]?.[0] || -1;
606
+ // MIGRATED: Get the last inserted row ID using helper
607
+ return getLastInsertId(this.db);
538
608
  }
539
609
  /**
540
610
  * Get all model configurations
@@ -557,29 +627,28 @@ export class FSSLinkDatabase {
557
627
  */
558
628
  async getFavoriteModels() {
559
629
  await this.ensureInitialized();
560
- if (!this.db)
561
- return [];
562
- const stmt = this.db.prepare(`
563
- SELECT id, auth_type, model_name, endpoint_url, api_key, display_name,
564
- is_favorite, is_active, last_used, created_at, source
565
- FROM model_configs
566
- WHERE is_favorite = 1
567
- ORDER BY last_used DESC, created_at DESC
568
- `);
569
- const results = [];
570
- while (stmt.step()) {
571
- results.push(this.mapRowToModelConfig(stmt.getAsObject()));
572
- }
573
- return results;
630
+ return await this.pool.withConnection(async (db) => {
631
+ const query = `
632
+ SELECT id, auth_type, model_name, endpoint_url, api_key, display_name,
633
+ is_favorite, is_active, last_used, created_at, source
634
+ FROM model_configs
635
+ WHERE is_favorite = 1
636
+ ORDER BY last_used DESC, created_at DESC
637
+ `;
638
+ const results = await this.queryOptimizer.executeQuery(db, query);
639
+ return results.map(row => this.mapRowToModelConfig(row));
640
+ });
574
641
  }
575
642
  /**
576
643
  * Get recently used models
644
+ * @todo UPGRADE: Move to ModelRepository.findRecentlyUsed()
577
645
  */
578
646
  async getRecentModels(limit = 10) {
579
647
  await this.ensureInitialized();
580
648
  if (!this.db)
581
649
  return [];
582
- const stmt = this.db.prepare(`
650
+ // MIGRATED: Using safeQuery wrapper to eliminate manual .free() calls
651
+ const rows = safeQuery(this.db, `
583
652
  SELECT id, auth_type, model_name, endpoint_url, api_key, display_name,
584
653
  is_favorite, is_active, last_used, created_at, source
585
654
  FROM model_configs
@@ -587,11 +656,7 @@ export class FSSLinkDatabase {
587
656
  ORDER BY last_used DESC
588
657
  LIMIT ?
589
658
  `, [limit]);
590
- const results = [];
591
- while (stmt.step()) {
592
- results.push(this.mapRowToModelConfig(stmt.getAsObject()));
593
- }
594
- return results;
659
+ return rows.map(row => this.mapRowToModelConfig(row));
595
660
  }
596
661
  /**
597
662
  * Toggle favorite status for a model
@@ -631,30 +696,29 @@ export class FSSLinkDatabase {
631
696
  this.save();
632
697
  }
633
698
  /**
634
- * Get user preference
699
+ * Get user preference by key
700
+ * @todo UPGRADE: Move to PreferencesRepository.findByKey()
635
701
  */
636
702
  async getUserPreference(key) {
637
703
  await this.ensureInitialized();
638
704
  if (!this.db)
639
705
  return null;
640
- const stmt = this.db.prepare('SELECT value FROM user_preferences WHERE key = ?', [key]);
641
- if (stmt.step()) {
642
- const result = stmt.getAsObject();
643
- return result.value || null;
644
- }
645
- return null;
706
+ // MIGRATED: Using safeQueryFirst wrapper to eliminate manual .free() calls
707
+ const result = safeQueryFirst(this.db, 'SELECT value FROM user_preferences WHERE key = ?', [key]);
708
+ return result?.value || null;
646
709
  }
647
710
  /**
648
711
  * Get all user preferences
712
+ * @todo UPGRADE: Move to PreferencesRepository.findAll()
649
713
  */
650
714
  async getAllUserPreferences() {
651
715
  await this.ensureInitialized();
652
716
  if (!this.db)
653
717
  return {};
654
- const stmt = this.db.prepare('SELECT key, value FROM user_preferences');
718
+ // MIGRATED: Using safeQuery wrapper to eliminate manual .free() calls
719
+ const rows = safeQuery(this.db, 'SELECT key, value FROM user_preferences');
655
720
  const prefs = {};
656
- while (stmt.step()) {
657
- const row = stmt.getAsObject();
721
+ for (const row of rows) {
658
722
  prefs[row.key] = row.value;
659
723
  }
660
724
  return prefs;
@@ -811,26 +875,72 @@ export class FSSLinkDatabase {
811
875
  * Record provider usage for intelligent default selection
812
876
  */
813
877
  async recordProviderUsage(providerId, tokensUsed, sessionDuration) {
814
- await this.ensureInitialized();
815
- if (!this.db)
816
- return;
817
- // Update or insert daily usage statistics
818
- this.db.exec(`
819
- INSERT INTO provider_usage (provider_id, tokens_used, session_duration)
820
- VALUES (?, ?, ?)
821
- ON CONFLICT(provider_id, session_date)
822
- DO UPDATE SET
823
- session_count = session_count + 1,
824
- tokens_used = tokens_used + excluded.tokens_used,
825
- session_duration = session_duration + excluded.session_duration
826
- `, [providerId, tokensUsed, sessionDuration]);
827
- // Update last_used timestamp in model_configs
828
- this.db.exec(`
829
- UPDATE model_configs
830
- SET last_used = CURRENT_TIMESTAMP
831
- WHERE id = ?
832
- `, [providerId]);
833
- this.save();
878
+ console.log(`recordProviderUsage called: providerId=${providerId}, tokensUsed=${tokensUsed}, sessionDuration=${sessionDuration}`);
879
+ try {
880
+ // Ensure database is fully initialized including migrations
881
+ debugLog('Ensuring database is initialized for usage recording...');
882
+ await this.ensureInitialized();
883
+ // Triple-check initialization completed with detailed logging
884
+ if (!this.initialized) {
885
+ console.warn('Database not initialized after ensureInitialized(), skipping usage recording');
886
+ return;
887
+ }
888
+ debugLog('Database initialized, attempting to record usage...');
889
+ await this.pool.withConnection(async (db) => {
890
+ // Check if table exists before trying to use it
891
+ debugLog('Checking if provider_usage_stats table exists...');
892
+ const tableCheck = db.exec(`
893
+ SELECT name FROM sqlite_master
894
+ WHERE type='table' AND name='provider_usage_stats'
895
+ `);
896
+ if (tableCheck.length === 0) {
897
+ console.error('provider_usage_stats table does not exist, this should not happen after initialization!');
898
+ // Emergency table creation as absolute last resort
899
+ console.log('Attempting emergency table creation...');
900
+ try {
901
+ db.exec(`
902
+ CREATE TABLE IF NOT EXISTS provider_usage_stats (
903
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
904
+ provider_id INTEGER NOT NULL,
905
+ tokens_used INTEGER DEFAULT 0,
906
+ session_duration_seconds INTEGER DEFAULT 0,
907
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
908
+ )
909
+ `);
910
+ console.log('Emergency table creation completed');
911
+ }
912
+ catch (emergencyError) {
913
+ console.error('Emergency table creation failed:', emergencyError);
914
+ return;
915
+ }
916
+ }
917
+ else {
918
+ debugLog('provider_usage_stats table exists, proceeding with insert');
919
+ }
920
+ // MIGRATED: Insert usage statistics and update timestamp in transaction
921
+ console.log('Inserting usage statistics and updating timestamp...');
922
+ safeTransaction(db, () => {
923
+ // Insert usage statistics
924
+ safeExec(db, `
925
+ INSERT INTO provider_usage_stats (provider_id, tokens_used, session_duration_seconds, created_at)
926
+ VALUES (?, ?, ?, CURRENT_TIMESTAMP)
927
+ `, [providerId, tokensUsed, sessionDuration]);
928
+ // Update last_used timestamp in model_configs
929
+ safeExec(db, `
930
+ UPDATE model_configs
931
+ SET last_used = CURRENT_TIMESTAMP
932
+ WHERE id = ?
933
+ `, [providerId]);
934
+ });
935
+ console.log('Usage statistics and timestamp updated successfully');
936
+ });
937
+ this.scheduleBatchedSave();
938
+ console.log('Provider usage recording completed successfully');
939
+ }
940
+ catch (error) {
941
+ console.error('Failed to record provider usage (detailed error):', error);
942
+ console.error('Error stack:', error instanceof Error ? error.stack : 'No stack trace');
943
+ }
834
944
  }
835
945
  /**
836
946
  * Save custom endpoint for a provider
@@ -857,24 +967,19 @@ export class FSSLinkDatabase {
857
967
  }
858
968
  /**
859
969
  * Get custom endpoints for a provider
970
+ * @todo UPGRADE: Move to CustomEndpointsRepository.findByProviderId()
860
971
  */
861
972
  async getCustomEndpoints(providerId) {
862
973
  await this.ensureInitialized();
863
974
  if (!this.db)
864
975
  return [];
865
- const stmt = this.db.prepare(`
976
+ // MIGRATED: Using safeQuery wrapper to eliminate manual .free() calls
977
+ return safeQuery(this.db, `
866
978
  SELECT id, provider_id, name, url, description, is_default, created_at
867
979
  FROM custom_endpoints
868
980
  WHERE provider_id = ?
869
981
  ORDER BY is_default DESC, created_at DESC
870
- `);
871
- stmt.bind([providerId]);
872
- const endpoints = [];
873
- while (stmt.step()) {
874
- const row = stmt.getAsObject();
875
- endpoints.push(row);
876
- }
877
- return endpoints;
982
+ `, [providerId]);
878
983
  }
879
984
  /**
880
985
  * Get smart default provider with settings
@@ -888,16 +993,17 @@ export class FSSLinkDatabase {
888
993
  }
889
994
  /**
890
995
  * Get smart default provider based on usage and preferences
996
+ * @todo UPGRADE: Move to ModelRepository.findSmartDefault()
891
997
  */
892
998
  async getSmartDefaultProvider() {
893
999
  await this.ensureInitialized();
894
1000
  if (!this.db)
895
1001
  return null;
896
- // Priority order:
1002
+ // MIGRATED: Priority order using safeQueryFirst wrapper
897
1003
  // 1. User-marked favorite (is_favorite = 1)
898
1004
  // 2. Most used provider in last 30 days
899
1005
  // 3. Most recently used provider
900
- const stmt = this.db.prepare(`
1006
+ const result = safeQueryFirst(this.db, `
901
1007
  SELECT
902
1008
  m.id, m.auth_type, m.model_name, m.endpoint_url, m.api_key,
903
1009
  m.display_name, m.is_favorite, m.is_active, m.last_used, m.created_at,
@@ -918,9 +1024,8 @@ export class FSSLinkDatabase {
918
1024
  m.created_at DESC
919
1025
  LIMIT 1
920
1026
  `);
921
- if (!stmt.step())
1027
+ if (!result)
922
1028
  return null;
923
- const result = stmt.getAsObject();
924
1029
  return {
925
1030
  id: result.id,
926
1031
  authType: result.auth_type,
@@ -934,18 +1039,18 @@ export class FSSLinkDatabase {
934
1039
  }
935
1040
  /**
936
1041
  * Get a specific model configuration by ID
1042
+ * @todo UPGRADE: Move to ModelRepository.findById()
937
1043
  */
938
1044
  async getModelById(modelId) {
939
1045
  await this.ensureInitialized();
940
1046
  if (!this.db)
941
1047
  return null;
942
- const stmt = this.db.prepare(`
1048
+ // MIGRATED: Using safeQueryFirst wrapper to eliminate manual .free() calls
1049
+ const result = safeQueryFirst(this.db, `
943
1050
  SELECT * FROM model_configs WHERE id = ?
944
- `);
945
- stmt.bind([modelId]);
946
- if (!stmt.step())
1051
+ `, [modelId]);
1052
+ if (!result)
947
1053
  return null;
948
- const result = stmt.getAsObject();
949
1054
  return {
950
1055
  id: result.id,
951
1056
  authType: result.auth_type,
@@ -957,7 +1062,238 @@ export class FSSLinkDatabase {
957
1062
  isActive: Boolean(result.is_active)
958
1063
  };
959
1064
  }
1065
+ /**
1066
+ * Get query performance metrics from the optimizer
1067
+ */
1068
+ async getQueryMetrics() {
1069
+ return this.queryOptimizer.getQueryMetrics();
1070
+ }
1071
+ /**
1072
+ * Get slow queries (above threshold)
1073
+ */
1074
+ async getSlowQueries(thresholdMs = 100) {
1075
+ return this.queryOptimizer.getSlowQueries(thresholdMs);
1076
+ }
1077
+ /**
1078
+ * Get most frequently executed queries
1079
+ */
1080
+ async getFrequentQueries(limit = 10) {
1081
+ return this.queryOptimizer.getFrequentQueries(limit);
1082
+ }
1083
+ /**
1084
+ * Get prepared statement cache statistics
1085
+ */
1086
+ async getCacheStats() {
1087
+ return this.queryOptimizer.getCacheStats();
1088
+ }
1089
+ /**
1090
+ * Clear query performance metrics
1091
+ */
1092
+ async clearQueryMetrics() {
1093
+ this.queryOptimizer.clearMetrics();
1094
+ }
1095
+ /**
1096
+ * Optimize database by running ANALYZE
1097
+ */
1098
+ async optimizeDatabase() {
1099
+ await this.pool.withConnection(async (db) => {
1100
+ await this.queryOptimizer.optimizeDatabase(db);
1101
+ });
1102
+ }
1103
+ /**
1104
+ * Get comprehensive database statistics
1105
+ */
1106
+ async getDatabaseStats() {
1107
+ return await this.pool.withConnection(async (db) => {
1108
+ return await this.queryOptimizer.getDatabaseStats(db);
1109
+ });
1110
+ }
1111
+ /**
1112
+ * Run VACUUM to reclaim space and defragment
1113
+ */
1114
+ async vacuumDatabase() {
1115
+ await this.pool.withConnection(async (db) => {
1116
+ await this.queryOptimizer.vacuumDatabase(db);
1117
+ });
1118
+ }
1119
+ /**
1120
+ * Collect comprehensive database metrics
1121
+ */
1122
+ async collectMetrics() {
1123
+ return await this.pool.withConnection(async (db) => {
1124
+ return await this.metricsCollector.collectMetrics(db, this.queryOptimizer, this.pool, this.backupManager);
1125
+ });
1126
+ }
1127
+ /**
1128
+ * Get historical metrics for trend analysis
1129
+ */
1130
+ async getHistoricalMetrics(hours = 24) {
1131
+ return this.metricsCollector.getHistoricalMetrics(hours);
1132
+ }
1133
+ /**
1134
+ * Get performance trends
1135
+ */
1136
+ async getPerformanceTrends(hours = 24) {
1137
+ return this.metricsCollector.getPerformanceTrends(hours);
1138
+ }
1139
+ /**
1140
+ * Generate a comprehensive database health report
1141
+ */
1142
+ async generateHealthReport() {
1143
+ const metrics = await this.collectMetrics();
1144
+ const trends = await this.getPerformanceTrends();
1145
+ const recommendations = [];
1146
+ // Generate recommendations based on metrics
1147
+ if (metrics.queryPerformance.averageQueryTime > 100) {
1148
+ recommendations.push('Consider running ANALYZE to optimize query plans');
1149
+ }
1150
+ if (metrics.cachePerformance.hitRate < 80) {
1151
+ recommendations.push('Increase prepared statement cache size for better performance');
1152
+ }
1153
+ if (metrics.connectionPool.connectionUtilization > 90) {
1154
+ recommendations.push('Consider increasing database connection pool size');
1155
+ }
1156
+ if (metrics.database.freelistCount > 1000) {
1157
+ recommendations.push('Run VACUUM to reclaim unused database space');
1158
+ }
1159
+ if (metrics.usage.lastBackupAge > 24) {
1160
+ recommendations.push('Create a fresh backup - last backup is over 24 hours old');
1161
+ }
1162
+ if (metrics.health.overallScore < 70) {
1163
+ recommendations.push('Database health is below optimal - consider maintenance');
1164
+ }
1165
+ return {
1166
+ metrics,
1167
+ trends,
1168
+ recommendations
1169
+ };
1170
+ }
1171
+ /**
1172
+ * Set custom metrics thresholds
1173
+ */
1174
+ async setMetricsThresholds(thresholds) {
1175
+ this.metricsCollector.setThresholds(thresholds);
1176
+ }
1177
+ /**
1178
+ * Clear metrics history (for testing or cleanup)
1179
+ */
1180
+ async clearMetricsHistory() {
1181
+ this.metricsCollector.clearHistory();
1182
+ }
1183
+ /**
1184
+ * Perform comprehensive health check
1185
+ */
1186
+ async performHealthCheck() {
1187
+ const metrics = await this.collectMetrics();
1188
+ return await this.healthMonitor.checkHealth(metrics);
1189
+ }
1190
+ /**
1191
+ * Get maintenance recommendations
1192
+ */
1193
+ async getMaintenanceRecommendations() {
1194
+ const metrics = await this.collectMetrics();
1195
+ return this.healthMonitor.getMaintenanceRecommendations(metrics);
1196
+ }
1197
+ /**
1198
+ * Get recent health check history
1199
+ */
1200
+ async getHealthHistory(hours = 24) {
1201
+ return this.healthMonitor.getRecentHealthChecks(hours);
1202
+ }
1203
+ /**
1204
+ * Update health monitoring thresholds
1205
+ */
1206
+ async updateHealthThresholds(thresholds) {
1207
+ this.healthMonitor.updateThresholds(thresholds);
1208
+ }
1209
+ /**
1210
+ * Clear health monitoring history
1211
+ */
1212
+ async clearHealthHistory() {
1213
+ this.healthMonitor.clearHistory();
1214
+ }
1215
+ /**
1216
+ * Validate database schema against expected structure
1217
+ */
1218
+ async validateSchema() {
1219
+ return await this.pool.withConnection(async (db) => {
1220
+ return await this.schemaValidator.validateSchema(db);
1221
+ });
1222
+ }
1223
+ /**
1224
+ * Validate specific table schema
1225
+ */
1226
+ async validateTableSchema(tableName) {
1227
+ return await this.pool.withConnection(async (db) => {
1228
+ return await this.schemaValidator.validateTableSchema(db, tableName);
1229
+ });
1230
+ }
1231
+ /**
1232
+ * Check if database needs migration based on schema validation
1233
+ */
1234
+ async needsSchemaMigration() {
1235
+ return await this.pool.withConnection(async (db) => {
1236
+ return await this.schemaValidator.needsMigration(db);
1237
+ });
1238
+ }
1239
+ /**
1240
+ * Get missing tables from schema
1241
+ */
1242
+ async getMissingTables() {
1243
+ return await this.pool.withConnection(async (db) => {
1244
+ return await this.schemaValidator.getMissingTables(db);
1245
+ });
1246
+ }
1247
+ /**
1248
+ * Get missing indexes from schema
1249
+ */
1250
+ async getMissingIndexes() {
1251
+ return await this.pool.withConnection(async (db) => {
1252
+ return await this.schemaValidator.getMissingIndexes(db);
1253
+ });
1254
+ }
1255
+ /**
1256
+ * Get formatted schema validation report
1257
+ */
1258
+ async getSchemaValidationReport() {
1259
+ const result = await this.validateSchema();
1260
+ return this.schemaValidator.formatValidationReport(result);
1261
+ }
1262
+ /**
1263
+ * Perform comprehensive database integrity check
1264
+ */
1265
+ async performIntegrityCheck() {
1266
+ // Run all checks in parallel
1267
+ const [schemaValidation, healthCheck, recommendations] = await Promise.all([
1268
+ this.validateSchema(),
1269
+ this.performHealthCheck(),
1270
+ this.getMaintenanceRecommendations()
1271
+ ]);
1272
+ // Determine overall status
1273
+ let overallStatus = 'healthy';
1274
+ if (!schemaValidation.isValid ||
1275
+ healthCheck.some(h => h.status === 'critical' || h.status === 'error')) {
1276
+ overallStatus = 'critical';
1277
+ }
1278
+ else if (schemaValidation.warnings.length > 0 ||
1279
+ healthCheck.some(h => h.status === 'warning') ||
1280
+ recommendations.length > 0) {
1281
+ overallStatus = 'warning';
1282
+ }
1283
+ return {
1284
+ schemaValidation,
1285
+ healthCheck,
1286
+ recommendations,
1287
+ overallStatus
1288
+ };
1289
+ }
960
1290
  }
1291
+ // Debug logging helper - only log if DEBUG environment variable is set
1292
+ const debugLog = (message) => {
1293
+ if (process.env['DEBUG'] === '1' || process.env['DEBUG'] === 'true') {
1294
+ console.log(`[DB] ${message}`);
1295
+ }
1296
+ };
961
1297
  // Singleton instance for global access
962
1298
  let databaseInstance = null;
963
1299
  /**
@@ -965,8 +1301,11 @@ let databaseInstance = null;
965
1301
  */
966
1302
  export async function getFSSLinkDatabase() {
967
1303
  if (!databaseInstance) {
1304
+ debugLog('Creating new FSS Link database instance...');
968
1305
  databaseInstance = new FSSLinkDatabase();
1306
+ debugLog('Initializing FSS Link database...');
969
1307
  await databaseInstance.initialize();
1308
+ debugLog('FSS Link database ready for use');
970
1309
  }
971
1310
  return databaseInstance;
972
1311
  }