mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
Merge branch 'master' of github.com:Mintplex-Labs/anything-llm
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import PostgreSQLLogo from "./icons/postgresql.png";
|
||||
import MySQLLogo from "./icons/mysql.png";
|
||||
import MSSQLLogo from "./icons/mssql.png";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { PencilSimple, X } from "@phosphor-icons/react";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import EditSQLConnection from "./SQLConnectionModal";
|
||||
|
||||
export const DB_LOGOS = {
|
||||
postgresql: PostgreSQLLogo,
|
||||
@@ -9,8 +11,16 @@ export const DB_LOGOS = {
|
||||
"sql-server": MSSQLLogo,
|
||||
};
|
||||
|
||||
export default function DBConnection({ connection, onRemove }) {
|
||||
export default function DBConnection({
|
||||
connection,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
setHasChanges,
|
||||
connections = [],
|
||||
}) {
|
||||
const { database_id, engine } = connection;
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
|
||||
function removeConfirmation() {
|
||||
if (
|
||||
!window.confirm(
|
||||
@@ -33,14 +43,33 @@ export default function DBConnection({ connection, onRemove }) {
|
||||
<div className="text-sm font-semibold text-white">{database_id}</div>
|
||||
<div className="mt-1 text-xs text-description">{engine}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={removeConfirmation}
|
||||
className="border-none text-white hover:text-red-500"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
<div className="flex gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
data-tooltip-id="edit-sql-connection-tooltip"
|
||||
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors duration-200 p-1 rounded"
|
||||
onClick={openModal}
|
||||
>
|
||||
<PencilSimple size={18} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-tooltip-id="delete-sql-connection-tooltip"
|
||||
onClick={removeConfirmation}
|
||||
className="border-none text-theme-text-secondary hover:text-red-500"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<EditSQLConnection
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
existingConnection={connection}
|
||||
onSubmit={onUpdate}
|
||||
setHasChanges={setHasChanges}
|
||||
connections={connections}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { WarningOctagon, X } from "@phosphor-icons/react";
|
||||
@@ -7,6 +7,33 @@ import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import Toggle from "@/components/lib/Toggle";
|
||||
|
||||
/**
|
||||
* Converts a string to a URL-friendly slug format.
|
||||
* Matches backend slugify behavior for consistent database_id generation.
|
||||
* @param {string} str - The string to slugify
|
||||
* @returns {string} - The slugified string (lowercase, hyphens, no special chars)
|
||||
*/
|
||||
function slugify(str) {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, "") // Remove special characters
|
||||
.replace(/[\s_]+/g, "-") // Replace spaces and underscores with hyphens
|
||||
.replace(/^-+|-+$/g, ""); // Remove leading/trailing hyphens
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles a database connection string based on the engine type and configuration.
|
||||
* @param {Object} params - Connection parameters
|
||||
* @param {string} params.engine - The database engine ('postgresql', 'mysql', or 'sql-server')
|
||||
* @param {string} [params.username=""] - Database username
|
||||
* @param {string} [params.password=""] - Database password
|
||||
* @param {string} [params.host=""] - Database host/endpoint
|
||||
* @param {string} [params.port=""] - Database port
|
||||
* @param {string} [params.database=""] - Database name
|
||||
* @param {boolean} [params.encrypt=false] - Enable encryption (SQL Server only)
|
||||
* @returns {string|null} - The assembled connection string, error message if fields missing, or null if engine invalid
|
||||
*/
|
||||
function assembleConnectionString({
|
||||
engine,
|
||||
username = "",
|
||||
@@ -32,6 +59,7 @@ function assembleConnectionString({
|
||||
|
||||
const DEFAULT_ENGINE = "postgresql";
|
||||
const DEFAULT_CONFIG = {
|
||||
name: "",
|
||||
username: null,
|
||||
password: null,
|
||||
host: null,
|
||||
@@ -41,15 +69,59 @@ const DEFAULT_CONFIG = {
|
||||
encrypt: false,
|
||||
};
|
||||
|
||||
export default function NewSQLConnection({
|
||||
/**
|
||||
* Modal component for creating or editing SQL database connections.
|
||||
* Supports PostgreSQL, MySQL, and SQL Server with connection validation.
|
||||
* Handles duplicate connection name detection and connection string assembly.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {boolean} props.isOpen - Whether the modal is currently open
|
||||
* @param {Function} props.closeModal - Callback to close the modal
|
||||
* @param {Function} props.onSubmit - Callback when connection is successfully validated and saved
|
||||
* @param {Function} props.setHasChanges - Callback to mark that changes have been made
|
||||
* @param {Object|null} [props.existingConnection=null] - Existing connection data for edit mode (contains database_id, engine, username, password, host, port, database, scheme, encrypt)
|
||||
* @param {Array} [props.connections=[]] - List of all existing connections for duplicate detection
|
||||
* @returns {React.ReactPortal|null} - Portal containing the modal UI, or null if not open
|
||||
*/
|
||||
export default function SQLConnectionModal({
|
||||
isOpen,
|
||||
closeModal,
|
||||
onSubmit,
|
||||
setHasChanges,
|
||||
existingConnection = null, // { database_id, engine } for edit mode
|
||||
connections = [], // List of all existing connections for duplicate detection
|
||||
}) {
|
||||
const isEditMode = !!existingConnection;
|
||||
const [engine, setEngine] = useState(DEFAULT_ENGINE);
|
||||
const [config, setConfig] = useState(DEFAULT_CONFIG);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
// Sync state when modal opens - useState initial values only run once on mount,
|
||||
// so we need this effect to update state when the modal is reopened
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
if (existingConnection) {
|
||||
setEngine(existingConnection.engine);
|
||||
setConfig({
|
||||
name: existingConnection.database_id,
|
||||
username: existingConnection.username,
|
||||
password: existingConnection.password,
|
||||
host: existingConnection.host,
|
||||
port: existingConnection.port,
|
||||
database: existingConnection.database,
|
||||
scheme: existingConnection.scheme,
|
||||
encrypt: existingConnection?.encrypt,
|
||||
});
|
||||
} else {
|
||||
setEngine(DEFAULT_ENGINE);
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
}
|
||||
}, [isOpen, existingConnection]);
|
||||
|
||||
// Track original database ID to send to server for updating if in edit mode
|
||||
const originalDatabaseId = isEditMode ? existingConnection.database_id : null;
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
function handleClose() {
|
||||
@@ -61,6 +133,7 @@ export default function NewSQLConnection({
|
||||
function onFormChange(e) {
|
||||
const form = new FormData(e.target.form);
|
||||
setConfig({
|
||||
name: form.get("name").trim(),
|
||||
username: form.get("username").trim(),
|
||||
password: form.get("password"),
|
||||
host: form.get("host").trim(),
|
||||
@@ -70,14 +143,61 @@ export default function NewSQLConnection({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a connection name (slugified) already exists in the connections list.
|
||||
* For edit mode, excludes the original connection being edited.
|
||||
* @param {string} slugifiedName - The slugified name to check
|
||||
* @returns {boolean} - True if duplicate exists, false otherwise
|
||||
*/
|
||||
function isDuplicateConnectionName(slugifiedName) {
|
||||
// Get active connections (not marked for removal)
|
||||
const activeConnections = connections.filter(
|
||||
(conn) => conn.action !== "remove"
|
||||
);
|
||||
|
||||
// Check for duplicates, excluding the original connection in edit mode
|
||||
return activeConnections.some((conn) => {
|
||||
// In edit mode, skip the original connection being edited
|
||||
if (isEditMode && conn.database_id === originalDatabaseId) {
|
||||
return false;
|
||||
}
|
||||
return conn.database_id === slugifiedName;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles form submission for both creating new connections and updating existing ones.
|
||||
* Process:
|
||||
* 1. Slugify the connection name to match backend behavior
|
||||
* 2. Check for duplicate names (prevents frontend from sending invalid updates)
|
||||
* 3. Validate the connection string by attempting to connect to the database
|
||||
* 4. If valid, submit with appropriate action ("add" or "update")
|
||||
*
|
||||
* For updates: Includes originalDatabaseId so backend can find and replace the old connection
|
||||
* For new connections: Just includes the new connection data
|
||||
*/
|
||||
async function handleUpdate(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const form = new FormData(e.target);
|
||||
const connectionString = assembleConnectionString({ engine, ...config });
|
||||
|
||||
// Slugify the database_id immediately to match backend behavior
|
||||
const slugifiedDatabaseId = slugify(form.get("name"));
|
||||
|
||||
// Check for duplicate connection names before validation
|
||||
if (isDuplicateConnectionName(slugifiedDatabaseId)) {
|
||||
showToast(
|
||||
`A connection with the name "${slugifiedDatabaseId}" already exists. Please choose a different name.`,
|
||||
"error",
|
||||
{ clear: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidating(true);
|
||||
try {
|
||||
// Validate that we can actually connect to this database
|
||||
const { success, error } = await System.validateSQLConnection(
|
||||
engine,
|
||||
connectionString
|
||||
@@ -93,11 +213,30 @@ export default function NewSQLConnection({
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
const connectionData = {
|
||||
engine,
|
||||
database_id: form.get("name"),
|
||||
database_id: slugifiedDatabaseId,
|
||||
connectionString,
|
||||
});
|
||||
};
|
||||
|
||||
if (isEditMode) {
|
||||
// EDIT MODE: Send update action with originalDatabaseId
|
||||
// This tells the backend to find the connection with originalDatabaseId
|
||||
// and replace it with the new connection data
|
||||
onSubmit({
|
||||
...connectionData,
|
||||
action: "update",
|
||||
originalDatabaseId: originalDatabaseId,
|
||||
});
|
||||
} else {
|
||||
// CREATE MODE: Send add action
|
||||
// Backend will check for duplicates and add if unique
|
||||
onSubmit({
|
||||
...connectionData,
|
||||
action: "add",
|
||||
});
|
||||
}
|
||||
|
||||
setHasChanges(true);
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
@@ -123,7 +262,7 @@ export default function NewSQLConnection({
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
New SQL Connection
|
||||
{isEditMode ? "Edit SQL Connection" : "New SQL Connection"}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
@@ -142,8 +281,9 @@ export default function NewSQLConnection({
|
||||
<div className="px-7 py-6">
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
|
||||
<p className="text-sm text-white/60">
|
||||
Add the connection information for your database below and it
|
||||
will be available for future SQL agent calls.
|
||||
{isEditMode
|
||||
? "Update the connection information for your database below."
|
||||
: "Add the connection information for your database below and it will be available for future SQL agent calls."}
|
||||
</p>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="border border-red-800 bg-zinc-800 light:bg-red-200/50 p-4 rounded-lg flex items-center gap-x-2 text-sm text-red-400 light:text-red-500">
|
||||
@@ -191,6 +331,7 @@ export default function NewSQLConnection({
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.name || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -207,6 +348,7 @@ export default function NewSQLConnection({
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.username || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
@@ -214,13 +356,14 @@ export default function NewSQLConnection({
|
||||
Database user password
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
type="password"
|
||||
name="password"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="password123"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.password || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,6 +381,7 @@ export default function NewSQLConnection({
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.host || ""}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -252,6 +396,7 @@ export default function NewSQLConnection({
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.port || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,6 +413,7 @@ export default function NewSQLConnection({
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.database || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -284,6 +430,7 @@ export default function NewSQLConnection({
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.schema || ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -328,6 +475,16 @@ export default function NewSQLConnection({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Database engine selection button component.
|
||||
* Displays a database logo and handles selection state.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.provider - The database provider identifier ('postgresql', 'mysql', 'sql-server')
|
||||
* @param {boolean} props.active - Whether this engine is currently selected
|
||||
* @param {Function} props.onClick - Callback when the engine is clicked
|
||||
* @returns {JSX.Element} - Button element with database logo
|
||||
*/
|
||||
function DBEngine({ provider, active, onClick }) {
|
||||
return (
|
||||
<button
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import DBConnection from "./DBConnection";
|
||||
import { Plus, Database } from "@phosphor-icons/react";
|
||||
import NewSQLConnection from "./NewConnectionModal";
|
||||
import { Plus, Database, CircleNotch } from "@phosphor-icons/react";
|
||||
import NewSQLConnection from "./SQLConnectionModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import SQLAgentImage from "@/media/agents/sql-agent.png";
|
||||
import Admin from "@/models/admin";
|
||||
import Toggle from "@/components/lib/Toggle";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
export default function AgentSQLConnectorSelection({
|
||||
skill,
|
||||
@@ -13,15 +14,40 @@ export default function AgentSQLConnectorSelection({
|
||||
toggleSkill,
|
||||
enabled = false,
|
||||
setHasChanges,
|
||||
hasChanges = false,
|
||||
}) {
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const prevHasChanges = useRef(hasChanges);
|
||||
|
||||
// Load connections on mount
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Admin.systemPreferencesByFields(["agent_sql_connections"])
|
||||
.then((res) => setConnections(res?.settings?.agent_sql_connections ?? []))
|
||||
.catch(() => setConnections([]));
|
||||
.catch(() => setConnections([]))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
// Refresh connections from backend when save completes (hasChanges: true -> false)
|
||||
// This ensures we get clean data without stale action properties
|
||||
useEffect(() => {
|
||||
if (prevHasChanges.current === true && hasChanges === false) {
|
||||
Admin.systemPreferencesByFields(["agent_sql_connections"])
|
||||
.then((res) =>
|
||||
setConnections(res?.settings?.agent_sql_connections ?? [])
|
||||
)
|
||||
.catch(() => {});
|
||||
}
|
||||
prevHasChanges.current = hasChanges;
|
||||
}, [hasChanges]);
|
||||
|
||||
/**
|
||||
* Marks a connection for removal by adding action: "remove".
|
||||
* The connection stays in the array (for undo capability) until saved.
|
||||
* @param {string} databaseId - The database_id of the connection to remove
|
||||
*/
|
||||
function handleRemoveConnection(databaseId) {
|
||||
setHasChanges(true);
|
||||
setConnections((prev) =>
|
||||
@@ -33,6 +59,38 @@ export default function AgentSQLConnectorSelection({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing connection by replacing it in the local state.
|
||||
* This removes the old connection (by originalDatabaseId) and adds the updated version.
|
||||
*
|
||||
* Note: The old connection is removed from local state immediately, but the backend
|
||||
* handles the actual update logic when saved. See mergeConnections in server/models/systemSettings.js
|
||||
*
|
||||
* @param {Object} updatedConnection - The updated connection data
|
||||
* @param {string} updatedConnection.originalDatabaseId - The original database_id before the update
|
||||
* @param {string} updatedConnection.database_id - The new database_id
|
||||
* @param {string} updatedConnection.action - Should be "update"
|
||||
*/
|
||||
function handleUpdateConnection(updatedConnection) {
|
||||
setHasChanges(true);
|
||||
setConnections((prev) =>
|
||||
prev.map((conn) =>
|
||||
conn.database_id === updatedConnection.originalDatabaseId
|
||||
? updatedConnection
|
||||
: conn
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Adds a new connection to the local state with action: "add".
|
||||
* The backend will validate and deduplicate when saved.
|
||||
* @param {Object} newConnection - The new connection data with action: "add"
|
||||
*/
|
||||
function handleAddConnection(newConnection) {
|
||||
setHasChanges(true);
|
||||
setConnections((prev) => [...prev, newConnection]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-2">
|
||||
@@ -84,15 +142,27 @@ export default function AgentSQLConnectorSelection({
|
||||
Your database connections
|
||||
</p>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{connections
|
||||
.filter((connection) => connection.action !== "remove")
|
||||
.map((connection) => (
|
||||
<DBConnection
|
||||
key={connection.database_id}
|
||||
connection={connection}
|
||||
onRemove={handleRemoveConnection}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<CircleNotch
|
||||
size={24}
|
||||
className="animate-spin text-theme-text-primary"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
connections
|
||||
.filter((connection) => connection.action !== "remove")
|
||||
.map((connection) => (
|
||||
<DBConnection
|
||||
key={connection.database_id}
|
||||
connection={connection}
|
||||
onRemove={handleRemoveConnection}
|
||||
onUpdate={handleUpdateConnection}
|
||||
setHasChanges={setHasChanges}
|
||||
connections={connections}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openModal}
|
||||
@@ -121,9 +191,32 @@ export default function AgentSQLConnectorSelection({
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
setHasChanges={setHasChanges}
|
||||
onSubmit={(newDb) =>
|
||||
setConnections((prev) => [...prev, { action: "add", ...newDb }])
|
||||
}
|
||||
onSubmit={handleAddConnection}
|
||||
connections={connections}
|
||||
/>
|
||||
<Tooltip
|
||||
id="edit-sql-connection-tooltip"
|
||||
content="Edit SQL connection"
|
||||
place="top"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs !opacity-100"
|
||||
style={{
|
||||
maxWidth: "250px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
id="delete-sql-connection-tooltip"
|
||||
content="Delete SQL connection"
|
||||
place="top"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs !opacity-100"
|
||||
style={{
|
||||
maxWidth: "250px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -400,6 +400,7 @@ export default function AdminAgents() {
|
||||
configurableSkills[selectedSkill]?.skill
|
||||
)}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChanges={hasChanges}
|
||||
{...configurableSkills[selectedSkill]}
|
||||
/>
|
||||
)}
|
||||
@@ -591,6 +592,7 @@ export default function AdminAgents() {
|
||||
configurableSkills[selectedSkill]?.skill
|
||||
)}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChanges={hasChanges}
|
||||
{...configurableSkills[selectedSkill]}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -376,7 +376,7 @@ function adminEndpoints(app) {
|
||||
break;
|
||||
case "agent_sql_connections":
|
||||
requestedSettings[label] =
|
||||
await SystemSettings.brief.agent_sql_connections();
|
||||
await SystemSettings.agent_sql_connections();
|
||||
break;
|
||||
case "default_agent_skills":
|
||||
requestedSettings[label] = safeJsonParse(setting?.value, []);
|
||||
|
||||
@@ -10,6 +10,9 @@ const { MetaGenerator } = require("../utils/boot/MetaGenerator");
|
||||
const { PGVector } = require("../utils/vectorDbProviders/pgvector");
|
||||
const { NativeEmbedder } = require("../utils/EmbeddingEngines/native");
|
||||
const { getBaseLLMProviderModel } = require("../utils/helpers");
|
||||
const {
|
||||
ConnectionStringParser,
|
||||
} = require("../utils/agents/aibitat/plugins/sql-agent/SQLConnectors/utils");
|
||||
|
||||
function isNullOrNaN(value) {
|
||||
if (value === null) return true;
|
||||
@@ -681,18 +684,31 @@ const SystemSettings = {
|
||||
};
|
||||
},
|
||||
|
||||
// For special retrieval of a key setting that does not expose any credential information
|
||||
brief: {
|
||||
agent_sql_connections: async function () {
|
||||
const setting = await SystemSettings.get({
|
||||
label: "agent_sql_connections",
|
||||
});
|
||||
if (!setting) return [];
|
||||
return safeJsonParse(setting.value, []).map((dbConfig) => {
|
||||
const { connectionString, ...rest } = dbConfig;
|
||||
return rest;
|
||||
});
|
||||
},
|
||||
agent_sql_connections: async function () {
|
||||
const setting = await SystemSettings.get({
|
||||
label: "agent_sql_connections",
|
||||
});
|
||||
if (!setting) return [];
|
||||
|
||||
const connections = safeJsonParse(setting.value, []).map((conn) => {
|
||||
let scheme = conn.engine;
|
||||
if (scheme === "sql-server") scheme = "mssql";
|
||||
if (scheme === "postgresql") scheme = "postgres";
|
||||
const parser = new ConnectionStringParser({ scheme });
|
||||
|
||||
const parsed = parser.parse(conn.connectionString);
|
||||
return {
|
||||
...conn,
|
||||
username: parsed.username,
|
||||
password: parsed.password,
|
||||
host: parsed.hosts?.[0]?.host,
|
||||
port: parsed.hosts?.[0]?.port,
|
||||
database: parsed.endpoint,
|
||||
scheme: parsed.scheme,
|
||||
};
|
||||
});
|
||||
|
||||
return connections;
|
||||
},
|
||||
getFeatureFlags: async function () {
|
||||
return {
|
||||
@@ -742,42 +758,90 @@ const SystemSettings = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges SQL connection updates from the frontend with existing backend connections.
|
||||
* Processes three types of actions: "remove", "update", and "add".
|
||||
*
|
||||
* @param {Array<Object>} existingConnections - Current connections stored in the database
|
||||
* @param {Array<Object>} updates - Connection updates from frontend, each with an action property
|
||||
* @returns {Array<Object>} - The merged connections array
|
||||
*/
|
||||
function mergeConnections(existingConnections = [], updates = []) {
|
||||
let updatedConnections = [...existingConnections];
|
||||
const existingDbIds = existingConnections.map((conn) => conn.database_id);
|
||||
|
||||
// First remove all 'action:remove' candidates from existing connections.
|
||||
const toRemove = updates
|
||||
.filter((conn) => conn.action === "remove")
|
||||
.map((conn) => conn.database_id);
|
||||
updatedConnections = updatedConnections.filter(
|
||||
(conn) => !toRemove.includes(conn.database_id)
|
||||
const connectionsMap = new Map(
|
||||
existingConnections.map((conn) => [conn.database_id, conn])
|
||||
);
|
||||
|
||||
// Next add all 'action:add' candidates into the updatedConnections; We DO NOT validate the connection strings.
|
||||
// but we do validate their database_id is unique.
|
||||
updates
|
||||
.filter((conn) => conn.action === "add")
|
||||
.forEach((update) => {
|
||||
if (!update.connectionString) return; // invalid connection string
|
||||
for (const update of updates) {
|
||||
const {
|
||||
action,
|
||||
database_id,
|
||||
originalDatabaseId,
|
||||
connectionString,
|
||||
engine,
|
||||
} = update;
|
||||
|
||||
// Remap name to be unique to entire set.
|
||||
if (existingDbIds.includes(update.database_id)) {
|
||||
update.database_id = slugify(
|
||||
`${update.database_id}-${v4().slice(0, 4)}`
|
||||
);
|
||||
} else {
|
||||
update.database_id = slugify(update.database_id);
|
||||
switch (action) {
|
||||
case "remove": {
|
||||
connectionsMap.delete(database_id);
|
||||
break;
|
||||
}
|
||||
case "update": {
|
||||
if (!connectionString) continue;
|
||||
const newId = slugify(database_id);
|
||||
|
||||
// Verify original connection exists
|
||||
if (!connectionsMap.has(originalDatabaseId)) {
|
||||
console.warn(
|
||||
`[mergeConnections] Update skipped: Original connection "${originalDatabaseId}" not found`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for name conflict (excluding the one being updated)
|
||||
if (newId !== originalDatabaseId && connectionsMap.has(newId)) {
|
||||
console.warn(
|
||||
`[mergeConnections] Update skipped: New name "${newId}" conflicts with existing connection`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove old and add updated connection
|
||||
connectionsMap.delete(originalDatabaseId);
|
||||
connectionsMap.set(newId, {
|
||||
engine,
|
||||
database_id: newId,
|
||||
connectionString,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
updatedConnections.push({
|
||||
engine: update.engine,
|
||||
database_id: update.database_id,
|
||||
connectionString: update.connectionString,
|
||||
});
|
||||
});
|
||||
case "add": {
|
||||
if (!connectionString) continue;
|
||||
const slugifiedId = slugify(database_id);
|
||||
|
||||
return updatedConnections;
|
||||
// Skip if already exists
|
||||
if (connectionsMap.has(slugifiedId)) {
|
||||
console.warn(
|
||||
`[mergeConnections] Add skipped: Connection "${slugifiedId}" already exists`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
connectionsMap.set(slugifiedId, {
|
||||
engine,
|
||||
database_id: slugifiedId,
|
||||
connectionString,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error("SQL connection update contains an invalid action.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(connectionsMap.values());
|
||||
}
|
||||
|
||||
module.exports.SystemSettings = SystemSettings;
|
||||
|
||||
Reference in New Issue
Block a user