godcrm/backend/services/data-source/queries.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

212 lines
6.3 KiB
JavaScript

// data-source/queries.js — External database query operations
import { dbGet, dbAll } from '../../database/connection.js';
import { escapeIdentifier } from '../../utils/sqlSanitizer.js';
import { dbLogger } from '../../utils/logger.js';
import { get, getDecryptedPassword } from './crud.js';
/**
* List tables from data source
* @param {string} id - Data source ID
* @returns {Promise<Array>} List of tables
*/
async function listTables(id) {
const dataSource = await get(id);
// For now, only support local_mysql
if (dataSource.type === 'local_mysql') {
const mysql = await import('mysql2/promise');
// ADR-064: Decrypt password for actual connection
const dbPassword = getDecryptedPassword(dataSource);
const connection = await mysql.createConnection({
host: dataSource.db_host,
port: dataSource.db_port,
database: dataSource.db_name,
user: dataSource.db_username,
password: dbPassword
});
try {
const [rows] = await connection.execute('SHOW TABLES');
const tableKey = `Tables_in_${dataSource.db_name}`;
const tables = rows.map(row => ({
name: row[tableKey],
type: 'table'
}));
return tables;
} finally {
await connection.end();
}
}
throw new Error(`Listing tables not supported for type: ${dataSource.type}`);
}
/**
* List columns from a specific table in data source
* @param {string} id - Data source ID
* @param {string} tableName - Table name
* @returns {Promise<Array>} List of columns
*/
async function listTableColumns(id, tableName) {
const dataSource = await get(id);
// For now, only support local_mysql
if (dataSource.type === 'local_mysql') {
const mysql = await import('mysql2/promise');
// ADR-064: Decrypt password for actual connection
const dbPassword = getDecryptedPassword(dataSource);
const connection = await mysql.createConnection({
host: dataSource.db_host,
port: dataSource.db_port,
database: dataSource.db_name,
user: dataSource.db_username,
password: dbPassword
});
try {
const [rows] = await connection.execute(`DESCRIBE \`${tableName}\``);
const columns = rows.map(row => ({
name: row.Field,
type: row.Type
}));
return columns;
} finally {
await connection.end();
}
}
throw new Error(`Listing columns not supported for type: ${dataSource.type}`);
}
/**
* Query data from a specific table in data source
* @param {string} id - Data source ID
* @param {string} tableName - Table name
* @param {Object} options - Query options (limit, offset)
* @returns {Promise<Array>} List of rows as plain objects
*/
async function queryTable(id, tableName, options = {}) {
const { limit = 50, offset = 0 } = options;
const dataSource = await get(id);
// Internal data source - query local SQLite tables directly
if (dataSource.type === 'internal') {
try {
// Use escapeIdentifier for safe table name handling
const safeTableName = escapeIdentifier(tableName);
const rows = await dbAll(
`SELECT * FROM ${safeTableName} LIMIT ? OFFSET ?`,
[limit, offset]
);
dbLogger.debug({ tableName, rowCount: rows.length }, 'Fetched rows from local table');
return rows;
} catch (error) {
dbLogger.error({ err: error, tableName }, 'Error querying local table');
throw error;
}
}
// External MySQL data source
if (dataSource.type === 'local_mysql') {
const mysql = await import('mysql2/promise');
// ADR-064: Decrypt password for actual connection
const dbPassword = getDecryptedPassword(dataSource);
const connection = await mysql.createConnection({
host: dataSource.db_host,
port: dataSource.db_port,
database: dataSource.db_name,
user: dataSource.db_username,
password: dbPassword
});
try {
// Validate table name - MySQL uses backticks for identifiers
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
throw new Error(`Invalid table name: ${tableName}`);
}
const [rows] = await connection.execute(
`SELECT * FROM \`${tableName}\` LIMIT ? OFFSET ?`,
[limit, offset]
);
dbLogger.debug({ tableName, rowCount: rows.length }, 'Fetched rows from external table');
return rows;
} finally {
await connection.end();
}
}
throw new Error(`Querying table not supported for type: ${dataSource.type}`);
}
/**
* Query single row by ID from external data source
* @param {string} id - Data source ID
* @param {string} tableName - Table name
* @param {string|number} rowId - Row ID
* @returns {Promise<Object|null>} Row data or null
*/
async function queryRowById(id, tableName, rowId) {
const dataSource = await get(id);
// Internal data source
if (dataSource.type === 'internal') {
try {
// Use escapeIdentifier for safe table name handling
const safeTableName = escapeIdentifier(tableName);
const row = await dbGet(
`SELECT * FROM ${safeTableName} WHERE id = ?`,
[rowId]
);
return row;
} catch (error) {
dbLogger.error({ err: error, tableName, rowId }, 'Error querying row by ID');
throw error;
}
}
// External MySQL data source
if (dataSource.type === 'local_mysql') {
const mysql = await import('mysql2/promise');
// ADR-064: Decrypt password for actual connection
const dbPassword = getDecryptedPassword(dataSource);
const connection = await mysql.createConnection({
host: dataSource.db_host,
port: dataSource.db_port,
database: dataSource.db_name,
user: dataSource.db_username,
password: dbPassword
});
try {
// Validate table name - MySQL uses backticks for identifiers
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
throw new Error(`Invalid table name: ${tableName}`);
}
const [rows] = await connection.execute(
`SELECT * FROM \`${tableName}\` WHERE id = ?`,
[rowId]
);
dbLogger.debug({ tableName, rowId }, 'Fetched row from external table');
return rows.length > 0 ? rows[0] : null;
} finally {
await connection.end();
}
}
throw new Error(`Query row by ID not supported for type: ${dataSource.type}`);
}
export {
listTables,
listTableColumns,
queryTable,
queryRowById,
};