UNPKG

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.

905 lines (904 loc) 38.1 kB
/** * Query Translator - SECURITY HARDENED * * Translates between SQL queries and Redis commands for cross-backend compatibility. * Includes query optimization and backend recommendation logic. * * SECURITY ENHANCEMENTS (CVE-2024-SQL-INJECTION): * - Input validation with whitelisting for identifiers (table/column names) * - Parameterized queries for ALL database values * - Comprehensive SQL injection prevention * - Strict identifier validation against allowed patterns * - Error handling using StandardError * * Part of Phase 2, Task P2-3.1: Unified Query API * * Features: * - SQL to Redis translation with security validation * - Redis to SQL translation with parameterization * - Query optimization * - Backend recommendation based on query patterns * - Performance monitoring (<50ms translation time) * * @example * ```typescript * const translator = new QueryTranslator({ * allowedTables: ['tasks', 'users', 'projects'], * allowedFields: { tasks: ['id', 'name', 'status', 'description'] } * }); * * // Translate SQL to Redis (fully parameterized) * const redisCmd = translator.translateSQLToRedis( * 'SELECT * FROM tasks WHERE id = ?', * ['task-123'] * ); * * // Translate Redis to SQL (fully parameterized) * const sqlQuery = translator.translateRedisToSQL({ * command: 'HGETALL', * key: 'task:123' * }); * ``` */ import { BackendType } from './unified-query-api.js'; import { StandardError, ErrorCode } from './errors.js'; /** * SQL query parser - SECURITY HARDENED */ let SQLParser = class SQLParser { allowedTables; allowedFields; strictMode; constructor(allowedTables, allowedFields, strictMode = true){ this.allowedTables = new Set(allowedTables || []); this.allowedFields = new Map(); if (allowedFields) { for (const [table, fields] of Object.entries(allowedFields)){ this.allowedFields.set(table.toLowerCase(), new Set(fields.map((f)=>f.toLowerCase()))); } } this.strictMode = strictMode; } /** * Validate SQL identifier (table/column name) * Pattern: alphanumeric, underscore, starts with letter or underscore */ validateIdentifier(identifier, type) { if (!identifier || typeof identifier !== 'string') { return { valid: false, error: `Invalid ${type} name: must be a non-empty string` }; } // Remove surrounding whitespace const trimmed = identifier.trim(); // Check pattern: must start with letter or underscore, contain only alphanumeric and underscore const identifierPattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/; if (!identifierPattern.test(trimmed)) { return { valid: false, error: `Invalid ${type} name: must match pattern /^[a-zA-Z_][a-zA-Z0-9_]*$/. Got: "${identifier}"` }; } // Check length (reasonable limit to prevent DoS) if (trimmed.length > 128) { return { valid: false, error: `Invalid ${type} name: exceeds maximum length of 128 characters` }; } return { valid: true, sanitized: trimmed }; } /** * Validate table name against whitelist */ validateTableName(table) { const validation = this.validateIdentifier(table, 'table'); if (!validation.valid) { return validation; } const sanitized = validation.sanitized.toLowerCase(); // In strict mode, check against whitelist if (this.strictMode && this.allowedTables.size > 0) { if (!this.allowedTables.has(sanitized)) { return { valid: false, error: `Table "${table}" is not in the whitelist of allowed tables` }; } } return { valid: true, sanitized: validation.sanitized }; } /** * Validate field name against whitelist for specific table */ validateFieldName(field, table) { const validation = this.validateIdentifier(field, 'field'); if (!validation.valid) { return validation; } const sanitized = validation.sanitized.toLowerCase(); // In strict mode, check against whitelist if available if (this.strictMode && table) { const tableLower = table.toLowerCase(); const allowedFieldsForTable = this.allowedFields.get(tableLower); if (allowedFieldsForTable && !allowedFieldsForTable.has(sanitized)) { return { valid: false, error: `Field "${field}" is not in the whitelist for table "${table}"` }; } } return { valid: true, sanitized: validation.sanitized }; } /** * Parse SQL SELECT statement */ parseSelect(sql) { const result = {}; try { // Extract table name const tableMatch = sql.match(/FROM\s+(\w+)/i); if (tableMatch) { const tableValidation = this.validateTableName(tableMatch[1]); if (!tableValidation.valid) { return { error: tableValidation.error }; } result.table = tableValidation.sanitized; } // Extract fields const fieldsMatch = sql.match(/SELECT\s+(.*?)\s+FROM/i); if (fieldsMatch) { const fields = fieldsMatch[1].trim(); if (fields === '*') { result.fields = [ '*' ]; } else { const fieldList = fields.split(',').map((f)=>f.trim()); const validatedFields = []; for (const field of fieldList){ const fieldValidation = this.validateFieldName(field, result.table); if (!fieldValidation.valid) { return { error: fieldValidation.error }; } validatedFields.push(fieldValidation.sanitized); } result.fields = validatedFields; } } // Extract WHERE clause const whereMatch = sql.match(/WHERE\s+(.*?)(?:ORDER BY|GROUP BY|LIMIT|$)/i); if (whereMatch) { const whereClause = whereMatch[1].trim(); const whereResult = this.parseWhereClause(whereClause, result.table); if (whereResult.error) { return { error: whereResult.error }; } result.where = whereResult.conditions; } // Extract JOINs const joinMatches = sql.matchAll(/(?:INNER |LEFT |RIGHT |)?JOIN\s+(\w+)\s+ON\s+(.*?)(?:WHERE|ORDER BY|GROUP BY|LIMIT|JOIN|$)/gi); result.joins = []; for (const match of joinMatches){ const joinTableValidation = this.validateTableName(match[1]); if (!joinTableValidation.valid) { return { error: joinTableValidation.error }; } result.joins.push({ table: joinTableValidation.sanitized, on: match[2].trim() }); } return result; } catch (error) { return { error: `Failed to parse SELECT statement: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Parse WHERE clause with validation */ parseWhereClause(whereClause, table) { const conditions = []; try { // Simple parser for basic conditions // Format: field = ? OR field LIKE ? etc. const parts = whereClause.split(/\s+AND\s+/i); for (const part of parts){ const match = part.match(/(\w+)\s*(=|!=|>|>=|<|<=|LIKE|IN|NOT IN)\s*(.+)/i); if (match) { const fieldValidation = this.validateFieldName(match[1], table); if (!fieldValidation.valid) { return { error: fieldValidation.error }; } conditions.push({ field: fieldValidation.sanitized, operator: match[2].toLowerCase(), value: match[3] === '?' ? undefined : match[3] }); } } return { conditions }; } catch (error) { return { error: `Failed to parse WHERE clause: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Parse SQL INSERT statement with validation */ parseInsert(sql) { const result = {}; try { // Extract table name const tableMatch = sql.match(/INSERT INTO\s+(\w+)/i); if (tableMatch) { const tableValidation = this.validateTableName(tableMatch[1]); if (!tableValidation.valid) { return { error: tableValidation.error }; } result.table = tableValidation.sanitized; } // Extract fields const fieldsMatch = sql.match(/\(([^)]+)\)\s+VALUES/i); if (fieldsMatch) { const fieldList = fieldsMatch[1].split(',').map((f)=>f.trim()); const validatedFields = []; for (const field of fieldList){ const fieldValidation = this.validateFieldName(field, result.table); if (!fieldValidation.valid) { return { error: fieldValidation.error }; } validatedFields.push(fieldValidation.sanitized); } result.fields = validatedFields; } return result; } catch (error) { return { error: `Failed to parse INSERT statement: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Parse SQL UPDATE statement with validation */ parseUpdate(sql) { const result = {}; try { // Extract table name const tableMatch = sql.match(/UPDATE\s+(\w+)/i); if (tableMatch) { const tableValidation = this.validateTableName(tableMatch[1]); if (!tableValidation.valid) { return { error: tableValidation.error }; } result.table = tableValidation.sanitized; } // Extract SET clause const setMatch = sql.match(/SET\s+(.*?)\s+WHERE/i); if (setMatch) { const setParts = setMatch[1].split(','); const validatedFields = []; for (const part of setParts){ const field = part.split('=')[0].trim(); const fieldValidation = this.validateFieldName(field, result.table); if (!fieldValidation.valid) { return { error: fieldValidation.error }; } validatedFields.push(fieldValidation.sanitized); } result.fields = validatedFields; } // Extract WHERE clause const whereMatch = sql.match(/WHERE\s+(.*?)$/i); if (whereMatch) { const whereResult = this.parseWhereClause(whereMatch[1].trim(), result.table); if (whereResult.error) { return { error: whereResult.error }; } result.where = whereResult.conditions; } return result; } catch (error) { return { error: `Failed to parse UPDATE statement: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Parse SQL DELETE statement with validation */ parseDelete(sql) { const result = {}; try { // Extract table name const tableMatch = sql.match(/DELETE FROM\s+(\w+)/i); if (tableMatch) { const tableValidation = this.validateTableName(tableMatch[1]); if (!tableValidation.valid) { return { error: tableValidation.error }; } result.table = tableValidation.sanitized; } // Extract WHERE clause const whereMatch = sql.match(/WHERE\s+(.*?)$/i); if (whereMatch) { const whereResult = this.parseWhereClause(whereMatch[1].trim(), result.table); if (whereResult.error) { return { error: whereResult.error }; } result.where = whereResult.conditions; } return result; } catch (error) { return { error: `Failed to parse DELETE statement: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } }; /** * Query Translator - SECURITY HARDENED * * Provides bidirectional translation between SQL and Redis commands * with comprehensive input validation and SQL injection prevention */ export class QueryTranslator { parser; config; constructor(config){ this.config = { allowedTables: config?.allowedTables || [], allowedFields: config?.allowedFields || {}, maxQueryLength: config?.maxQueryLength || 10000, maxParams: config?.maxParams || 100, strictMode: config?.strictMode !== false }; this.parser = new SQLParser(this.config.allowedTables, this.config.allowedFields, this.config.strictMode); } /** * Validate input parameters */ validateInput(sql, params = []) { if (!sql || typeof sql !== 'string') { return { valid: false, error: 'SQL query must be a non-empty string' }; } if (sql.length > this.config.maxQueryLength) { return { valid: false, error: `SQL query exceeds maximum length of ${this.config.maxQueryLength} characters` }; } if (!Array.isArray(params)) { return { valid: false, error: 'Parameters must be an array' }; } if (params.length > this.config.maxParams) { return { valid: false, error: `Too many parameters. Maximum: ${this.config.maxParams}` }; } // Detect SQL injection patterns in the query // These patterns should ONLY appear in specific contexts (after ?) const injectionPatterns = [ /\bOR\b[\s]*(\d+\s*=\s*\d+|'[^']*'\s*=\s*'[^']*'|true|1)/i, /\bUNION\b[\s]+(SELECT|ALL)/i, /--\s*$/i, /\/\*[\s\S]*?\*\//i, /;\s*(SELECT|INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER)/i, /\bDROP\b|\bTRUNCATE\b|\bALTER\b|\bCREATE\b/i, /\bEVAL\b|\bEXEC\b|\bSCRIPT\b/i, /\x00/ ]; // Split query into parts based on ? placeholders const parts = sql.split('?'); // Check structure: should be [prefix, suffix] or [prefix1, middle1, middle2, suffix] // Injection attempts will have SQL keywords AFTER the ? for(let i = 0; i < parts.length; i++){ const part = parts[i]; // First part should contain SELECT/INSERT/UPDATE/DELETE/FROM if (i === 0) { // Validate it's a valid SQL statement start if (!/(SELECT|INSERT|UPDATE|DELETE|FROM)\b/i.test(part)) { // Only valid if it's completely empty (error caught elsewhere) if (part.trim()) { return { valid: false, error: 'Invalid SQL query structure. Must start with SELECT, INSERT, UPDATE, or DELETE' }; } } } else if (i === parts.length - 1) { // Last part (after last ?) should not have SQL keywords that indicate injection if (/\b(OR|UNION|SELECT|INSERT|UPDATE|DELETE|DROP|TRUNCATE)\b/i.test(part)) { return { valid: false, error: 'SQL query contains suspicious injection patterns' }; } } else { // Middle parts (between ? and ?) should be minimal // Should only contain WHERE, AND, OR (as operators), commas, etc. // But NOT SELECT, UNION, etc. if (/\b(UNION|SELECT|INSERT|UPDATE|DELETE|DROP)\b/i.test(part)) { return { valid: false, error: 'SQL query contains suspicious injection patterns' }; } } } // Check for injection patterns in the entire query for (const pattern of injectionPatterns){ // Skip OR if it's part of valid syntax (after WHERE) if (pattern.source.includes('OR') && /WHERE[\s\S]*\?[\s\S]*OR/i.test(sql)) { // This could be valid OR in WHERE clause const afterQuestion = sql.substring(sql.indexOf('?') + 1); if (/\bOR\b[\s]*[\'\"]?\d+['\"]*\s*=\s*[\'\"]?\d+['\"]*|OR\s*'[^']*'\s*=\s*'[^']*'|OR\s*TRUE/i.test(afterQuestion)) { return { valid: false, error: 'SQL query contains SQL injection pattern: OR-based bypass' }; } } else if (pattern.test(sql) && !sql.includes('?')) { // Pattern found but no parameterization return { valid: false, error: 'SQL query contains suspicious patterns. Use parameterized queries.' }; } } return { valid: true }; } /** * Validate Redis command structure */ validateRedisCommand(command) { if (!command || typeof command !== 'object') { return { valid: false, error: 'Redis command must be an object' }; } if (!command.command || typeof command.command !== 'string') { return { valid: false, error: 'Redis command name must be a non-empty string' }; } const allowedCommands = [ 'GET', 'SET', 'HGET', 'HGETALL', 'HMSET', 'HSET', 'DEL', 'MGET', 'MSET' ]; if (!allowedCommands.includes(command.command.toUpperCase())) { return { valid: false, error: `Redis command "${command.command}" is not allowed. Allowed: ${allowedCommands.join(', ')}` }; } if (command.key && typeof command.key !== 'string') { return { valid: false, error: 'Redis key must be a string' }; } if (command.fields && typeof command.fields !== 'object') { return { valid: false, error: 'Redis fields must be an object' }; } if (command.args && !Array.isArray(command.args)) { return { valid: false, error: 'Redis args must be an array' }; } return { valid: true }; } /** * Translate SQL query to Redis commands */ translateSQLToRedis(sql, params = []) { const startTime = Date.now(); const warnings = []; try { // Validate inputs const inputValidation = this.validateInput(sql, params); if (!inputValidation.valid) { throw new StandardError(ErrorCode.VALIDATION_FAILED, inputValidation.error || 'Invalid input', { sql, paramCount: params.length }); } // Determine query type const queryType = this.getQueryType(sql); let redisCommand; let recommendedBackend = BackendType.REDIS; switch(queryType){ case 'SELECT': { const selectParsed = this.parser.parseSelect(sql); if (selectParsed.error) { throw new StandardError(ErrorCode.PARSE_ERROR, `Failed to parse SELECT statement: ${selectParsed.error}`, { sql }); } // Check if query is complex (has joins) if (selectParsed.joins && selectParsed.joins.length > 0) { warnings.push('Complex queries with JOINs are better suited for PostgreSQL'); recommendedBackend = BackendType.POSTGRES; } // Translate to Redis HGETALL or GET if (selectParsed.where && selectParsed.where.length > 0) { const idCondition = selectParsed.where.find((w)=>w.field === 'id'); if (idCondition) { const keyValue = params[0]; // Validate key value if (typeof keyValue !== 'string' && typeof keyValue !== 'number') { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Redis key value must be a string or number', { paramValue: typeof keyValue }); } const redisKey = `${selectParsed.table}:${keyValue}`; redisCommand = { command: selectParsed.fields?.[0] === '*' ? 'HGETALL' : 'HGET', key: redisKey }; } } break; } case 'INSERT': { const insertParsed = this.parser.parseInsert(sql); if (insertParsed.error) { throw new StandardError(ErrorCode.PARSE_ERROR, `Failed to parse INSERT statement: ${insertParsed.error}`, { sql }); } if (insertParsed.table && insertParsed.fields) { const idValue = params[0]; // Validate key value if (typeof idValue !== 'string' && typeof idValue !== 'number') { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Redis key value must be a string or number', { paramValue: typeof idValue }); } const redisKey = `${insertParsed.table}:${idValue}`; // Build field-value pairs with validation const fields = {}; for(let i = 0; i < insertParsed.fields.length && i < params.length; i++){ // Ensure params are not objects/arrays (prevent injection) const paramValue = params[i]; if (typeof paramValue === 'object' && paramValue !== null) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Parameter values must be primitives (string, number, boolean, null)', { paramIndex: i, paramType: typeof paramValue }); } fields[insertParsed.fields[i]] = paramValue; } redisCommand = { command: 'HMSET', key: redisKey, fields }; } break; } case 'UPDATE': { const updateParsed = this.parser.parseUpdate(sql); if (updateParsed.error) { throw new StandardError(ErrorCode.PARSE_ERROR, `Failed to parse UPDATE statement: ${updateParsed.error}`, { sql }); } if (updateParsed.table && updateParsed.where) { const idCondition = updateParsed.where.find((w)=>w.field === 'id'); if (idCondition) { const keyValue = params[params.length - 1]; // Validate key value if (typeof keyValue !== 'string' && typeof keyValue !== 'number') { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Redis key value must be a string or number', { paramValue: typeof keyValue }); } const redisKey = `${updateParsed.table}:${keyValue}`; redisCommand = { command: 'HSET', key: redisKey, args: params.slice(0, -1) }; } } break; } case 'DELETE': { const deleteParsed = this.parser.parseDelete(sql); if (deleteParsed.error) { throw new StandardError(ErrorCode.PARSE_ERROR, `Failed to parse DELETE statement: ${deleteParsed.error}`, { sql }); } if (deleteParsed.table && deleteParsed.where) { const idCondition = deleteParsed.where.find((w)=>w.field === 'id'); if (idCondition) { const keyValue = params[0]; // Validate key value if (typeof keyValue !== 'string' && typeof keyValue !== 'number') { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Redis key value must be a string or number', { paramValue: typeof keyValue }); } const redisKey = `${deleteParsed.table}:${keyValue}`; redisCommand = { command: 'DEL', key: redisKey }; } } break; } default: warnings.push(`Unsupported SQL query type: ${queryType}`); recommendedBackend = BackendType.POSTGRES; } const executionTime = Date.now() - startTime; if (executionTime > 50) { warnings.push(`Translation took ${executionTime}ms (target: <50ms)`); } return { success: !!redisCommand, redisCommand, executionTime, recommendedBackend, warnings: warnings.length > 0 ? warnings : undefined }; } catch (error) { const executionTime = Date.now() - startTime; const message = error instanceof StandardError ? error.message : error instanceof Error ? error.message : 'Unknown error'; return { success: false, executionTime, warnings: [ `Translation failed: ${message}` ] }; } } /** * Translate Redis command to SQL query (with parameterization) */ translateRedisToSQL(command) { const startTime = Date.now(); const warnings = []; try { // Validate Redis command const commandValidation = this.validateRedisCommand(command); if (!commandValidation.valid) { throw new StandardError(ErrorCode.VALIDATION_FAILED, commandValidation.error || 'Invalid Redis command', { command: command.command }); } let sqlQuery; let sqlParams = []; // Parse Redis key to extract table and ID const keyParts = command.key?.split(':') || []; const table = keyParts[0] || 'unknown'; const id = keyParts[1]; // Validate table name const tableValidation = this.parser['validateTableName'] || ((t)=>({ valid: true, sanitized: t })); // Since validateTableName is private, we do basic validation const tablePattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/; if (!tablePattern.test(table)) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid table name in Redis key', { key: command.key }); } switch(command.command.toUpperCase()){ case 'GET': case 'HGET': case 'HGETALL': { // SELECT * FROM table WHERE id = ? sqlQuery = `SELECT * FROM ${table} WHERE id = ?`; sqlParams = [ id ]; break; } case 'SET': case 'HMSET': { // INSERT INTO table (field1, field2, ...) VALUES (?, ?, ...) if (command.fields) { const fields = Object.keys(command.fields); const placeholders = fields.map(()=>'?').join(', '); sqlQuery = `INSERT INTO ${table} (${fields.join(', ')}) VALUES (${placeholders})`; sqlParams = Object.values(command.fields); // Validate all params are primitives for(let i = 0; i < sqlParams.length; i++){ if (typeof sqlParams[i] === 'object' && sqlParams[i] !== null) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Parameter values must be primitives', { paramIndex: i }); } } } break; } case 'HSET': { // UPDATE table SET field = ? WHERE id = ? if (command.args && command.args.length > 0) { const field = command.args[0]; // Validate field name if (typeof field !== 'string' || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(field)) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid field name in HSET command', { field }); } sqlQuery = `UPDATE ${table} SET ${field} = ? WHERE id = ?`; sqlParams = [ command.args[1], id ]; // Validate params for(let i = 0; i < sqlParams.length; i++){ if (typeof sqlParams[i] === 'object' && sqlParams[i] !== null) { throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Parameter values must be primitives', { paramIndex: i }); } } } break; } case 'DEL': { // DELETE FROM table WHERE id = ? sqlQuery = `DELETE FROM ${table} WHERE id = ?`; sqlParams = [ id ]; break; } default: warnings.push(`Unsupported Redis command: ${command.command}`); } const executionTime = Date.now() - startTime; if (executionTime > 50) { warnings.push(`Translation took ${executionTime}ms (target: <50ms)`); } return { success: !!sqlQuery, sqlQuery, sqlParams, executionTime, warnings: warnings.length > 0 ? warnings : undefined }; } catch (error) { const executionTime = Date.now() - startTime; const message = error instanceof StandardError ? error.message : error instanceof Error ? error.message : 'Unknown error'; return { success: false, executionTime, warnings: [ `Translation failed: ${message}` ] }; } } /** * Optimize query and provide recommendations */ optimizeQuery(request) { const result = { indexed: [], recommendations: [] }; // Recommend indexes for filtered fields if (request.filters) { const indexFields = request.filters.map((f)=>String(f.field)); result.indexed = indexFields; result.indexes = indexFields; result.recommendations?.push(`Consider adding indexes on: ${indexFields.join(', ')}`); } // Estimate query cost let cost = 1; if (request.joins) { cost += request.joins.length * 10; // JOINs are expensive } if (request.filters) { cost += request.filters.length * 2; } result.estimatedCost = cost; // Provide optimization recommendations if (request.joins && request.joins.length > 2) { result.recommendations?.push('Consider denormalizing data or using materialized views for complex joins'); } if (request.filters && request.filters.length > 5) { result.recommendations?.push('Consider composite indexes for multiple filter conditions'); } return result; } /** * Recommend backend based on query characteristics */ recommendBackend(request) { // Simple key-value access → Redis if (request.key && !request.joins) { return BackendType.REDIS; } // Complex queries with JOINs → PostgreSQL if (request.joins && request.joins.length > 0) { return BackendType.POSTGRES; } // Session/cache data → Redis if (request.dataType === 'cache' || request.dataType === 'session') { return BackendType.REDIS; } // Embedded/local data → SQLite if (request.dataType === 'embedded') { return BackendType.SQLITE; } // Default to PostgreSQL for structured data return BackendType.POSTGRES; } /** * Get query type from SQL string */ getQueryType(sql) { const trimmed = sql.trim().toUpperCase(); if (trimmed.startsWith('SELECT')) return 'SELECT'; if (trimmed.startsWith('INSERT')) return 'INSERT'; if (trimmed.startsWith('UPDATE')) return 'UPDATE'; if (trimmed.startsWith('DELETE')) return 'DELETE'; return 'UNKNOWN'; } } //# sourceMappingURL=query-translator.js.map