Files
tasq/node_modules/agentdb/dist/src/db-fallback.js
T
2026-04-09 19:01:53 +08:00

264 lines
9.7 KiB
JavaScript

/**
* Database System using sql.js (WASM SQLite)
* Pure JavaScript implementation with NO build dependencies
*
* SECURITY: Fixed SQL injection vulnerabilities:
* - PRAGMA commands validated against whitelist
* - Removed eval() usage (replaced with async import)
*/
import { validatePragmaCommand, ValidationError } from './security/input-validation.js';
import * as fs from 'fs';
import * as path from 'path';
let sqlJsWrapper = null;
/**
* Get sql.js database implementation (ONLY sql.js, no better-sqlite3)
*/
export async function getDatabaseImplementation() {
// Return cached wrapper
if (sqlJsWrapper) {
return sqlJsWrapper;
}
try {
console.log('✅ Using sql.js (WASM SQLite, no build tools required)');
// sql.js requires async initialization
const mod = await import('sql.js');
const SQL = await mod.default();
// Create database wrapper
sqlJsWrapper = createSqlJsWrapper(SQL);
return sqlJsWrapper;
}
catch (error) {
console.error('❌ Failed to initialize sql.js:', error.message);
throw new Error('Failed to initialize SQLite. Please ensure sql.js is installed:\n' +
'npm install sql.js');
}
}
/**
* Create a better-sqlite3 compatible wrapper around sql.js
* This allows AgentDB to work (with reduced performance) without native compilation
*/
function createSqlJsWrapper(SQL) {
return class SqlJsDatabase {
db;
filename;
activeStatements = new Map();
statementCounter = 0;
intervalId = null;
constructor(filename, options) {
this.filename = filename;
// In-memory database
if (filename === ':memory:') {
this.db = new SQL.Database();
}
else {
// File-based database - use safe fs module (no eval)
try {
if (fs.existsSync(filename)) {
const buffer = fs.readFileSync(filename);
this.db = new SQL.Database(buffer);
}
else {
this.db = new SQL.Database();
}
}
catch (error) {
console.warn('⚠️ Could not read database file:', error.message);
this.db = new SQL.Database();
}
}
// Warn if too many active statements (memory leak detection)
this.intervalId = setInterval(() => {
if (this.activeStatements.size > 50) {
console.warn(`⚠️ Detected ${this.activeStatements.size} active SQL statements - possible memory leak`);
}
}, 10000);
}
prepare(sql) {
const stmt = this.db.prepare(sql);
let isFinalized = false;
const stmtId = ++this.statementCounter;
// Track active statement
this.activeStatements.set(stmtId, stmt);
return {
run: (...params) => {
if (isFinalized)
throw new Error('Statement already finalized');
try {
stmt.bind(params);
stmt.step();
stmt.reset();
return {
changes: this.db.getRowsModified(),
lastInsertRowid: this.db.exec('SELECT last_insert_rowid()')[0]?.values[0]?.[0] || 0
};
}
catch (error) {
// Auto-free on error to prevent memory leak
if (!isFinalized) {
stmt.free();
isFinalized = true;
this.activeStatements.delete(stmtId);
}
throw error;
}
},
get: (...params) => {
if (isFinalized)
throw new Error('Statement already finalized');
try {
stmt.bind(params);
const hasRow = stmt.step();
if (!hasRow) {
stmt.reset();
return undefined;
}
const columns = stmt.getColumnNames();
const values = stmt.get();
stmt.reset();
const result = {};
columns.forEach((col, idx) => {
result[col] = values[idx];
});
return result;
}
catch (error) {
// Auto-free on error to prevent memory leak
if (!isFinalized) {
stmt.free();
isFinalized = true;
this.activeStatements.delete(stmtId);
}
throw error;
}
},
all: (...params) => {
if (isFinalized)
throw new Error('Statement already finalized');
try {
stmt.bind(params);
const results = [];
while (stmt.step()) {
const columns = stmt.getColumnNames();
const values = stmt.get();
const result = {};
columns.forEach((col, idx) => {
result[col] = values[idx];
});
results.push(result);
}
stmt.reset();
return results;
}
catch (error) {
// Auto-free on error to prevent memory leak
if (!isFinalized) {
stmt.free();
isFinalized = true;
this.activeStatements.delete(stmtId);
}
throw error;
}
},
finalize: () => {
if (!isFinalized) {
stmt.free();
isFinalized = true;
this.activeStatements.delete(stmtId);
}
}
};
}
exec(sql) {
return this.db.exec(sql);
}
save() {
// Save to file if needed
if (this.filename !== ':memory:') {
try {
// Create parent directories if they don't exist
const dir = path.dirname(this.filename);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const data = this.db.export();
fs.writeFileSync(this.filename, Buffer.from(data));
}
catch (error) {
console.error('❌ Could not save database to file:', error.message);
throw error;
}
}
}
close() {
// Clear interval timer
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
// Free all active statements to prevent memory leaks
for (const [stmtId, stmt] of this.activeStatements.entries()) {
try {
stmt.free();
}
catch (e) {
// Statement may already be freed
}
}
this.activeStatements.clear();
// Save to file before closing
this.save();
this.db.close();
}
pragma(pragma, options) {
try {
// SECURITY: Validate PRAGMA command against whitelist to prevent SQL injection
const validatedPragma = validatePragmaCommand(pragma);
// Execute validated PRAGMA
const result = this.db.exec(`PRAGMA ${validatedPragma}`);
return result[0]?.values[0]?.[0];
}
catch (error) {
if (error instanceof ValidationError) {
console.error(`❌ Invalid PRAGMA command: ${error.message}`);
throw error;
}
throw error;
}
}
transaction(fn) {
// Return a function that executes the transaction when called
// This matches better-sqlite3 API where transaction() returns a callable function
return () => {
try {
this.db.exec('BEGIN TRANSACTION');
const result = fn();
this.db.exec('COMMIT');
return result;
}
catch (error) {
this.db.exec('ROLLBACK');
throw error;
}
};
}
};
}
/**
* Create a database instance using sql.js
*/
export async function createDatabase(filename, options) {
const DatabaseImpl = await getDatabaseImplementation();
return new DatabaseImpl(filename, options);
}
/**
* Get information about current database implementation
*/
export function getDatabaseInfo() {
return {
implementation: 'sql.js (WASM)',
isNative: false,
performance: 'medium',
requiresBuildTools: false
};
}
//# sourceMappingURL=db-fallback.js.map