76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
/**
|
|
* 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<StepExecutorResult> => {
|
|
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}` };
|
|
}
|
|
});
|