claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes CodeSearch (hybrid SQLite + pgvector), mem0/memgraph specialists, and all CFN skills.
240 lines (196 loc) • 6.31 kB
text/typescript
/**
* Database Adapter for SQLite operations
* Supports parameterized queries to prevent SQL injection
*/
import { spawn } from 'child_process';
import type { DatabaseAdapter } from './types';
export class SQLiteDatabaseAdapter implements DatabaseAdapter {
private databasePath: string;
constructor(databasePath: string) {
this.databasePath = databasePath;
}
/**
* Execute raw SQL query and return results
*/
async query(sql: string, params?: any[]): Promise<any> {
const results = await this.selectAll(sql, params);
return results.length > 0 ? results[0] : null;
}
/**
* Select single row
*/
async selectOne(sql: string, params?: any[]): Promise<any> {
const results = await this.selectAll(sql, params);
return results.length > 0 ? results[0] : null;
}
/**
* Select all rows
*/
async selectAll(sql: string, params?: any[]): Promise<any[]> {
const output = await this.executeSqlite3(sql, params);
if (!output.trim()) {
return [];
}
// Parse pipe-delimited output
const lines = output.trim().split('\n');
return lines.map(line => this.parsePipeDelimitedRow(line));
}
/**
* Execute SQL statement (INSERT, UPDATE, DELETE)
*/
async execute(sql: string, params?: any[]): Promise<void> {
await this.executeSqlite3(sql, params);
}
/**
* Get affected agents using a skill
*/
async getAffectedAgents(skillId: number): Promise<string[]> {
const results = await this.selectAll(
'SELECT DISTINCT agent_type FROM agent_skill_mappings WHERE skill_id = ?1',
[skillId]
);
return results.map(row => row.agent_type).filter(Boolean);
}
/**
* Close database connection (no-op for SQLite)
*/
async close(): Promise<void> {
// SQLite doesn't require explicit connection closing
}
/**
* Execute sqlite3 command with parameter binding
*/
private async executeSqlite3(sql: string, params?: any[]): Promise<string> {
return new Promise((resolve, reject) => {
const sqlite3Args = [
'-header',
'-separator',
'|',
this.databasePath,
];
const sqlWithParams = this.bindParameters(sql, params);
const process = spawn('sqlite3', sqlite3Args, {
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
process.stdout?.on('data', (data) => {
stdout += data.toString();
});
process.stderr?.on('data', (data) => {
stderr += data.toString();
});
process.on('close', (code) => {
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(`SQLite error: ${stderr || 'unknown error'}`));
}
});
process.stdin?.write(sqlWithParams);
process.stdin?.end();
});
}
/**
* Bind parameters to SQL query
* Simple implementation - in production would use proper binding
*/
private bindParameters(sql: string, params?: any[]): string {
if (!params || params.length === 0) {
return sql;
}
let result = sql;
params.forEach((param, index) => {
const placeholder = `?${index + 1}`;
const value = this.escapeValue(param);
result = result.replace(placeholder, value);
});
return result;
}
/**
* Escape value for SQL safety
*/
private escapeValue(value: any): string {
if (value === null || value === undefined) {
return 'NULL';
}
if (typeof value === 'number') {
return value.toString();
}
if (typeof value === 'boolean') {
return value ? '1' : '0';
}
if (typeof value === 'string') {
// Escape single quotes by doubling them
return `'${value.replace(/'/g, "''")}'`;
}
throw new Error(`Unsupported parameter type: ${typeof value}`);
}
/**
* Parse pipe-delimited row into object
*/
private parsePipeDelimitedRow(line: string): Record<string, any> {
// For simplicity, return as array of values
// In production, would need column names from query
const values = line.split('|');
return {
value: values.length === 1 ? values[0] : values,
};
}
}
/**
* Mock database adapter for testing
*/
export class MockDatabaseAdapter implements DatabaseAdapter {
private data: Map<string, any[]> = new Map();
setTableData(table: string, rows: any[]): void {
this.data.set(table, rows);
}
async query(sql: string, params?: any[]): Promise<any> {
const results = await this.selectAll(sql, params);
return results.length > 0 ? results[0] : null;
}
async selectOne(sql: string, params?: any[]): Promise<any> {
const results = await this.selectAll(sql, params);
return results.length > 0 ? results[0] : null;
}
async selectAll(sql: string, params?: any[]): Promise<any[]> {
// Mock implementation - parse SQL to determine which table to query
if (sql.includes('FROM skills')) {
const skills = this.data.get('skills') || [];
// Handle WHERE name = ?1
if (params && params.length > 0 && sql.includes('WHERE name =')) {
return skills.filter(s => s.name === params[0]);
}
// Handle COUNT(*)
if (sql.includes('COUNT(*)')) {
return [{ count: skills.length, value: skills.length }];
}
return skills;
}
// Default: return empty array or result table
return this.data.get('result') || [];
}
async execute(sql: string, params?: any[]): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
void sql;
void params;
// Mock implementation
}
async getAffectedAgents(skillId: number): Promise<string[]> {
const mappings = this.data.get('agent_skill_mappings') || [];
const agents = new Set<string>();
for (const mapping of mappings) {
if (mapping.skill_id === skillId) {
agents.add(mapping.agent_type);
}
}
return Array.from(agents);
}
async close(): Promise<void> {
this.data.clear();
}
clear(): void {
this.data.clear();
}
}