264 lines
9.7 KiB
JavaScript
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
|