Replace generic Error instances with NativeModuleError in loadDatabaseModule to provide more structured error information, including rebuild status and restart requirements. Update getAuthStatus to re-throw NativeModuleError instances without wrapping.
170 lines
5.6 KiB
JavaScript
170 lines
5.6 KiB
JavaScript
/**
|
|
* SQLite Database Access Module
|
|
* Provides cross-platform database operations for Antigravity state.
|
|
*
|
|
* Uses better-sqlite3 for:
|
|
* - Windows compatibility (no CLI dependency)
|
|
* - Native performance
|
|
* - Synchronous API (simple error handling)
|
|
*
|
|
* Includes auto-rebuild capability for handling Node.js version updates
|
|
* that cause native module incompatibility.
|
|
*/
|
|
|
|
import { createRequire } from 'module';
|
|
import { ANTIGRAVITY_DB_PATH } from '../constants.js';
|
|
import { isModuleVersionError, attemptAutoRebuild, clearRequireCache } from '../utils/native-module-helper.js';
|
|
import { logger } from '../utils/logger.js';
|
|
import { NativeModuleError } from '../errors.js';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
|
|
// Lazy-loaded Database constructor
|
|
let Database = null;
|
|
let moduleLoadError = null;
|
|
|
|
/**
|
|
* Load the better-sqlite3 module with auto-rebuild on version mismatch
|
|
* Uses synchronous require to maintain API compatibility
|
|
* @returns {Function} The Database constructor
|
|
* @throws {Error} If module cannot be loaded even after rebuild
|
|
*/
|
|
function loadDatabaseModule() {
|
|
// Return cached module if already loaded
|
|
if (Database) return Database;
|
|
|
|
// Re-throw cached error if previous load failed permanently
|
|
if (moduleLoadError) throw moduleLoadError;
|
|
|
|
try {
|
|
Database = require('better-sqlite3');
|
|
return Database;
|
|
} catch (error) {
|
|
if (isModuleVersionError(error)) {
|
|
logger.warn('[Database] Native module version mismatch detected');
|
|
|
|
if (attemptAutoRebuild(error)) {
|
|
// Clear require cache and retry
|
|
try {
|
|
const resolvedPath = require.resolve('better-sqlite3');
|
|
// Clear the module and all its dependencies from cache
|
|
clearRequireCache(resolvedPath, require.cache);
|
|
|
|
Database = require('better-sqlite3');
|
|
logger.success('[Database] Module reloaded successfully after rebuild');
|
|
return Database;
|
|
} catch (retryError) {
|
|
// Rebuild succeeded but reload failed - user needs to restart
|
|
moduleLoadError = new NativeModuleError(
|
|
'Native module rebuild completed. Please restart the server to apply the fix.',
|
|
true, // rebuildSucceeded
|
|
true // restartRequired
|
|
);
|
|
logger.info('[Database] Rebuild succeeded - server restart required');
|
|
throw moduleLoadError;
|
|
}
|
|
} else {
|
|
moduleLoadError = new NativeModuleError(
|
|
'Failed to auto-rebuild native module. Please run manually:\n' +
|
|
' npm rebuild better-sqlite3\n' +
|
|
'Or if using npx, find the package location in the error and run:\n' +
|
|
' cd /path/to/better-sqlite3 && npm rebuild',
|
|
false, // rebuildSucceeded
|
|
false // restartRequired
|
|
);
|
|
throw moduleLoadError;
|
|
}
|
|
}
|
|
|
|
// Non-version-mismatch error, just throw it
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query Antigravity database for authentication status
|
|
* @param {string} [dbPath] - Optional custom database path
|
|
* @returns {Object} Parsed auth data with apiKey, email, name, etc.
|
|
* @throws {Error} If database doesn't exist, query fails, or no auth status found
|
|
*/
|
|
export function getAuthStatus(dbPath = ANTIGRAVITY_DB_PATH) {
|
|
const Db = loadDatabaseModule();
|
|
let db;
|
|
try {
|
|
// Open database in read-only mode
|
|
db = new Db(dbPath, {
|
|
readonly: true,
|
|
fileMustExist: true
|
|
});
|
|
|
|
// Prepare and execute query
|
|
const stmt = db.prepare(
|
|
"SELECT value FROM ItemTable WHERE key = 'antigravityAuthStatus'"
|
|
);
|
|
const row = stmt.get();
|
|
|
|
if (!row || !row.value) {
|
|
throw new Error('No auth status found in database');
|
|
}
|
|
|
|
// Parse JSON value
|
|
const authData = JSON.parse(row.value);
|
|
|
|
if (!authData.apiKey) {
|
|
throw new Error('Auth data missing apiKey field');
|
|
}
|
|
|
|
return authData;
|
|
} catch (error) {
|
|
// Enhance error messages for common issues
|
|
if (error.code === 'SQLITE_CANTOPEN') {
|
|
throw new Error(
|
|
`Database not found at ${dbPath}. ` +
|
|
'Make sure Antigravity is installed and you are logged in.'
|
|
);
|
|
}
|
|
// Re-throw with context if not already our error
|
|
if (error.message.includes('No auth status') || error.message.includes('missing apiKey')) {
|
|
throw error;
|
|
}
|
|
// Re-throw native module errors from loadDatabaseModule without wrapping
|
|
if (error instanceof NativeModuleError) {
|
|
throw error;
|
|
}
|
|
throw new Error(`Failed to read Antigravity database: ${error.message}`);
|
|
} finally {
|
|
// Always close database connection
|
|
if (db) {
|
|
db.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if database exists and is accessible
|
|
* @param {string} [dbPath] - Optional custom database path
|
|
* @returns {boolean} True if database exists and can be opened
|
|
*/
|
|
export function isDatabaseAccessible(dbPath = ANTIGRAVITY_DB_PATH) {
|
|
let db;
|
|
try {
|
|
const Db = loadDatabaseModule();
|
|
db = new Db(dbPath, {
|
|
readonly: true,
|
|
fileMustExist: true
|
|
});
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
if (db) {
|
|
db.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
export default {
|
|
getAuthStatus,
|
|
isDatabaseAccessible
|
|
};
|