{ "name": "connection-string", "type": "registry:ui", "title": "Connection String", "description": "A DSN parsed into the parts that matter for its driver. A file-backed database has a path and no host, user or port — pretending every database is Postgres is what makes most of these components useless outside it.", "category": "Database", "dependencies": [ "lucide-react@^1.39.0" ], "registryDependencies": [ "badge", "button", "copy-button", "lib-motion", "lib-styles", "lib-utils" ], "files": [ { "path": "components/ui/connection-string.tsx", "type": "registry:ui", "content": "'use client'\n\nimport { useMemo, useState, type ComponentProps, type ReactNode } from 'react'\nimport { Eye, EyeOff, TriangleAlert } from 'lucide-react'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { CopyButton } from '@/components/ui/copy-button'\nimport { enterFade } from '@/lib/motion'\nimport { radius, surface } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A connection string, parsed into the parts that matter for its driver.\n *\n * Which parts those are is not universal, and pretending otherwise is what\n * makes most of these components useless outside Postgres. A file-backed\n * database has a path and no host, user or port; a cluster URL has several\n * hosts and no single one; an ODBC string is not a URL at all. So the shape is\n * driven by a per-driver profile, and `drivers` lets you add or replace any of\n * them without touching the component.\n *\n * Two formats are parsed: URL-style (`postgres://…`, `sqlite:///…`) and the\n * semicolon key-value style SQL Server and ODBC use. A string that parses as\n * neither is reported rather than half-rendered from a regex that matched the\n * wrong separator.\n *\n * The secret is masked by default. This is the screen people paste into\n * tickets, and a DSN carries a live password in the middle of it.\n */\nexport type DsnField = 'user' | 'host' | 'port' | 'database' | 'path' | 'protocol'\n\nexport type DriverProfile = {\n /** Display name for the driver. */\n label: string\n /** Which parts to show, in order. */\n fields: DsnField[]\n /** Per-driver overrides for the field headings. */\n labels?: Partial>\n /** The path is a file on disk, not a network address. */\n fileBased?: boolean\n /** Shown when the port is absent, so the effective value is visible. */\n defaultPort?: number\n /** Parameters worth flagging, mapped to why. */\n warn?: Record\n}\n\nconst NETWORK: DsnField[] = ['protocol', 'user', 'host', 'port', 'database']\nconst FILE: DsnField[] = ['protocol', 'path']\n\n/**\n * Built-in profiles. Every one of these is overridable through `drivers`, and\n * an unknown scheme falls back to the network shape rather than refusing to\n * render — a driver nobody here anticipated is still probably a URL.\n */\nconst DRIVERS: Record = {\n postgres: {\n label: 'PostgreSQL', fields: NETWORK, defaultPort: 5432,\n warn: { sslmode: 'disable' },\n },\n postgresql: {\n label: 'PostgreSQL', fields: NETWORK, defaultPort: 5432,\n warn: { sslmode: 'disable' },\n },\n cockroachdb: { label: 'CockroachDB', fields: NETWORK, defaultPort: 26257, warn: { sslmode: 'disable' } },\n mysql: { label: 'MySQL', fields: NETWORK, defaultPort: 3306, warn: { tls: 'false' } },\n mariadb: { label: 'MariaDB', fields: NETWORK, defaultPort: 3306 },\n mongodb: { label: 'MongoDB', fields: NETWORK, defaultPort: 27017, warn: { tls: 'false', ssl: 'false' } },\n 'mongodb+srv': { label: 'MongoDB (SRV)', fields: ['protocol', 'user', 'host', 'database'] },\n redis: { label: 'Redis', fields: NETWORK, defaultPort: 6379 },\n rediss: { label: 'Redis (TLS)', fields: NETWORK, defaultPort: 6379 },\n clickhouse: { label: 'ClickHouse', fields: NETWORK, defaultPort: 8123 },\n mssql: { label: 'SQL Server', fields: NETWORK, defaultPort: 1433, warn: { encrypt: 'false' } },\n sqlserver: { label: 'SQL Server', fields: NETWORK, defaultPort: 1433, warn: { encrypt: 'false' } },\n amqp: { label: 'AMQP', fields: NETWORK, defaultPort: 5672 },\n // File-backed: no host, no user, no port. A path and nothing else.\n sqlite: { label: 'SQLite', fields: FILE, fileBased: true, labels: { path: 'File' } },\n sqlite3: { label: 'SQLite', fields: FILE, fileBased: true, labels: { path: 'File' } },\n file: { label: 'SQLite', fields: FILE, fileBased: true, labels: { path: 'File' } },\n duckdb: { label: 'DuckDB', fields: FILE, fileBased: true, labels: { path: 'File' } },\n libsql: { label: 'libSQL', fields: NETWORK },\n}\n\nconst FALLBACK: DriverProfile = { label: 'Database', fields: NETWORK }\n\nconst FIELD_LABELS: Record = {\n protocol: 'Driver',\n user: 'User',\n host: 'Host',\n port: 'Port',\n database: 'Database',\n path: 'Path',\n}\n\ntype Parsed = {\n scheme: string\n user: string\n password: string\n host: string\n port: string\n database: string\n path: string\n params: [string, string][]\n memory: boolean\n}\n\n/** `Server=x;Database=y;Password=z` — SQL Server and ODBC. */\nfunction parseKeyValue(dsn: string): Parsed | null {\n if (!/^[A-Za-z ]+\\s*=/.test(dsn.trim()) || !dsn.includes(';')) return null\n const entries = dsn\n .split(';')\n .map((pair) => pair.split('='))\n .filter((pair) => pair.length >= 2)\n .map(([key, ...rest]) => [key.trim().toLowerCase(), rest.join('=').trim()] as const)\n if (entries.length === 0) return null\n\n const get = (...keys: string[]) =>\n entries.find(([k]) => keys.includes(k))?.[1] ?? ''\n\n const server = get('server', 'data source', 'addr', 'host')\n const [host, port = ''] = server.split(/[,:]/)\n const known = new Set(['server', 'data source', 'addr', 'host', 'database', 'initial catalog', 'user id', 'uid', 'user', 'password', 'pwd'])\n\n return {\n scheme: 'sqlserver',\n user: get('user id', 'uid', 'user'),\n password: get('password', 'pwd'),\n host: host ?? '',\n port,\n database: get('database', 'initial catalog'),\n path: '',\n params: entries.filter(([k]) => !known.has(k)).map(([k, v]) => [k, v]),\n memory: false,\n }\n}\n\nfunction parseUrl(dsn: string): Parsed | null {\n try {\n const url = new URL(dsn)\n const scheme = url.protocol.replace(':', '').toLowerCase()\n // The text after the scheme, not `pathname`. `URL` normalises a relative\n // file path — `file:./data.db` becomes `/data.db` — which turns a path\n // relative to the working directory into an absolute one, and that is the\n // single most consequential detail in a file-backed DSN.\n const body = dsn.slice(dsn.indexOf(':') + 1).replace(/^\\/\\//, '').split('?')[0]\n const memory = /^:?memory:?$/.test(body)\n\n return {\n scheme,\n user: decodeURIComponent(url.username),\n password: decodeURIComponent(url.password),\n host: url.hostname,\n port: url.port,\n database: url.pathname.replace(/^\\//, ''),\n path: memory ? ':memory:' : body,\n params: [...url.searchParams.entries()],\n memory,\n }\n } catch {\n return null\n }\n}\n\nfunction ConnectionString({\n value,\n drivers,\n revealed: revealedProp,\n mask = '••••••••',\n invalidNote = 'That does not parse as a connection string. Expected a URL, or semicolon-separated key=value pairs.',\n unencodedNote = 'The password contains a character that must be percent-encoded. As written, the host will parse wrong.',\n relativePathNote = 'A relative path resolves against the working directory of whatever starts the process — not the config file.',\n memoryNote = 'An in-memory database is discarded when the process exits, and is not shared between connections.',\n revealLabel = 'Reveal password',\n hideLabel = 'Hide password',\n copyLabel = 'Copy connection string',\n fieldLabels,\n className,\n ...props\n}: Omit, 'children'> & {\n value: string\n /** Adds to or replaces the built-in driver profiles, keyed by scheme. */\n drivers?: Record\n revealed?: boolean\n mask?: string\n invalidNote?: ReactNode\n unencodedNote?: ReactNode\n /** Shown for a file-backed database given a relative path. */\n relativePathNote?: ReactNode\n memoryNote?: ReactNode\n revealLabel?: string\n hideLabel?: string\n copyLabel?: string\n /** Overrides the field headings for every driver. */\n fieldLabels?: Partial>\n}) {\n const [own, setOwn] = useState(false)\n const revealed = revealedProp ?? own\n\n const parsed = useMemo(() => parseUrl(value) ?? parseKeyValue(value), [value])\n\n const profile = useMemo(() => {\n if (!parsed) return FALLBACK\n return drivers?.[parsed.scheme] ?? DRIVERS[parsed.scheme] ?? FALLBACK\n }, [parsed, drivers])\n\n const display = useMemo(() => {\n if (!parsed || revealed || !parsed.password) return value\n return value\n .replace(encodeURIComponent(parsed.password), mask)\n .replace(parsed.password, mask)\n }, [value, parsed, revealed, mask])\n\n if (!parsed) {\n return (\n
\n

\n \n {invalidNote}\n

\n
\n )\n }\n\n const shown: Record = {\n protocol: parsed.scheme,\n user: parsed.user,\n host: parsed.host,\n port: parsed.port || (profile.defaultPort ? `${profile.defaultPort}` : ''),\n database: parsed.database,\n path: parsed.path,\n }\n\n const flagged = profile.warn\n ? parsed.params.filter(([key, val]) => profile.warn?.[key] === val)\n : []\n // A raw @ or / in the password silently reassigns the host.\n const unencoded =\n /[@/?#]/.test(parsed.password) && !value.includes(encodeURIComponent(parsed.password))\n const relative =\n profile.fileBased && !parsed.memory && Boolean(parsed.path) && !parsed.path.startsWith('/')\n\n return (\n \n
\n {display}\n {revealedProp === undefined && parsed.password && (\n setOwn((v) => !v)}\n >\n {revealed ? : }\n \n )}\n \n
\n\n
\n {profile.label}\n {parsed.memory && (\n \n in-memory\n \n )}\n
\n\n {/* Only the fields this driver actually has. */}\n
\n {profile.fields.map((field) => (\n
\n
\n {fieldLabels?.[field] ?? profile.labels?.[field] ?? FIELD_LABELS[field]}\n
\n
\n {shown[field] || —}\n {field === 'port' && !parsed.port && profile.defaultPort && (\n default\n )}\n
\n
\n ))}\n
\n\n {parsed.params.length > 0 && (\n
\n {parsed.params.map(([key, val]) => (\n \n {key}={val}\n \n ))}\n
\n )}\n\n {flagged.map(([key, val]) => (\n

\n \n {key}={val}\n {' '}\n — credentials and results travel in the clear.\n

\n ))}\n\n {unencoded && (\n

{unencodedNote}

\n )}\n {relative && (\n

{relativePathNote}

\n )}\n {parsed.memory && (\n

{memoryNote}

\n )}\n \n )\n}\n\nexport { ConnectionString, DRIVERS as connectionDrivers, parseUrl as parseDsn }\n" } ] }