Merge branch 'master' of github.com:Mintplex-Labs/anything-llm

This commit is contained in:
Timothy Carambat 2026-01-29 16:43:28 -08:00
commit f9473cc8b1
6 changed files with 420 additions and 75 deletions

View File

@ -1,7 +1,9 @@
import PostgreSQLLogo from "./icons/postgresql.png"; import PostgreSQLLogo from "./icons/postgresql.png";
import MySQLLogo from "./icons/mysql.png"; import MySQLLogo from "./icons/mysql.png";
import MSSQLLogo from "./icons/mssql.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 = { export const DB_LOGOS = {
postgresql: PostgreSQLLogo, postgresql: PostgreSQLLogo,
@ -9,8 +11,16 @@ export const DB_LOGOS = {
"sql-server": MSSQLLogo, "sql-server": MSSQLLogo,
}; };
export default function DBConnection({ connection, onRemove }) { export default function DBConnection({
connection,
onRemove,
onUpdate,
setHasChanges,
connections = [],
}) {
const { database_id, engine } = connection; const { database_id, engine } = connection;
const { isOpen, openModal, closeModal } = useModal();
function removeConfirmation() { function removeConfirmation() {
if ( if (
!window.confirm( !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="text-sm font-semibold text-white">{database_id}</div>
<div className="mt-1 text-xs text-description">{engine}</div> <div className="mt-1 text-xs text-description">{engine}</div>
</div> </div>
<div className="flex gap-x-2">
<button <button
type="button" type="button"
onClick={removeConfirmation} data-tooltip-id="edit-sql-connection-tooltip"
className="border-none text-white hover:text-red-500" className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors duration-200 p-1 rounded"
onClick={openModal}
> >
<X size={24} /> <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> </button>
</div> </div>
</div> </div>
<EditSQLConnection
isOpen={isOpen}
closeModal={closeModal}
existingConnection={connection}
onSubmit={onUpdate}
setHasChanges={setHasChanges}
connections={connections}
/>
</div>
); );
} }

View File

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
import { WarningOctagon, X } from "@phosphor-icons/react"; import { WarningOctagon, X } from "@phosphor-icons/react";
@ -7,6 +7,33 @@ import System from "@/models/system";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import Toggle from "@/components/lib/Toggle"; 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({ function assembleConnectionString({
engine, engine,
username = "", username = "",
@ -32,6 +59,7 @@ function assembleConnectionString({
const DEFAULT_ENGINE = "postgresql"; const DEFAULT_ENGINE = "postgresql";
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
name: "",
username: null, username: null,
password: null, password: null,
host: null, host: null,
@ -41,15 +69,59 @@ const DEFAULT_CONFIG = {
encrypt: false, 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, isOpen,
closeModal, closeModal,
onSubmit, onSubmit,
setHasChanges, 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 [engine, setEngine] = useState(DEFAULT_ENGINE);
const [config, setConfig] = useState(DEFAULT_CONFIG); const [config, setConfig] = useState(DEFAULT_CONFIG);
const [isValidating, setIsValidating] = useState(false); 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; if (!isOpen) return null;
function handleClose() { function handleClose() {
@ -61,6 +133,7 @@ export default function NewSQLConnection({
function onFormChange(e) { function onFormChange(e) {
const form = new FormData(e.target.form); const form = new FormData(e.target.form);
setConfig({ setConfig({
name: form.get("name").trim(),
username: form.get("username").trim(), username: form.get("username").trim(),
password: form.get("password"), password: form.get("password"),
host: form.get("host").trim(), 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) { async function handleUpdate(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const form = new FormData(e.target); const form = new FormData(e.target);
const connectionString = assembleConnectionString({ engine, ...config }); 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); setIsValidating(true);
try { try {
// Validate that we can actually connect to this database
const { success, error } = await System.validateSQLConnection( const { success, error } = await System.validateSQLConnection(
engine, engine,
connectionString connectionString
@ -93,11 +213,30 @@ export default function NewSQLConnection({
return; return;
} }
onSubmit({ const connectionData = {
engine, engine,
database_id: form.get("name"), database_id: slugifiedDatabaseId,
connectionString, 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); setHasChanges(true);
handleClose(); handleClose();
} catch (error) { } 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="relative p-6 border-b rounded-t border-theme-modal-border">
<div className="w-full flex gap-x-2 items-center"> <div className="w-full flex gap-x-2 items-center">
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap"> <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> </h3>
</div> </div>
<button <button
@ -142,8 +281,9 @@ export default function NewSQLConnection({
<div className="px-7 py-6"> <div className="px-7 py-6">
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2"> <div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
<p className="text-sm text-white/60"> <p className="text-sm text-white/60">
Add the connection information for your database below and it {isEditMode
will be available for future SQL agent calls. ? "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> </p>
<div className="flex flex-col w-full"> <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"> <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} required={true}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
defaultValue={config.name || ""}
/> />
</div> </div>
@ -207,6 +348,7 @@ export default function NewSQLConnection({
required={true} required={true}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
defaultValue={config.username || ""}
/> />
</div> </div>
<div className="flex flex-col"> <div className="flex flex-col">
@ -214,13 +356,14 @@ export default function NewSQLConnection({
Database user password Database user password
</label> </label>
<input <input
type="text" type="password"
name="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" 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" placeholder="password123"
required={true} required={true}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
defaultValue={config.password || ""}
/> />
</div> </div>
</div> </div>
@ -238,6 +381,7 @@ export default function NewSQLConnection({
required={true} required={true}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
defaultValue={config.host || ""}
/> />
</div> </div>
<div> <div>
@ -252,6 +396,7 @@ export default function NewSQLConnection({
required={false} required={false}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
defaultValue={config.port || ""}
/> />
</div> </div>
</div> </div>
@ -268,6 +413,7 @@ export default function NewSQLConnection({
required={true} required={true}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
defaultValue={config.database || ""}
/> />
</div> </div>
@ -284,6 +430,7 @@ export default function NewSQLConnection({
required={false} required={false}
autoComplete="off" autoComplete="off"
spellCheck={false} spellCheck={false}
defaultValue={config.schema || ""}
/> />
</div> </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 }) { function DBEngine({ provider, active, onClick }) {
return ( return (
<button <button

View File

@ -1,11 +1,12 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
import DBConnection from "./DBConnection"; import DBConnection from "./DBConnection";
import { Plus, Database } from "@phosphor-icons/react"; import { Plus, Database, CircleNotch } from "@phosphor-icons/react";
import NewSQLConnection from "./NewConnectionModal"; import NewSQLConnection from "./SQLConnectionModal";
import { useModal } from "@/hooks/useModal"; import { useModal } from "@/hooks/useModal";
import SQLAgentImage from "@/media/agents/sql-agent.png"; import SQLAgentImage from "@/media/agents/sql-agent.png";
import Admin from "@/models/admin"; import Admin from "@/models/admin";
import Toggle from "@/components/lib/Toggle"; import Toggle from "@/components/lib/Toggle";
import { Tooltip } from "react-tooltip";
export default function AgentSQLConnectorSelection({ export default function AgentSQLConnectorSelection({
skill, skill,
@ -13,15 +14,40 @@ export default function AgentSQLConnectorSelection({
toggleSkill, toggleSkill,
enabled = false, enabled = false,
setHasChanges, setHasChanges,
hasChanges = false,
}) { }) {
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
const [connections, setConnections] = useState([]); const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(true);
const prevHasChanges = useRef(hasChanges);
// Load connections on mount
useEffect(() => { useEffect(() => {
setLoading(true);
Admin.systemPreferencesByFields(["agent_sql_connections"]) Admin.systemPreferencesByFields(["agent_sql_connections"])
.then((res) => setConnections(res?.settings?.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) { function handleRemoveConnection(databaseId) {
setHasChanges(true); setHasChanges(true);
setConnections((prev) => 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 ( return (
<> <>
<div className="p-2"> <div className="p-2">
@ -84,15 +142,27 @@ export default function AgentSQLConnectorSelection({
Your database connections Your database connections
</p> </p>
<div className="flex flex-col gap-y-3"> <div className="flex flex-col gap-y-3">
{connections {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") .filter((connection) => connection.action !== "remove")
.map((connection) => ( .map((connection) => (
<DBConnection <DBConnection
key={connection.database_id} key={connection.database_id}
connection={connection} connection={connection}
onRemove={handleRemoveConnection} onRemove={handleRemoveConnection}
onUpdate={handleUpdateConnection}
setHasChanges={setHasChanges}
connections={connections}
/> />
))} ))
)}
<button <button
type="button" type="button"
onClick={openModal} onClick={openModal}
@ -121,9 +191,32 @@ export default function AgentSQLConnectorSelection({
isOpen={isOpen} isOpen={isOpen}
closeModal={closeModal} closeModal={closeModal}
setHasChanges={setHasChanges} setHasChanges={setHasChanges}
onSubmit={(newDb) => onSubmit={handleAddConnection}
setConnections((prev) => [...prev, { action: "add", ...newDb }]) 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",
}}
/> />
</> </>
); );

View File

@ -400,6 +400,7 @@ export default function AdminAgents() {
configurableSkills[selectedSkill]?.skill configurableSkills[selectedSkill]?.skill
)} )}
setHasChanges={setHasChanges} setHasChanges={setHasChanges}
hasChanges={hasChanges}
{...configurableSkills[selectedSkill]} {...configurableSkills[selectedSkill]}
/> />
)} )}
@ -591,6 +592,7 @@ export default function AdminAgents() {
configurableSkills[selectedSkill]?.skill configurableSkills[selectedSkill]?.skill
)} )}
setHasChanges={setHasChanges} setHasChanges={setHasChanges}
hasChanges={hasChanges}
{...configurableSkills[selectedSkill]} {...configurableSkills[selectedSkill]}
/> />
)} )}

View File

@ -376,7 +376,7 @@ function adminEndpoints(app) {
break; break;
case "agent_sql_connections": case "agent_sql_connections":
requestedSettings[label] = requestedSettings[label] =
await SystemSettings.brief.agent_sql_connections(); await SystemSettings.agent_sql_connections();
break; break;
case "default_agent_skills": case "default_agent_skills":
requestedSettings[label] = safeJsonParse(setting?.value, []); requestedSettings[label] = safeJsonParse(setting?.value, []);

View File

@ -10,6 +10,9 @@ const { MetaGenerator } = require("../utils/boot/MetaGenerator");
const { PGVector } = require("../utils/vectorDbProviders/pgvector"); const { PGVector } = require("../utils/vectorDbProviders/pgvector");
const { NativeEmbedder } = require("../utils/EmbeddingEngines/native"); const { NativeEmbedder } = require("../utils/EmbeddingEngines/native");
const { getBaseLLMProviderModel } = require("../utils/helpers"); const { getBaseLLMProviderModel } = require("../utils/helpers");
const {
ConnectionStringParser,
} = require("../utils/agents/aibitat/plugins/sql-agent/SQLConnectors/utils");
function isNullOrNaN(value) { function isNullOrNaN(value) {
if (value === null) return true; 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 () { agent_sql_connections: async function () {
const setting = await SystemSettings.get({ const setting = await SystemSettings.get({
label: "agent_sql_connections", label: "agent_sql_connections",
}); });
if (!setting) return []; if (!setting) return [];
return safeJsonParse(setting.value, []).map((dbConfig) => {
const { connectionString, ...rest } = dbConfig; const connections = safeJsonParse(setting.value, []).map((conn) => {
return rest; 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 () { getFeatureFlags: async function () {
return { 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 = []) { function mergeConnections(existingConnections = [], updates = []) {
let updatedConnections = [...existingConnections]; const connectionsMap = new Map(
const existingDbIds = existingConnections.map((conn) => conn.database_id); existingConnections.map((conn) => [conn.database_id, conn])
// 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)
); );
// Next add all 'action:add' candidates into the updatedConnections; We DO NOT validate the connection strings. for (const update of updates) {
// but we do validate their database_id is unique. const {
updates action,
.filter((conn) => conn.action === "add") database_id,
.forEach((update) => { originalDatabaseId,
if (!update.connectionString) return; // invalid connection string connectionString,
engine,
} = update;
// Remap name to be unique to entire set. switch (action) {
if (existingDbIds.includes(update.database_id)) { case "remove": {
update.database_id = slugify( connectionsMap.delete(database_id);
`${update.database_id}-${v4().slice(0, 4)}` 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`
); );
} else { break;
update.database_id = slugify(update.database_id);
} }
updatedConnections.push({ // Check for name conflict (excluding the one being updated)
engine: update.engine, if (newId !== originalDatabaseId && connectionsMap.has(newId)) {
database_id: update.database_id, console.warn(
connectionString: update.connectionString, `[mergeConnections] Update skipped: New name "${newId}" conflicts with existing connection`
}); );
}); break;
}
return updatedConnections; // Remove old and add updated connection
connectionsMap.delete(originalDatabaseId);
connectionsMap.set(newId, {
engine,
database_id: newId,
connectionString,
});
break;
}
case "add": {
if (!connectionString) continue;
const slugifiedId = slugify(database_id);
// 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; module.exports.SystemSettings = SystemSettings;