feat(connection): support Consul and Nacos service deep-link prefill
This commit is contained in:
13 files changed
+476
-26
No files matched your search
@@ -33,7 +33,7 @@ import * as api from "@/lib/backend/api";
|
||||
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
|
||||
import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl";
|
||||
import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "@/lib/connection/oracleTnsConnection";
|
||||
import { parseConnectionDeepLink, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
|
||||
import { connectionDeepLinkServiceHydrationValue, parseConnectionDeepLink, parseServiceConnectionUrl, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
|
||||
import { connectionUrlPlaceholder as getUrlPlaceholder } from "@/lib/connection/connectionPresentation";
|
||||
import { parseGaussdbHosts, serializeGaussdbHosts, type GaussdbHostEntry } from "@/lib/connection/gaussdbHosts";
|
||||
import { h2ConnectionModeForConfig, h2FileJdbcUrlWithPath, h2FilePathFromJdbcUrl, isH2SplitJdbcUrl, type H2ConnectionMode } from "@/lib/database/h2Connection";
|
||||
@@ -3502,7 +3502,7 @@ function clearEditedConnectionErrorAfterSuccessfulTest() {
|
||||
|
||||
function applyConnectionUrlToForm(input: string): boolean {
|
||||
try {
|
||||
const draft = parseConnectionDeepLink(input);
|
||||
const draft = parseConnectionDeepLink(input) ?? parseServiceConnectionUrl(input);
|
||||
if (draft) {
|
||||
applyConnectionDraftToForm({ ...draft, oneTime: undefined });
|
||||
resetTestState();
|
||||
@@ -4687,7 +4687,24 @@ function resetForm() {
|
||||
const submittedOneTimePrefillKey = ref<string | null>(null);
|
||||
|
||||
function oneTimePrefillKey(draft: ConnectionDeepLinkDraft) {
|
||||
return JSON.stringify([draft.name, draft.dbType, draft.driverProfile, draft.driverLabel, draft.host, draft.port, draft.portExplicit, draft.username, draft.password, draft.database, draft.urlParams, draft.ssl, draft.connectionString, draft.oracleConnectionType, draft.useMongoUrl]);
|
||||
return JSON.stringify([
|
||||
draft.name,
|
||||
draft.dbType,
|
||||
draft.driverProfile,
|
||||
draft.driverLabel,
|
||||
draft.host,
|
||||
draft.port,
|
||||
draft.portExplicit,
|
||||
draft.username,
|
||||
draft.password,
|
||||
draft.database,
|
||||
draft.urlParams,
|
||||
draft.ssl,
|
||||
draft.connectionString,
|
||||
draft.oracleConnectionType,
|
||||
draft.useMongoUrl,
|
||||
draft.serviceConfig,
|
||||
]);
|
||||
}
|
||||
|
||||
function submitOneTimePrefill(draft: ConnectionDeepLinkDraft) {
|
||||
@@ -4722,6 +4739,11 @@ function applyConnectionDraftToConfig(config: Omit<ConnectionConfig, "id">, draf
|
||||
function applyConnectionDraftToForm(draft: ConnectionDeepLinkDraft) {
|
||||
applyProfile(draft.driverProfile);
|
||||
form.value = applyConnectionDraftToConfig(form.value, draft);
|
||||
if (draft.serviceConfig?.kind === "consul") {
|
||||
hydrateConsulFields(connectionDeepLinkServiceHydrationValue(draft.serviceConfig));
|
||||
} else if (draft.serviceConfig?.kind === "nacos") {
|
||||
hydrateNacosFields(connectionDeepLinkServiceHydrationValue(draft.serviceConfig));
|
||||
}
|
||||
oracleTnsAdminPath.value = parseOracleTnsConnectionString(form.value.connection_string)?.tnsAdmin || "";
|
||||
selectedType.value = draft.driverProfile;
|
||||
if (form.value.db_type === "h2") {
|
||||
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { connectionDeepLinkServiceHydrationValue, parseConnectionDeepLink, parseServiceConnectionUrl } from "@/lib/connection/connectionDeepLink";
|
||||
import { connectionProfileForScheme } from "@/lib/connection/connectionUrl";
|
||||
|
||||
describe("service connection deep links", () => {
|
||||
it("registers Consul and every Nacos spelling against canonical profiles", () => {
|
||||
expect(connectionProfileForScheme("consul")).toMatchObject({ type: "consul", profile: "consul", defaultPort: 8500 });
|
||||
for (const type of ["nacos-v2", "nacos-v3", "r-nacos", "nacos", "rnacos"]) {
|
||||
expect(connectionProfileForScheme(type)).toMatchObject({ type: "nacos", profile: "nacos", defaultPort: 8848 });
|
||||
}
|
||||
});
|
||||
|
||||
it("builds a Consul endpoint and keeps the ACL token in the password field", () => {
|
||||
const draft = parseConnectionDeepLink("dbx://connection/new?type=consul&host=consul.internal&password=acl-token");
|
||||
expect(draft).toMatchObject({
|
||||
dbType: "consul",
|
||||
driverProfile: "consul",
|
||||
host: "consul.internal",
|
||||
port: 8500,
|
||||
password: "acl-token",
|
||||
serviceConfig: { kind: "consul", serverAddr: "http://consul.internal:8500" },
|
||||
});
|
||||
});
|
||||
|
||||
it("maps Nacos versions and aliases to explicit service profiles", () => {
|
||||
const cases = [
|
||||
["nacos", "v2"],
|
||||
["nacos-v2", "v2"],
|
||||
["nacos-v3", "v3"],
|
||||
["rnacos", "rnacos"],
|
||||
["r-nacos", "rnacos"],
|
||||
] as const;
|
||||
for (const [type, profile] of cases) {
|
||||
const draft = parseConnectionDeepLink(`dbx://connection/new?type=${type}&host=127.0.0.1&user=nacos&password=secret`);
|
||||
expect(draft?.dbType).toBe("nacos");
|
||||
expect(draft?.driverProfile).toBe("nacos");
|
||||
expect(draft?.serviceConfig).toMatchObject({
|
||||
kind: "nacos",
|
||||
profile,
|
||||
serverAddr: "http://127.0.0.1:8848",
|
||||
auth: { kind: "usernamePassword", username: "nacos", password: "secret" },
|
||||
});
|
||||
if (profile === "rnacos") expect(draft?.serviceConfig).toMatchObject({ rnacosHistoryEnabled: false });
|
||||
}
|
||||
});
|
||||
|
||||
it("preserves URL paths and lets top-level host, port and SSL fields override them", () => {
|
||||
const link = new URL("dbx://connection/new");
|
||||
link.searchParams.set("type", "nacos-v3");
|
||||
link.searchParams.set("url", "https://nacos:secret@old.example:9443/proxy/nacos");
|
||||
link.searchParams.set("host", "[2001:db8::8]");
|
||||
link.searchParams.set("port", "18848");
|
||||
link.searchParams.set("ssl", "false");
|
||||
link.searchParams.set("user", "admin");
|
||||
link.searchParams.set("password", "override");
|
||||
|
||||
const draft = parseConnectionDeepLink(link.toString());
|
||||
expect(draft).toMatchObject({ host: "[2001:db8::8]", port: 18848, ssl: false, username: "admin", password: "override" });
|
||||
expect(draft?.serviceConfig).toEqual({
|
||||
kind: "nacos",
|
||||
profile: "v3",
|
||||
serverAddr: "http://[2001:db8::8]:18848/proxy/nacos",
|
||||
auth: { kind: "usernamePassword", username: "admin", password: "override" },
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts service-specific URL schemes and unwrapped IPv6 hosts", () => {
|
||||
const link = new URL("dbx://connection/new");
|
||||
link.searchParams.set("type", "nacos-v3");
|
||||
link.searchParams.set("url", "nacos-v3://nacos:secret@old.example:8848/proxy/nacos");
|
||||
link.searchParams.set("host", "2001:db8::9");
|
||||
const draft = parseConnectionDeepLink(link.toString());
|
||||
expect(draft?.serviceConfig).toMatchObject({
|
||||
kind: "nacos",
|
||||
profile: "v3",
|
||||
serverAddr: "http://[2001:db8::9]:8848/proxy/nacos",
|
||||
});
|
||||
|
||||
const inferred = new URL("dbx://connection/new");
|
||||
inferred.searchParams.set("url", "consul://127.0.0.1:8500/proxy");
|
||||
expect(parseConnectionDeepLink(inferred.toString())?.serviceConfig).toEqual({
|
||||
kind: "consul",
|
||||
serverAddr: "http://127.0.0.1:8500/proxy",
|
||||
});
|
||||
});
|
||||
|
||||
it("converts raw service URLs into drafts with specialized configuration", () => {
|
||||
expect(parseServiceConnectionUrl("consul://remote.example:18500/prefix")).toMatchObject({
|
||||
dbType: "consul",
|
||||
host: "remote.example",
|
||||
port: 18500,
|
||||
serviceConfig: { kind: "consul", serverAddr: "http://remote.example:18500/prefix" },
|
||||
});
|
||||
expect(parseServiceConnectionUrl("nacos-v3://nacos:secret@remote.example:18848/nacos")).toMatchObject({
|
||||
dbType: "nacos",
|
||||
username: "nacos",
|
||||
password: "secret",
|
||||
serviceConfig: {
|
||||
kind: "nacos",
|
||||
profile: "v3",
|
||||
serverAddr: "http://remote.example:18848/nacos",
|
||||
auth: { kind: "usernamePassword", username: "nacos", password: "secret" },
|
||||
},
|
||||
});
|
||||
expect(parseServiceConnectionUrl("mysql://root:secret@127.0.0.1:3306/app")).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts missing v and v=1, but rejects unsupported versions", () => {
|
||||
expect(parseConnectionDeepLink("dbx://connection/new?type=consul&host=127.0.0.1")).not.toBeNull();
|
||||
expect(parseConnectionDeepLink("dbx://connection/new?v=1&type=consul&host=127.0.0.1")).not.toBeNull();
|
||||
expect(() => parseConnectionDeepLink("dbx://connection/new?v=2&type=consul&host=127.0.0.1")).toThrow(/Unsupported connection deep-link version/);
|
||||
});
|
||||
|
||||
it("parses explicit false values and rejects invalid booleans", () => {
|
||||
expect(parseConnectionDeepLink("dbx://connection/new?type=consul&ssl=false&one_time=false")).toMatchObject({
|
||||
ssl: false,
|
||||
oneTime: false,
|
||||
serviceConfig: { kind: "consul", serverAddr: "http://127.0.0.1:8500" },
|
||||
});
|
||||
expect(() => parseConnectionDeepLink("dbx://connection/new?type=consul&ssl=maybe")).toThrow(/Invalid boolean value/);
|
||||
expect(() => parseConnectionDeepLink("dbx://connection/new?type=consul&one_time=2")).toThrow(/Invalid boolean value/);
|
||||
});
|
||||
|
||||
it("maps service drafts to the exact specialized form hydration values", () => {
|
||||
expect(connectionDeepLinkServiceHydrationValue({ kind: "consul", serverAddr: "https://consul.example/prefix" })).toEqual({
|
||||
serverAddr: "https://consul.example/prefix",
|
||||
});
|
||||
expect(
|
||||
connectionDeepLinkServiceHydrationValue({
|
||||
kind: "nacos",
|
||||
profile: "v3",
|
||||
serverAddr: "https://nacos.example/prefix",
|
||||
auth: { kind: "usernamePassword", username: "nacos", password: "secret" },
|
||||
}),
|
||||
).toEqual({
|
||||
implementation: "nacos",
|
||||
versionMode: "v3",
|
||||
serverAddr: "https://nacos.example/prefix",
|
||||
auth: { kind: "usernamePassword", username: "nacos", password: "secret" },
|
||||
});
|
||||
expect(
|
||||
connectionDeepLinkServiceHydrationValue({
|
||||
kind: "nacos",
|
||||
profile: "rnacos",
|
||||
serverAddr: "http://rnacos.example/nacos",
|
||||
auth: { kind: "none" },
|
||||
rnacosHistoryEnabled: false,
|
||||
}),
|
||||
).toEqual({
|
||||
implementation: "rnacos",
|
||||
serverAddr: "http://rnacos.example/nacos",
|
||||
rnacosHistoryEnabled: false,
|
||||
auth: { kind: "none" },
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects invalid ports, targets and incomplete Nacos credentials", () => {
|
||||
expect(() => parseConnectionDeepLink("dbx://connection/new?type=consul&port=0")).toThrow(/Invalid connection port/);
|
||||
expect(() => parseConnectionDeepLink("dbx://connection/new?type=consul&port=70000")).toThrow(/Invalid connection port/);
|
||||
expect(() => parseConnectionDeepLink("dbx://connection/new?type=nacos-v2&password=secret")).toThrow(/username is required/);
|
||||
expect(() => parseConnectionDeepLink("dbx://connection/new?type=consul&host=bad%2Fhost")).toThrow(/Invalid service connection host/);
|
||||
expect(parseConnectionDeepLink("dbx://connection/edit?type=consul")).toBeNull();
|
||||
});
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const source = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("ConnectionDialog service deep-link hydration", () => {
|
||||
it("hydrates specialized refs after generic fields and before one-time submit", () => {
|
||||
const applyStart = source.indexOf("function applyConnectionDraftToForm");
|
||||
const applyEnd = source.indexOf("function applyConnectionPrefill", applyStart);
|
||||
const applyBody = source.slice(applyStart, applyEnd);
|
||||
expect(applyBody.indexOf("applyConnectionDraftToConfig")).toBeGreaterThanOrEqual(0);
|
||||
expect(applyBody.indexOf("hydrateConsulFields")).toBeGreaterThan(applyBody.indexOf("applyConnectionDraftToConfig"));
|
||||
expect(applyBody.indexOf("hydrateNacosFields")).toBeGreaterThan(applyBody.indexOf("applyConnectionDraftToConfig"));
|
||||
|
||||
const prefillStart = source.indexOf("function applyConnectionPrefill");
|
||||
const prefillEnd = source.indexOf("watch(\n open", prefillStart);
|
||||
const prefillBody = source.slice(prefillStart, prefillEnd);
|
||||
expect(prefillBody.indexOf("applyConnectionDraftToForm")).toBeLessThan(prefillBody.indexOf("submitOneTimePrefill"));
|
||||
});
|
||||
|
||||
it("includes service configuration in one-time submission deduplication", () => {
|
||||
const keyStart = source.indexOf("function oneTimePrefillKey");
|
||||
const keyEnd = source.indexOf("function submitOneTimePrefill", keyStart);
|
||||
expect(source.slice(keyStart, keyEnd)).toContain("draft.serviceConfig");
|
||||
});
|
||||
|
||||
it("hydrates raw service URLs through the deep-link draft path", () => {
|
||||
const applyStart = source.indexOf("function applyConnectionUrlToForm");
|
||||
const applyEnd = source.indexOf("function hasPendingConnectionUrlInput", applyStart);
|
||||
const applyBody = source.slice(applyStart, applyEnd);
|
||||
expect(applyBody).toContain("parseConnectionDeepLink(input) ?? parseServiceConnectionUrl(input)");
|
||||
expect(applyBody.indexOf("applyConnectionDraftToForm")).toBeLessThan(applyBody.indexOf("parseConnectionUrl(input"));
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,16 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
import { connectionProfileForScheme, parseConnectionUrl } from "@/lib/connection/connectionUrl";
|
||||
|
||||
export type ConnectionDeepLinkServiceConfig =
|
||||
| { kind: "consul"; serverAddr: string }
|
||||
| {
|
||||
kind: "nacos";
|
||||
profile: "v2" | "v3" | "rnacos";
|
||||
serverAddr: string;
|
||||
auth: { kind: "none" } | { kind: "usernamePassword"; username: string; password: string };
|
||||
rnacosHistoryEnabled?: false;
|
||||
};
|
||||
|
||||
export interface ConnectionDeepLinkDraft {
|
||||
name?: string;
|
||||
dbType: DatabaseType;
|
||||
@@ -18,6 +28,25 @@ export interface ConnectionDeepLinkDraft {
|
||||
oracleConnectionType?: "service_name" | "sid";
|
||||
useMongoUrl?: boolean;
|
||||
oneTime?: boolean;
|
||||
serviceConfig?: ConnectionDeepLinkServiceConfig;
|
||||
}
|
||||
|
||||
export function connectionDeepLinkServiceHydrationValue(config: ConnectionDeepLinkServiceConfig): Record<string, unknown> {
|
||||
if (config.kind === "consul") return { serverAddr: config.serverAddr };
|
||||
if (config.profile === "rnacos") {
|
||||
return {
|
||||
implementation: "rnacos",
|
||||
serverAddr: config.serverAddr,
|
||||
rnacosHistoryEnabled: false,
|
||||
auth: config.auth,
|
||||
};
|
||||
}
|
||||
return {
|
||||
implementation: "nacos",
|
||||
versionMode: config.profile,
|
||||
serverAddr: config.serverAddr,
|
||||
auth: config.auth,
|
||||
};
|
||||
}
|
||||
|
||||
const CONNECTION_DEEP_LINK_TARGET = "connection/new";
|
||||
@@ -34,16 +63,115 @@ function optionalParam(params: URLSearchParams, ...keys: string[]): string | und
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function optionalNumberParam(params: URLSearchParams, ...keys: string[]): number | undefined {
|
||||
function optionalPortParam(params: URLSearchParams, ...keys: string[]): number | undefined {
|
||||
const value = optionalParam(params, ...keys);
|
||||
if (!value) return undefined;
|
||||
const numberValue = Number(value);
|
||||
return Number.isFinite(numberValue) && numberValue > 0 ? numberValue : undefined;
|
||||
if (!Number.isInteger(numberValue) || numberValue < 1 || numberValue > 65535) {
|
||||
throw new Error(`Invalid connection port: ${value}`);
|
||||
}
|
||||
return numberValue;
|
||||
}
|
||||
|
||||
function optionalBooleanParam(params: URLSearchParams, ...keys: string[]): boolean {
|
||||
function optionalBooleanParam(params: URLSearchParams, ...keys: string[]): boolean | undefined {
|
||||
const value = optionalParam(params, ...keys)?.toLowerCase();
|
||||
return value === "true" || value === "1" || value === "yes" || value === "on";
|
||||
if (value === undefined) return undefined;
|
||||
if (["true", "1", "yes", "on"].includes(value)) return true;
|
||||
if (["false", "0", "no", "off"].includes(value)) return false;
|
||||
throw new Error(`Invalid boolean value: ${value}`);
|
||||
}
|
||||
|
||||
type ServiceType = "consul" | "nacos-v2" | "nacos-v3" | "r-nacos";
|
||||
|
||||
function normalizeServiceType(value?: string): ServiceType | undefined {
|
||||
switch (value?.trim().toLowerCase()) {
|
||||
case "consul":
|
||||
return "consul";
|
||||
case "nacos":
|
||||
case "nacos-v2":
|
||||
return "nacos-v2";
|
||||
case "nacos-v3":
|
||||
return "nacos-v3";
|
||||
case "rnacos":
|
||||
case "r-nacos":
|
||||
return "r-nacos";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function validateProtocolVersion(params: URLSearchParams) {
|
||||
const version = optionalParam(params, "v");
|
||||
if (version !== undefined && version !== "1") {
|
||||
throw new Error(`Unsupported connection deep-link version: ${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validatedServiceHost(value: string): string {
|
||||
const rawHost = value.trim();
|
||||
if (!rawHost) throw new Error("Service connection host is required");
|
||||
const unwrappedHost = rawHost.startsWith("[") && rawHost.endsWith("]") ? rawHost.slice(1, -1) : rawHost;
|
||||
const authorityHost = unwrappedHost.includes(":") ? `[${unwrappedHost}]` : unwrappedHost;
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(`http://${authorityHost}:1`);
|
||||
} catch {
|
||||
throw new Error(`Invalid service connection host: ${value}`);
|
||||
}
|
||||
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.port !== "1") {
|
||||
throw new Error(`Invalid service connection host: ${value}`);
|
||||
}
|
||||
return parsed.hostname;
|
||||
}
|
||||
|
||||
function serviceEndpoint(rawConnectionUrl: string | undefined, serviceType: ServiceType, host: string | undefined, port: number, ssl: boolean): string {
|
||||
let endpoint: URL;
|
||||
if (rawConnectionUrl) {
|
||||
try {
|
||||
endpoint = new URL(rawConnectionUrl);
|
||||
} catch {
|
||||
throw new Error("Invalid service connection URL");
|
||||
}
|
||||
const sourceScheme = endpoint.protocol.replace(/:$/, "").toLowerCase();
|
||||
const serviceScheme = normalizeServiceType(sourceScheme);
|
||||
if (endpoint.protocol !== "http:" && endpoint.protocol !== "https:" && serviceScheme !== serviceType) {
|
||||
throw new Error(`Unsupported service connection URL scheme: ${sourceScheme}`);
|
||||
}
|
||||
if (endpoint.search || endpoint.hash) throw new Error("Service connection URL must not contain query parameters or a fragment");
|
||||
if (serviceScheme) {
|
||||
const pathname = endpoint.pathname;
|
||||
const hostWithPort = endpoint.host;
|
||||
if (!hostWithPort) throw new Error("Service connection URL must include a host");
|
||||
endpoint = new URL(`http://${hostWithPort}`);
|
||||
endpoint.pathname = pathname;
|
||||
}
|
||||
} else {
|
||||
const endpointHost = validatedServiceHost(host || "127.0.0.1");
|
||||
endpoint = new URL(`${ssl ? "https" : "http"}://${endpointHost}:${port}`);
|
||||
}
|
||||
|
||||
endpoint.username = "";
|
||||
endpoint.password = "";
|
||||
endpoint.protocol = ssl ? "https:" : "http:";
|
||||
if (host) endpoint.hostname = validatedServiceHost(host);
|
||||
endpoint.port = String(port);
|
||||
return endpoint.toString().replace(/\/$/, "");
|
||||
}
|
||||
|
||||
function serviceConfigForDraft(serviceType: ServiceType | undefined, rawConnectionUrl: string | undefined, host: string | undefined, port: number | undefined, ssl: boolean | undefined, username: string | undefined, password: string | undefined): ConnectionDeepLinkServiceConfig | undefined {
|
||||
if (!serviceType) return undefined;
|
||||
const defaultPort = serviceType === "consul" ? 8500 : 8848;
|
||||
const serverAddr = serviceEndpoint(rawConnectionUrl, serviceType, host, port ?? defaultPort, ssl ?? false);
|
||||
if (serviceType === "consul") return { kind: "consul", serverAddr };
|
||||
if (password && !username) throw new Error("Nacos username is required when a password is provided");
|
||||
const auth = username ? { kind: "usernamePassword" as const, username, password: password || "" } : { kind: "none" as const };
|
||||
return {
|
||||
kind: "nacos",
|
||||
profile: serviceType === "nacos-v3" ? "v3" : serviceType === "r-nacos" ? "rnacos" : "v2",
|
||||
serverAddr,
|
||||
auth,
|
||||
...(serviceType === "r-nacos" ? { rnacosHistoryEnabled: false as const } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function draftFromConnectionUrl(value: string, preferredProfile?: string): ConnectionDeepLinkDraft {
|
||||
@@ -79,8 +207,17 @@ export function parseConnectionDeepLink(value: string): ConnectionDeepLinkDraft
|
||||
if (normalizePath(url) !== CONNECTION_DEEP_LINK_TARGET) return null;
|
||||
|
||||
const params = url.searchParams;
|
||||
const preferredProfile = optionalParam(params, "type");
|
||||
validateProtocolVersion(params);
|
||||
const preferredProfile = optionalParam(params, "type")?.toLowerCase();
|
||||
const rawConnectionUrl = optionalParam(params, "url");
|
||||
let serviceType = normalizeServiceType(preferredProfile);
|
||||
if (!serviceType && rawConnectionUrl) {
|
||||
try {
|
||||
serviceType = normalizeServiceType(new URL(rawConnectionUrl).protocol.replace(/:$/, ""));
|
||||
} catch {
|
||||
// parseConnectionUrl below reports the canonical invalid URL error.
|
||||
}
|
||||
}
|
||||
const draft: ConnectionDeepLinkDraft = rawConnectionUrl
|
||||
? draftFromConnectionUrl(rawConnectionUrl, preferredProfile)
|
||||
: (() => {
|
||||
@@ -96,19 +233,44 @@ export function parseConnectionDeepLink(value: string): ConnectionDeepLinkDraft
|
||||
})();
|
||||
|
||||
const oneTime = optionalBooleanParam(params, "one_time");
|
||||
const explicitPort = optionalNumberParam(params, "port");
|
||||
const explicitPort = optionalPortParam(params, "port");
|
||||
const explicitSsl = optionalBooleanParam(params, "ssl");
|
||||
const host = optionalParam(params, "host") ?? draft.host;
|
||||
const port = explicitPort ?? draft.port;
|
||||
const username = optionalParam(params, "user") ?? draft.username;
|
||||
const password = optionalParam(params, "password") ?? draft.password;
|
||||
const ssl = explicitSsl ?? draft.ssl;
|
||||
const serviceConfig = serviceConfigForDraft(serviceType, rawConnectionUrl, host, port, ssl, username, password);
|
||||
|
||||
return {
|
||||
...draft,
|
||||
name: optionalParam(params, "name") ?? draft.name,
|
||||
host: optionalParam(params, "host") ?? draft.host,
|
||||
port: explicitPort ?? draft.port,
|
||||
host,
|
||||
port,
|
||||
...((explicitPort !== undefined && draft.dbType === "sqlserver") || draft.portExplicit ? { portExplicit: true } : {}),
|
||||
username: optionalParam(params, "user") ?? draft.username,
|
||||
password: optionalParam(params, "password") ?? draft.password,
|
||||
username,
|
||||
password,
|
||||
database: optionalParam(params, "database") ?? draft.database,
|
||||
urlParams: optionalParam(params, "url_params") ?? draft.urlParams,
|
||||
ssl: optionalBooleanParam(params, "ssl") ? true : draft.ssl,
|
||||
...(oneTime ? { oneTime: true } : {}),
|
||||
ssl,
|
||||
...(oneTime !== undefined ? { oneTime } : {}),
|
||||
...(serviceConfig ? { serviceConfig } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseServiceConnectionUrl(value: string): ConnectionDeepLinkDraft | null {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serviceType = normalizeServiceType(url.protocol.replace(/:$/, ""));
|
||||
if (!serviceType) return null;
|
||||
|
||||
const deepLink = new URL("dbx://connection/new");
|
||||
deepLink.searchParams.set("type", serviceType);
|
||||
deepLink.searchParams.set("url", value);
|
||||
return parseConnectionDeepLink(deepLink.toString());
|
||||
}
|
||||
@@ -38,6 +38,12 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
|
||||
redis: { type: "redis", profile: "redis", label: "Redis", defaultPort: 6379 },
|
||||
rediss: { type: "redis", profile: "redis", label: "Redis", defaultPort: 6379 },
|
||||
etcd: { type: "etcd", profile: "etcd", label: "etcd", defaultPort: 2379 },
|
||||
consul: { type: "consul", profile: "consul", label: "Consul", defaultPort: 8500 },
|
||||
"nacos-v2": { type: "nacos", profile: "nacos", label: "Nacos", defaultPort: 8848 },
|
||||
"nacos-v3": { type: "nacos", profile: "nacos", label: "Nacos", defaultPort: 8848 },
|
||||
"r-nacos": { type: "nacos", profile: "nacos", label: "r-nacos", defaultPort: 8848 },
|
||||
nacos: { type: "nacos", profile: "nacos", label: "Nacos", defaultPort: 8848 },
|
||||
rnacos: { type: "nacos", profile: "nacos", label: "r-nacos", defaultPort: 8848 },
|
||||
zookeeper: { type: "zookeeper", profile: "zookeeper", label: "Apache ZooKeeper", defaultPort: 2181 },
|
||||
mongodb: { type: "mongodb", profile: "mongodb", label: "MongoDB", defaultPort: 27017 },
|
||||
"mongodb+srv": { type: "mongodb", profile: "mongodb", label: "MongoDB", defaultPort: 27017 },
|
||||
@@ -81,6 +87,12 @@ const HTTP_SELECTED_PROFILES: Record<string, ConnectionProfile> = {
|
||||
weaviate: SCHEME_PROFILES.weaviate,
|
||||
chromadb: SCHEME_PROFILES.chromadb,
|
||||
victoriametrics: SCHEME_PROFILES.victoriametrics,
|
||||
consul: SCHEME_PROFILES.consul,
|
||||
"nacos-v2": SCHEME_PROFILES["nacos-v2"],
|
||||
"nacos-v3": SCHEME_PROFILES["nacos-v3"],
|
||||
"r-nacos": SCHEME_PROFILES["r-nacos"],
|
||||
nacos: SCHEME_PROFILES.nacos,
|
||||
rnacos: SCHEME_PROFILES.rnacos,
|
||||
};
|
||||
|
||||
function decodeUrlPart(value: string): string {
|
||||
@@ -306,14 +318,16 @@ function isTidbCloudHost(host: string): boolean {
|
||||
}
|
||||
|
||||
export function connectionProfileForScheme(scheme: string, preferredProfile?: string): ConnectionProfile | undefined {
|
||||
if ((scheme === "http" || scheme === "https") && preferredProfile) {
|
||||
return HTTP_SELECTED_PROFILES[preferredProfile];
|
||||
const normalizedScheme = scheme.trim().toLowerCase();
|
||||
const normalizedPreferredProfile = preferredProfile?.trim().toLowerCase();
|
||||
if ((normalizedScheme === "http" || normalizedScheme === "https") && normalizedPreferredProfile) {
|
||||
return HTTP_SELECTED_PROFILES[normalizedPreferredProfile];
|
||||
}
|
||||
// Cloudberry uses PostgreSQL URLs, so keep the selected product profile when parsing a pasted URL.
|
||||
if ((scheme === "postgres" || scheme === "postgresql") && preferredProfile === "cloudberry") {
|
||||
if ((normalizedScheme === "postgres" || normalizedScheme === "postgresql") && normalizedPreferredProfile === "cloudberry") {
|
||||
return SCHEME_PROFILES.cloudberry;
|
||||
}
|
||||
return SCHEME_PROFILES[scheme];
|
||||
return SCHEME_PROFILES[normalizedScheme];
|
||||
}
|
||||
|
||||
function parseJdbcSqlServerUrl(source: string): ParsedConnectionUrl | null {
|
||||
|
||||
Reference in new issue
Block a user