import { createHash } from "node:crypto"; import { getTargetPool } from "../../db/target.js"; import { AppError } from "../../lib/errors.js"; import { guardSql } from "../../lib/sql-guard.js"; import type { SessionUser } from "../../types/auth.js"; import { createAuditEvent } from "../audit/audit.service.js"; function isReadOnly(user: SessionUser) { if (user.isRoot) { return false; } const hasWrite = user.permissions.some((grant) => grant.action === "write" || grant.action === "schema_change"); return !hasWrite; } export async function executeSql(sql: string, user: SessionUser, context: { ip?: string; userAgent?: string }) { if (process.env.FEATURE_SQL_CONSOLE === "false") { throw new AppError(403, "SQL_CONSOLE_DISABLED", "SQL console feature is disabled"); } const guard = guardSql(sql, { allowMultiStatement: user.isRoot, readOnly: isReadOnly(user), allowSchemaChanges: user.isRoot || user.permissions.some((grant) => grant.action === "schema_change") }); const pool = getTargetPool(); const startedAt = Date.now(); const result = await pool.query(guard.normalized); const durationMs = Date.now() - startedAt; const maskedSql = guard.normalized.slice(0, 4000); await createAuditEvent({ actorUserId: user.id, action: "sql.execute", resourceType: "sql_console", resourceName: guard.statementType, sqlTextMasked: maskedSql, payloadAfter: { rowCount: result.rowCount, durationMs, statementHash: createHash("sha256").update(guard.normalized).digest("hex") }, ip: context.ip ?? null, userAgent: context.userAgent ?? null, status: "success" }); return { rows: result.rows, fields: result.fields.map((field) => ({ name: field.name, dataTypeId: field.dataTypeID })), rowCount: result.rowCount ?? 0, durationMs, statementType: guard.statementType, notice: guard.isMutating ? "Mutation executed" : "Query executed" }; }