/** * Step Executor: db_query * Run a parameterized read-only SQL query against local Postgres. * Useful for trend analysis, history lookups, aggregations. * * Config: * query: SQL string with $1, $2 etc. placeholders * params: array of template strings for parameter values * output_key: context key to store results (default: 'query_result') * single_row: if true, store only first row instead of array * * Security: Only SELECT statements allowed. No mutations. */ import { registerStepExecutor } from '../pipeline-engine'; import { postgresClient } from '../postgres-client'; import { StepExecutorResult, PipelineStep, PipelineContext } from '../../types/pipeline'; const FORBIDDEN_KEYWORDS = [ 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'CREATE', 'TRUNCATE', 'GRANT', 'REVOKE', 'COPY', 'EXECUTE', 'CALL', ]; registerStepExecutor('db_query', async ( step: PipelineStep, context: PipelineContext, executionId: number ): Promise => { const config = step.config as { query?: string; params?: string[]; output_key?: string; single_row?: boolean; }; const query = config.query || ''; const params = config.params || []; const outputKey = config.output_key || 'query_result'; const singleRow = config.single_row ?? false; if (!query) { return { success: false, output: {}, error: 'query is required' }; } // Security: block mutations const upperQuery = query.toUpperCase().replace(/\s+/g, ' '); for (const keyword of FORBIDDEN_KEYWORDS) { // Check for keyword as a standalone word (not inside a string literal) const regex = new RegExp(`\\b${keyword}\\b`); if (regex.test(upperQuery)) { return { success: false, output: {}, error: `Forbidden SQL keyword: ${keyword}. Only SELECT queries are allowed.`, }; } } try { const result = await postgresClient.query(query, params); const rows = result.rows; const value = singleRow ? (rows[0] || null) : rows; return { success: true, output: { [outputKey]: value, [`${outputKey}_count`]: rows.length, }, }; } catch (error) { const msg = error instanceof Error ? error.message : String(error); return { success: false, output: { [outputKey]: null }, error: `DB query failed: ${msg}` }; } });