fix(meilisearch): preserve proxy paths and secure REST requests

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-08-17 01:50:11 +08:00
1 parent 47337ac048
commit 50e6476cb0
8 files changed
+303 -23

No files matched your search

@@ -33,7 +33,7 @@ import { useToast } from "@/composables/useToast";
import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
import * as api from "@/lib/backend/api";
import { isTauriRuntime } from "@/lib/backend/tauriRuntime";
import { applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl";
import { applyMeilisearchBasePathToExternalConfig, applyParsedConnectionUrl, normalizeMongoConnectionString, parseConnectionUrl } from "@/lib/connection/connectionUrl";
import { MAX_CONNECT_TIMEOUT_SECS, MAX_QUERY_TIMEOUT_SECS } from "@/lib/connection/timeoutLimits";
import { buildOracleTnsConnectionString, normalizeOracleTnsAdminPath, parseOracleTnsConnectionString } from "@/lib/connection/oracleTnsConnection";
import { connectionDeepLinkServiceHydrationValue, parseConnectionDeepLink, parseServiceConnectionUrl, type ConnectionDeepLinkDraft } from "@/lib/connection/connectionDeepLink";
@@ -354,7 +354,6 @@ const elasticsearchConnectionPorts = ref<Record<ElasticsearchConnectionMode, num
direct: 9200,
kibana: 5601,
});
function resetElasticsearchProxyFields(externalConfig?: unknown) {
const mode = elasticsearchConnectionModeFromConfig(externalConfig);
elasticsearchConnectionMode.value = mode;
@@ -603,6 +602,31 @@ function externalConfigRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? { ...(value as Record<string, unknown>) } : {};
}
function meilisearchConnectionUrl(config: Pick<ConnectionConfig, "host" | "port" | "ssl" | "url_params" | "external_config">): string {
const host = config.host.trim();
if (!host) return "";
const scheme = config.ssl ? "https" : "http";
const endpointHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
const externalConfig = externalConfigRecord(config.external_config);
const storedBasePath = externalConfig.basePath ?? externalConfig.base_path;
const basePathSegments = typeof storedBasePath === "string" ? storedBasePath.trim().split("/").filter(Boolean) : [];
const basePath = basePathSegments.length ? `/${basePathSegments.join("/")}` : "";
const urlParams = config.url_params?.trim().replace(/^\?/, "") || "";
return `${scheme}://${endpointHost}:${config.port}${basePath}${urlParams ? `?${urlParams}` : ""}`;
}
function syncMeilisearchHostInput(config: Pick<ConnectionConfig, "host" | "port" | "ssl" | "url_params" | "external_config">) {
meilisearchHostInput.value = meilisearchConnectionUrl(config);
appliedMeilisearchHostInput.value = meilisearchHostInput.value;
}
function resetMeilisearchHostInput() {
meilisearchHostInput.value = "";
appliedMeilisearchHostInput.value = "";
}
function sqlServerPortExplicitFromConfig(config: Pick<ConnectionConfig, "db_type" | "external_config">): boolean {
if (config.db_type !== "sqlserver") return false;
const external = externalConfigRecord(config.external_config);
@@ -651,6 +675,8 @@ const selectedJdbcDriverPath = ref("");
const jdbcManualClasspathOpen = ref(false);
const connectionUrlInput = ref("");
const appliedConnectionUrlInput = ref("");
const meilisearchHostInput = ref("");
const appliedMeilisearchHostInput = ref("");
const oracleTnsAdminPath = ref("");
const oceanbaseSubMode = ref<"mysql" | "oracle">("mysql");
const h2ConnectionMode = ref<H2ConnectionMode>("file");
@@ -2313,13 +2339,13 @@ function applyProfile(val: string, preserveConnectionFields = false) {
form.value.db_type = profile.type;
form.value.driver_profile = val;
form.value.driver_label = isCustomCompatibleProfile() ? customDriverName.value.trim() || profile.label : profile.label;
if (profile.type !== "sqlserver") {
const preserveMeilisearchConfig = preserveConnectionFields && previousDatabaseType === "meilisearch" && profile.type === "meilisearch";
if (profile.type !== "sqlserver" && !preserveMeilisearchConfig) {
form.value.external_config = undefined;
}
if (profile.type !== "elasticsearch" || previousDatabaseType !== "elasticsearch") {
resetElasticsearchProxyFields();
}
if (!preserveConnectionFields) {
oracleTnsAdminPath.value = "";
form.value.port = profile.port;
@@ -2427,6 +2453,11 @@ function applyProfile(val: string, preserveConnectionFields = false) {
}
resetHiveKerberosFields(profile.type === "hive" || profile.type === "kyuubi" || profile.type === "impala" ? form.value : undefined);
}
if (profile.type === "meilisearch") {
syncMeilisearchHostInput(form.value);
} else {
resetMeilisearchHostInput();
}
}
function switchOceanbaseMode(mode: "mysql" | "oracle") {
@@ -2520,6 +2551,11 @@ watch(
productionProtectionEnabled.value = !!config.is_production || (config.production_databases?.length ?? 0) > 0;
connectionUrlInput.value = config.db_type === "h2" && config.connection_string ? config.connection_string : "";
appliedConnectionUrlInput.value = connectionUrlInput.value.trim();
if (config.db_type === "meilisearch") {
syncMeilisearchHostInput(config);
} else {
resetMeilisearchHostInput();
}
if (config.db_type === "mq") {
hydrateMqFields(config.external_config);
} else {
@@ -3577,6 +3613,11 @@ function applyConnectionUrlToForm(input: string): boolean {
const draft = parseConnectionDeepLink(input) ?? parseServiceConnectionUrl(input);
if (draft) {
applyConnectionDraftToForm({ ...draft, oneTime: undefined });
if (form.value.db_type === "meilisearch") {
syncMeilisearchHostInput(form.value);
} else {
resetMeilisearchHostInput();
}
resetTestState();
appliedConnectionUrlInput.value = input.trim();
return true;
@@ -3587,6 +3628,11 @@ function applyConnectionUrlToForm(input: string): boolean {
if (form.value.db_type === "victoriametrics") {
hydrateVictoriaMetricsFields(form.value.external_config);
}
if (form.value.db_type === "meilisearch") {
syncMeilisearchHostInput(form.value);
} else {
resetMeilisearchHostInput();
}
oracleTnsAdminPath.value = parseOracleTnsConnectionString(parsed.connectionString)?.tnsAdmin || "";
selectedType.value = parsed.driverProfile;
customDriverName.value = isCustomCompatibleProfile() ? parsed.driverLabel : "";
@@ -3611,21 +3657,46 @@ function hasPendingConnectionUrlInput(): boolean {
return !!url && url !== appliedConnectionUrlInput.value;
}
function hasPendingMeilisearchHostInput(): boolean {
const url = meilisearchHostInput.value.trim();
return url !== appliedMeilisearchHostInput.value;
}
function applyMeilisearchHostInput(): boolean {
try {
const input = meilisearchHostInput.value.trim();
form.value = applyParsedConnectionUrl(form.value, parseConnectionUrl(input, "meilisearch"));
appliedMeilisearchHostInput.value = input;
resetTestState();
return true;
} catch (e: any) {
toast(t("connection.parseConnectionUrlFailed", { message: e?.message || String(e) }), 5000);
return false;
}
}
function ensureConnectionHostResolvedFromUrl(): boolean {
if (!hasPendingConnectionUrlInput()) return true;
return applyConnectionUrlToForm(connectionUrlInput.value.trim());
if (hasPendingConnectionUrlInput() && !applyConnectionUrlToForm(connectionUrlInput.value.trim())) return false;
if (form.value.db_type === "meilisearch" && hasPendingMeilisearchHostInput()) return applyMeilisearchHostInput();
return true;
}
function formValueForSubmit(): Omit<ConnectionConfig, "id"> {
const url = connectionUrlInput.value.trim();
if (!url || url === appliedConnectionUrlInput.value) return form.value;
if (url && url !== appliedConnectionUrlInput.value) {
const draft = parseConnectionDeepLink(url);
if (draft) {
return applyConnectionDraftToConfig(form.value, { ...draft, oneTime: undefined });
}
const draft = parseConnectionDeepLink(url);
if (draft) {
return applyConnectionDraftToConfig(form.value, { ...draft, oneTime: undefined });
return applyParsedConnectionUrl(form.value, parseConnectionUrl(url, selectedType.value));
}
return applyParsedConnectionUrl(form.value, parseConnectionUrl(url, selectedType.value));
if (form.value.db_type === "meilisearch" && hasPendingMeilisearchHostInput()) {
return applyParsedConnectionUrl(form.value, parseConnectionUrl(meilisearchHostInput.value.trim(), "meilisearch"));
}
return form.value;
}
function applyDremioJdbcMetadata(config: LegacyConnectionConfig) {
@@ -3902,7 +3973,6 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
config.username = "";
config.password = config.password.trim();
config.database = undefined;
config.external_config = undefined;
} else if (config.db_type === "sqlserver") {
config.external_config = sqlServerPortExplicitFromConfig(config) ? { portExplicit: true } : undefined;
} else if (supportsGaussdbIdentifierQuoteStyle(config)) {
@@ -4754,6 +4824,7 @@ function resetForm() {
selectedJdbcDriverPath.value = "";
connectionUrlInput.value = "";
appliedConnectionUrlInput.value = "";
resetMeilisearchHostInput();
oracleTnsAdminPath.value = "";
dialogStep.value = "select";
dbSearchQuery.value = "";
@@ -4810,6 +4881,7 @@ function applyConnectionDraftToConfig(config: Omit<ConnectionConfig, "id">, draf
database: draft.database ?? config.database,
url_params: draft.urlParams ?? config.url_params,
ssl: draft.ssl ?? config.ssl,
external_config: draft.dbType === "meilisearch" && draft.basePath !== undefined ? applyMeilisearchBasePathToExternalConfig(config.external_config, draft.basePath) : config.external_config,
connection_string: draft.connectionString ?? config.connection_string,
oracle_connection_type: draft.oracleConnectionType ?? config.oracle_connection_type,
one_time: draft.oneTime || undefined,
@@ -6989,6 +7061,10 @@ function openExternalUrl(url: string) {
</div>
</div>
</template>
<div v-else-if="form.db_type === 'meilisearch'" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.host") }}</Label>
<Input v-model="meilisearchHostInput" class="col-span-3" :placeholder="connectionUrlPlaceholder" @input="resetTestState" />
</div>
<div v-else-if="form.db_type !== 'oracle' || form.oracle_connection_type !== 'tns'" class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ form.db_type === "elasticsearch" && elasticsearchConnectionMode === "kibana" ? t("connection.elasticsearchKibanaHost") : t("connection.host") }}</Label>
<Input v-model="form.host" class="col-span-2" />
@@ -1,9 +1,15 @@
import { describe, expect, it } from "vitest";
import { connectionProfileForScheme, parseConnectionUrl } from "@/lib/connection/connectionUrl";
import { applyParsedConnectionUrl, connectionProfileForScheme, parseConnectionUrl } from "@/lib/connection/connectionUrl";
import { parseConnectionDeepLink } from "@/lib/connection/connectionDeepLink";
import type { ConnectionConfig } from "@/types/database";
describe("Meilisearch connection URLs", () => {
it("parses the dedicated scheme with the default port and API key", () => {
expect(parseConnectionUrl("meilisearch://:secret@search.example.com")).toMatchObject({
it("rejects an empty host URL", () => {
expect(() => parseConnectionUrl("", "meilisearch")).toThrow("Connection URL is empty");
});
it("parses the dedicated scheme with the default port, API key, and proxy path", () => {
expect(parseConnectionUrl("meilisearch://:secret@search.example.com/gateway/meili")).toMatchObject({
dbType: "meilisearch",
driverProfile: "meilisearch",
driverLabel: "Meilisearch",
@@ -12,6 +18,7 @@ describe("Meilisearch connection URLs", () => {
username: "",
password: "secret",
ssl: false,
basePath: "/gateway/meili",
});
});
@@ -25,6 +32,76 @@ describe("Meilisearch connection URLs", () => {
});
});
it.each([
["http://search.example.com/gateway/meili/", 80, false],
["https://search.example.com/gateway/meili", 443, true],
["https://search.example.com:8443/gateway/meili", 8443, true],
])("parses proxy URL %s", (url, port, ssl) => {
expect(parseConnectionUrl(url, "meilisearch")).toMatchObject({
dbType: "meilisearch",
host: "search.example.com",
port,
ssl,
database: undefined,
basePath: "/gateway/meili",
});
});
it("stores the proxy path in canonical external config without dropping other fields", () => {
const parsed = parseConnectionUrl("http://search.example.com/gateway/meili", "meilisearch");
const config = applyParsedConnectionUrl(
{
db_type: "meilisearch",
driver_profile: "meilisearch",
driver_label: "Meilisearch",
host: "localhost",
port: 7700,
username: "",
password: "",
url_params: "",
ssl: false,
external_config: { base_path: "/legacy", custom: true },
} as Omit<ConnectionConfig, "id">,
parsed,
);
expect(config.external_config).toEqual({ basePath: "/gateway/meili", custom: true });
});
it("clears stale proxy paths when the parsed URL uses the root path", () => {
const parsed = parseConnectionUrl("http://search.example.com/", "meilisearch");
const config = applyParsedConnectionUrl(
{
db_type: "meilisearch",
driver_profile: "meilisearch",
driver_label: "Meilisearch",
host: "localhost",
port: 7700,
username: "",
password: "",
url_params: "",
ssl: false,
external_config: { basePath: "/old", base_path: "/legacy", custom: true },
} as Omit<ConnectionConfig, "id">,
parsed,
);
expect(parsed.basePath).toBe("");
expect(config.external_config).toEqual({ custom: true });
});
it("preserves the proxy path when a Meilisearch URL arrives through a deep link", () => {
const link = new URL("dbx://connection/new");
link.searchParams.set("type", "meilisearch");
link.searchParams.set("url", "https://search.example.com/gateway/meili?insecure=true");
expect(parseConnectionDeepLink(link.toString())).toMatchObject({
dbType: "meilisearch",
basePath: "/gateway/meili",
urlParams: "insecure=true",
});
});
it("exposes the Meilisearch scheme profile", () => {
expect(connectionProfileForScheme("meilisearch")).toEqual({
type: "meilisearch",
@@ -23,6 +23,7 @@ export interface ConnectionDeepLinkDraft {
password?: string;
database?: string;
urlParams?: string;
basePath?: string;
ssl?: boolean;
connectionString?: string;
oracleConnectionType?: "service_name" | "sid";
@@ -188,6 +189,7 @@ function draftFromConnectionUrl(value: string, preferredProfile?: string): Conne
password: parsed.password,
database: parsed.database,
urlParams: parsed.urlParams,
basePath: parsed.basePath,
ssl: parsed.ssl,
connectionString: parsed.connectionString,
oracleConnectionType: parsed.oracleConnectionType,
@@ -208,13 +208,15 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string {
case "elasticsearch":
case "easysearch":
case "meilisearch":
case "qdrant":
case "milvus":
case "weaviate":
case "chromadb":
return "http://user:password@host:port";
case "meilisearch":
return "http://host:port/base/path";
case "dameng":
return "dm://user:password@host:port";
@@ -19,6 +19,7 @@ export interface ParsedConnectionUrl {
useMongoUrl?: boolean;
portExplicit?: boolean;
apiPath?: string;
basePath?: string;
}
export type ConnectionProfile = {
@@ -651,20 +652,24 @@ export function parseConnectionUrl(value: string, preferredProfile?: string): Pa
};
}
const isMeilisearch = profile.type === "meilisearch";
const defaultPort = isMeilisearch && scheme === "http" ? 80 : isMeilisearch && scheme === "https" ? 443 : profile.defaultPort;
return {
...(name ? { name } : {}),
dbType: profile.type,
driverProfile: profile.profile,
driverLabel: profile.label,
host: parsed.hostname,
port: parsed.port ? Number(parsed.port) : profile.defaultPort,
port: parsed.port ? Number(parsed.port) : defaultPort,
...(profile.type === "sqlserver" && parsed.port ? { portExplicit: true } : {}),
username: mysqlCredentials?.username ?? decodeUrlPart(parsed.username),
password: mysqlCredentials?.password ?? decodeUrlPart(parsed.password),
database: profile.type === "victoriametrics" ? "metrics" : databaseFromPath(parsed.pathname),
database: profile.type === "victoriametrics" ? "metrics" : isMeilisearch ? undefined : databaseFromPath(parsed.pathname),
urlParams: effectiveUrlParams,
ssl: scheme === "rediss" || scheme === "https" || urlParamsRequireTls(profile.type, effectiveUrlParams) || (profile.type === "mysql" && isTidbCloudHost(parsed.hostname)),
...(profile.type === "victoriametrics" ? { apiPath: parsed.pathname.replace(/\/+$/, "") } : {}),
...(isMeilisearch ? { basePath: parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "") } : {}),
};
}
@@ -711,12 +716,26 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === "object" && !Array.isArray(value);
}
export function applyMeilisearchBasePathToExternalConfig(existing: unknown, basePath: string | undefined): unknown {
const next = isRecord(existing) ? { ...existing } : {};
delete next.base_path;
if (basePath) {
next.basePath = basePath;
} else {
delete next.basePath;
}
return Object.keys(next).length > 0 ? next : undefined;
}
function parsedExternalConfig(existing: unknown, parsed: ParsedConnectionUrl): unknown {
if (parsed.dbType === "victoriametrics") {
const next = isRecord(existing) ? { ...existing } : {};
next.apiPath = parsed.apiPath || "/prometheus";
return next;
}
if (parsed.dbType === "meilisearch") {
return applyMeilisearchBasePathToExternalConfig(existing, parsed.basePath);
}
if (parsed.dbType !== "sqlserver") return existing;
const next = isRecord(existing) ? { ...existing } : {};