θΏ™ζ˜―indexlocζδΎ›ηš„ζœεŠ‘οΌŒδΈθ¦θΎ“ε…₯任何密码
Skip to content

SQL preflight connection validation #4150

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions frontend/src/models/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,25 @@ const System = {
return newVersion;
},

/**
* Validates a SQL connection string.
* @param {'postgresql'|'mysql'|'sql-server'} engine - the database engine identifier
* @param {string} connectionString - the connection string to validate
* @returns {Promise<{success: boolean, error: string | null}>}
*/
validateSQLConnection: async function (engine, connectionString) {
return fetch(`${API_BASE}/system/validate-sql-connection`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ engine, connectionString }),
})
.then((res) => res.json())
.catch((e) => {
console.error("Failed to validate SQL connection:", e);
return { success: false, error: e.message };
});
},

experimentalFeatures: {
liveSync: LiveDocumentSync,
agentPlugins: AgentPlugins,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,16 @@ export const DB_LOGOS = {
"sql-server": MSSQLLogo,
};

export default function DBConnection({ connection, onRemove, setHasChanges }) {
export default function DBConnection({ connection, onRemove }) {
const { database_id, engine } = connection;
function removeConfirmation() {
if (
!window.confirm(
`Delete ${database_id} from the list of available SQL connections? This cannot be undone.`
)
) {
)
return false;
}
onRemove(database_id);
setHasChanges(true);
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { createPortal } from "react-dom";
import ModalWrapper from "@/components/ModalWrapper";
import { WarningOctagon, X } from "@phosphor-icons/react";
import { DB_LOGOS } from "./DBConnection";
import System from "@/models/system";
import showToast from "@/utils/toast";

function assembleConnectionString({
engine,
Expand Down Expand Up @@ -37,9 +39,15 @@ const DEFAULT_CONFIG = {
encrypt: false,
};

export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
export default function NewSQLConnection({
isOpen,
closeModal,
onSubmit,
setHasChanges,
}) {
const [engine, setEngine] = useState(DEFAULT_ENGINE);
const [config, setConfig] = useState(DEFAULT_CONFIG);
const [isValidating, setIsValidating] = useState(false);
if (!isOpen) return null;

function handleClose() {
Expand All @@ -48,8 +56,8 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
closeModal();
}

function onFormChange() {
const form = new FormData(document.getElementById("sql-connection-form"));
function onFormChange(e) {
const form = new FormData(e.target.form);
setConfig({
username: form.get("username").trim(),
password: form.get("password"),
Expand All @@ -64,12 +72,41 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
e.preventDefault();
e.stopPropagation();
const form = new FormData(e.target);
onSubmit({
engine,
database_id: form.get("name"),
connectionString: assembleConnectionString({ engine, ...config }),
});
handleClose();
const connectionString = assembleConnectionString({ engine, ...config });

setIsValidating(true);
try {
const { success, error } = await System.validateSQLConnection(
engine,
connectionString
);
if (!success) {
showToast(
error ||
"Failed to establish database connection. Please check your connection details.",
"error"
);
setIsValidating(false);
return;
}

onSubmit({
engine,
database_id: form.get("name"),
connectionString,
});
setHasChanges(true);
handleClose();
} catch (error) {
console.error("Error validating connection:", error);
showToast(
error?.message ||
"Failed to validate connection. Please check your connection details.",
"error"
);
} finally {
setIsValidating(false);
}
return false;
}

Expand All @@ -95,8 +132,8 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
</div>
<form
id="sql-connection-form"
onSubmit={handleUpdate}
onChange={onFormChange}
onSubmit={handleUpdate}
>
<div className="px-7 py-6">
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
Expand Down Expand Up @@ -238,7 +275,6 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
name="encrypt"
value="true"
className="sr-only peer"
onChange={onFormChange}
checked={config.encrypt}
/>
<div className="w-11 h-6 bg-theme-settings-input-bg peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
Expand All @@ -265,9 +301,10 @@ export default function NewSQLConnection({ isOpen, closeModal, onSubmit }) {
<button
type="submit"
form="sql-connection-form"
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm"
disabled={isValidating}
className="transition-all duration-300 bg-white text-black hover:opacity-60 px-4 py-2 rounded-lg text-sm disabled:opacity-50"
>
Save connection
{isValidating ? "Validating..." : "Save connection"}
</button>
</div>
</form>
Expand Down
23 changes: 13 additions & 10 deletions frontend/src/pages/Admin/Agents/SQLConnectorSelection/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ export default function AgentSQLConnectorSelection({
.catch(() => setConnections([]));
}, []);

function handleRemoveConnection(databaseId) {
setHasChanges(true);
setConnections((prev) =>
prev.map((conn) => {
if (conn.database_id === databaseId)
return { ...conn, action: "remove" };
return conn;
})
);
}

return (
<>
<div className="p-2">
Expand Down Expand Up @@ -81,16 +92,7 @@ export default function AgentSQLConnectorSelection({
<DBConnection
key={connection.database_id}
connection={connection}
onRemove={(databaseId) => {
setHasChanges(true);
setConnections((prev) =>
prev.map((conn) => {
if (conn.database_id === databaseId)
return { ...conn, action: "remove" };
return conn;
})
);
}}
onRemove={handleRemoveConnection}
/>
))}
<button
Expand Down Expand Up @@ -120,6 +122,7 @@ export default function AgentSQLConnectorSelection({
<NewSQLConnection
isOpen={isOpen}
closeModal={closeModal}
setHasChanges={setHasChanges}
onSubmit={(newDb) =>
setConnections((prev) => [...prev, { action: "add", ...newDb }])
}
Expand Down
36 changes: 36 additions & 0 deletions server/endpoints/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -1383,6 +1383,42 @@ function systemEndpoints(app) {
}
}
);

app.post(
"/system/validate-sql-connection",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
async (request, response) => {
try {
const { engine, connectionString } = reqBody(request);
if (!engine || !connectionString) {
return response.status(400).json({
success: false,
error: "Both engine and connection details are required.",
});
}

const {
validateConnection,
} = require("../utils/agents/aibitat/plugins/sql-agent/SQLConnectors");
const result = await validateConnection(engine, { connectionString });

if (!result.success) {
return response.status(200).json({
success: false,
error: `Unable to connect to ${engine}. Please verify your connection details.`,
});
}

response.status(200).json(result);
} catch (error) {
console.error("SQL validation error:", error);
response.status(500).json({
success: false,
error: `Unable to connect to ${engine}. Please verify your connection details.`,
});
}
}
);
}

module.exports = { systemEndpoints };
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class MSSQLConnector {
/**
*
* @param {string} queryString the SQL query to be run
* @returns {import(".").QueryResult}
* @returns {Promise<import(".").QueryResult>}
*/
async runQuery(queryString = "") {
const result = { rows: [], count: 0, error: null };
Expand All @@ -75,12 +75,24 @@ class MSSQLConnector {
console.log(this.constructor.name, err);
result.error = err.message;
} finally {
await this._client.close();
this.#connected = false;
// Check client is connected before closing since we use this for validation
if (this._client) {
await this._client.close();
this.#connected = false;
}
}
return result;
}

async validateConnection() {
try {
const result = await this.runQuery("SELECT 1");
return { success: !result.error, error: result.error };
} catch (error) {
return { success: false, error: error.message };
}
}

getTablesSql() {
return `SELECT name FROM sysobjects WHERE xtype='U';`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class MySQLConnector {
/**
*
* @param {string} queryString the SQL query to be run
* @returns {import(".").QueryResult}
* @returns {Promise<import(".").QueryResult>}
*/
async runQuery(queryString = "") {
const result = { rows: [], count: 0, error: null };
Expand All @@ -42,12 +42,24 @@ class MySQLConnector {
console.log(this.constructor.name, err);
result.error = err.message;
} finally {
await this._client.end();
this.#connected = false;
// Check client is connected before closing since we use this for validation
if (this._client) {
await this._client.end();
this.#connected = false;
}
}
return result;
}

async validateConnection() {
try {
const result = await this.runQuery("SELECT 1");
return { success: !result.error, error: result.error };
} catch (error) {
return { success: false, error: error.message };
}
}

getTablesSql() {
return `SELECT table_name FROM information_schema.tables WHERE table_schema = '${this.database_id}'`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class PostgresSQLConnector {
/**
*
* @param {string} queryString the SQL query to be run
* @returns {import(".").QueryResult}
* @returns {Promise<import(".").QueryResult>}
*/
async runQuery(queryString = "") {
const result = { rows: [], count: 0, error: null };
Expand All @@ -35,12 +35,24 @@ class PostgresSQLConnector {
console.log(this.constructor.name, err);
result.error = err.message;
} finally {
await this._client.end();
this.#connected = false;
// Check client is connected before closing since we use this for validation
if (this._client) {
await this._client.end();
this.#connected = false;
}
}
return result;
}

async validateConnection() {
try {
const result = await this.runQuery("SELECT 1");
return { success: !result.error, error: result.error };
} catch (error) {
return { success: false, error: error.message };
}
}

getTablesSql() {
return `SELECT * FROM pg_catalog.pg_tables WHERE schemaname = 'public'`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,27 @@ async function listSQLConnections() {
);
}

/**
* Validates a SQL connection by attempting to connect and run a simple query
* @param {SQLEngine} identifier - The SQL engine type
* @param {object} connectionConfig - The connection configuration
* @returns {Promise<{success: boolean, error: string|null}>}
*/
async function validateConnection(identifier = "", connectionConfig = {}) {
try {
const client = getDBClient(identifier, connectionConfig);
return await client.validateConnection();
} catch (error) {
console.log(`Failed to connect to ${identifier} database.`);
return {
success: false,
error: `Unable to connect to ${identifier}. Please verify your connection details.`,
};
}
}

module.exports = {
getDBClient,
listSQLConnections,
validateConnection,
};