fix(meilisearch): preserve proxy paths and secure REST requests
This commit is contained in:
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" />
|
||||
|
||||
+80
-3
@@ -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 } : {};
|
||||
|
||||
Reference in new issue
Block a user