claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
360 lines (359 loc) • 13.7 kB
JavaScript
/**
* Redis Database Adapter
*
* Implements IDatabaseAdapter for Redis key-value store.
* Part of Task 0.4: Database Query Abstraction Layer (MVP)
*
* UPDATED: Now uses ConnectionPoolManager for proper connection pool initialization,
* health checks, automatic reconnection with exponential backoff, and connection metrics.
*
* SECURITY: Supports Redis authentication via requirepass with secure password handling
*/ import { randomUUID } from 'crypto';
import { DatabaseErrorCode, createDatabaseError, createSuccessResult, createFailedResult, mapRedisError } from './errors.js';
import { ConnectionPoolManager } from './connection-pool-manager.js';
import { v4 as uuidv4 } from 'uuid';
export class RedisAdapter {
poolManager = null;
client = null;
config;
connected = false;
errorAggregator;
correlationId;
constructor(config, errorAggregator){
this.config = config;
this.errorAggregator = errorAggregator;
this.correlationId = uuidv4();
}
/**
* Track error with error aggregator
* @private
*/ trackError(error, operation, context) {
if (this.errorAggregator) {
const dbError = error.code ? error : createDatabaseError(DatabaseErrorCode.QUERY_FAILED, `Redis ${operation} failed`, error instanceof Error ? error : new Error(String(error)), context);
this.errorAggregator.addError('redis', dbError, {
...context,
operation,
correlationId: this.correlationId
});
}
}
/**
* Record successful operation with error aggregator
* @private
*/ recordSuccess() {
if (this.errorAggregator) {
this.errorAggregator.recordSuccess('redis');
}
}
getType() {
return 'redis';
}
async connect() {
try {
// Initialize connection pool manager
this.poolManager = new ConnectionPoolManager(this.config);
await this.poolManager.initialize();
// Get the Redis client from pool manager
this.client = await this.poolManager.acquire();
// Start health checks (ping every 30s)
this.poolManager.startHealthChecks();
this.connected = true;
this.recordSuccess();
} catch (err) {
const error = createDatabaseError(DatabaseErrorCode.CONNECTION_FAILED, 'Failed to connect to Redis', err instanceof Error ? err : new Error(String(err)), {
config: this.config,
correlationId: this.correlationId
});
this.trackError(error, 'connect');
throw error;
}
}
async disconnect() {
if (this.poolManager) {
await this.poolManager.shutdown();
this.poolManager = null;
this.client = null;
this.connected = false;
}
}
isConnected() {
return this.connected && this.poolManager !== null && this.client !== null;
}
/**
* Get connection pool statistics
*/ getPoolStats() {
return this.poolManager?.getStats();
}
async get(key) {
this.ensureConnected();
try {
const value = await this.client.get(key);
if (value === null) {
return null;
}
// Try to parse as JSON, fall back to raw string
try {
this.recordSuccess();
return JSON.parse(value);
} catch {
this.recordSuccess();
return value;
}
} catch (err) {
const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err)));
const error = createDatabaseError(errorCode, `Failed to get key: ${key}`, err instanceof Error ? err : new Error(String(err)), {
key,
correlationId: this.correlationId
});
this.trackError(error, 'get', {
key
});
throw error;
}
}
async list(pattern, options) {
this.ensureConnected();
try {
const keys = await this.client.keys(pattern);
if (keys.length === 0) {
return [];
}
const values = await this.client.mGet(keys);
const results = values.map((value, index)=>{
if (value === null) {
return null;
}
try {
return JSON.parse(value);
} catch {
return value;
}
}).filter((v)=>v !== null);
// Apply limit and offset
const start = options?.offset || 0;
const end = options?.limit ? start + options.limit : undefined;
this.recordSuccess();
return results.slice(start, end);
} catch (err) {
const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err)));
const error = createDatabaseError(errorCode, `Failed to list keys: ${pattern}`, err instanceof Error ? err : new Error(String(err)), {
pattern,
options,
correlationId: this.correlationId
});
this.trackError(error, 'list', {
pattern
});
throw error;
}
}
async query(pattern, filters) {
// For Redis, query is similar to list but with additional filtering
const results = await this.list(pattern);
// Apply filters
return results.filter((item)=>{
return filters.every((filter)=>{
const value = item[filter.field];
switch(filter.operator){
case 'eq':
return value === filter.value;
case 'ne':
return value !== filter.value;
case 'gt':
return value > filter.value;
case 'gte':
return value >= filter.value;
case 'lt':
return value < filter.value;
case 'lte':
return value <= filter.value;
case 'in':
return Array.isArray(filter.value) && filter.value.includes(value);
case 'like':
return String(value).includes(String(filter.value));
default:
return true;
}
});
});
}
async insert(key, data) {
this.ensureConnected();
try {
const value = typeof data === 'string' ? data : JSON.stringify(data);
await this.client.set(key, value);
this.recordSuccess();
return createSuccessResult(data, 1);
} catch (err) {
const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err)));
const error = createDatabaseError(errorCode, `Failed to insert key: ${key}`, err instanceof Error ? err : new Error(String(err)), {
key,
data,
correlationId: this.correlationId
});
this.trackError(error, 'insert', {
key
});
return createFailedResult(error);
}
}
async insertMany(pattern, data) {
this.ensureConnected();
try {
const pipeline = this.client.multi();
data.forEach((item, index)=>{
const key = `${pattern}:${index}`;
const value = typeof item === 'string' ? item : JSON.stringify(item);
pipeline.set(key, value);
});
await pipeline.exec();
this.recordSuccess();
return createSuccessResult(data, data.length);
} catch (err) {
const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err)));
const error = createDatabaseError(errorCode, `Failed to insert multiple keys with pattern: ${pattern}`, err instanceof Error ? err : new Error(String(err)), {
pattern,
count: data.length,
correlationId: this.correlationId
});
this.trackError(error, 'insertMany', {
pattern,
count: data.length
});
return createFailedResult(error);
}
}
async update(key, data) {
this.ensureConnected();
try {
// Get existing data
const existing = await this.get(key);
if (existing === null) {
const error = createDatabaseError(DatabaseErrorCode.NOT_FOUND, `Key not found: ${key}`, undefined, {
key,
correlationId: this.correlationId
});
this.trackError(error, 'update', {
key
});
return createFailedResult(error);
}
// Merge with updates
const updated = {
...existing,
...data
};
const value = JSON.stringify(updated);
await this.client.set(key, value);
this.recordSuccess();
return createSuccessResult(updated, 1);
} catch (err) {
const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err)));
const error = createDatabaseError(errorCode, `Failed to update key: ${key}`, err instanceof Error ? err : new Error(String(err)), {
key,
data,
correlationId: this.correlationId
});
this.trackError(error, 'update', {
key
});
return createFailedResult(error);
}
}
async delete(_table, key) {
this.ensureConnected();
try {
const count = await this.client.del(key);
if (count === 0) {
const error = createDatabaseError(DatabaseErrorCode.NOT_FOUND, `Key not found: ${key}`, undefined, {
key,
correlationId: this.correlationId
});
this.trackError(error, 'delete', {
key
});
return createFailedResult(error);
}
this.recordSuccess();
return createSuccessResult(undefined, count);
} catch (err) {
const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err)));
const error = createDatabaseError(errorCode, `Failed to delete key: ${key}`, err instanceof Error ? err : new Error(String(err)), {
key,
correlationId: this.correlationId
});
this.trackError(error, 'delete', {
key
});
return createFailedResult(error);
}
}
async raw(command, params) {
this.ensureConnected();
try {
const result = await this.client.sendCommand([
command,
...params || []
]);
this.recordSuccess();
return result;
} catch (err) {
const errorCode = mapRedisError(err instanceof Error ? err : new Error(String(err)));
const error = createDatabaseError(errorCode, `Failed to execute raw command: ${command}`, err instanceof Error ? err : new Error(String(err)), {
command,
params,
correlationId: this.correlationId
});
this.trackError(error, 'raw', {
command
});
throw error;
}
}
async beginTransaction() {
return {
id: `redis-tx-${randomUUID()}`,
databases: [
'redis'
],
startTime: new Date(),
status: 'pending'
};
}
async prepareTransaction(context) {
try {
// Redis doesn't support traditional two-phase commit
// PREPARE validation: Check if Redis is available and can accept commands
this.ensureConnected();
// Test connection and command execution
await this.client.ping();
// Mark as prepared
context.status = 'prepared';
context.preparedAt = new Date();
return true;
} catch (err) {
// Prepare failed - typically due to connection issues
throw createDatabaseError(DatabaseErrorCode.TRANSACTION_FAILED, 'Failed to prepare transaction - Redis unavailable', err instanceof Error ? err : new Error(String(err)), {
transactionId: context.id
});
}
}
async commitTransaction(context) {
// Redis transactions are handled via MULTI/EXEC
// This is a placeholder for cross-database transaction support
context.status = 'committed';
}
async rollbackTransaction(context) {
// Redis doesn't support traditional rollback
// This is a placeholder for cross-database transaction support
context.status = 'rolled_back';
}
ensureConnected() {
if (!this.isConnected()) {
throw createDatabaseError(DatabaseErrorCode.CONNECTION_FAILED, 'Not connected to Redis', undefined, {
config: this.config
});
}
}
}
//# sourceMappingURL=redis-adapter.js.map