fix(elasticsearch): format Kibana-style REST requests

This commit is contained in:
Melody Ruch
2026-08-14 20:41:46 +08:00
committed by GitHub
parent f5a3652f6a
commit 185ff68d78
3 changed files with 130 additions and 10 deletions
@@ -27,6 +27,7 @@ import { buildSqlInConditionFromPasteSource, insertTextForSqlInCondition } from
import { resolveSqlSingleQuoteKeyAction } from "@/lib/sql/sqlQuoteCaret";
import { convertSqlSelectionCase, type SqlSelectionCaseMode } from "@/lib/sql/sqlSelectionCase";
import { formatMongoShellText } from "@/lib/mongo/mongoFormatter";
import { detectAndFormatElasticsearchRequests } from "@/lib/elasticsearch/elasticsearchFormatter";
import { useConnectionStore, COMPLETION_METADATA_CONCURRENCY } from "@/stores/connectionStore";
import { useSettingsStore } from "@/stores/settingsStore";
import { useTheme } from "@/composables/useTheme";
@@ -2932,19 +2933,27 @@ async function formatCurrentSql() {
if (props.databaseType === "mongodb") {
formatted = formatMongoShellText(source, settingsStore.editorSettings.sqlFormatter);
} else {
const structured = detectAndFormatStructured(source, {
indentSize: settingsStore.editorSettings.sqlFormatter.tabWidth,
useTabs: settingsStore.editorSettings.sqlFormatter.useTabs,
});
if (structured.kind === "json" || structured.kind === "xml") {
formatted = structured.formatted;
} else if (structured.kind === "unsupported") {
// Keep invalid structured text untouched — the SQL formatter would
// silently corrupt XML-looking content.
const esRequest = detectAndFormatElasticsearchRequests(source, props.databaseType, settingsStore.editorSettings.sqlFormatter.tabWidth);
if (esRequest.kind === "elasticsearch") {
formatted = esRequest.formatted;
} else if (esRequest.kind === "unsupported") {
toast(t("toolbar.formatAutoDetectFailed"), 3000);
return;
} else {
formatted = await formatSqlForEditing(source, props.formatDialect ?? props.dialect ?? "generic", settingsStore.editorSettings.sqlFormatter);
const structured = detectAndFormatStructured(source, {
indentSize: settingsStore.editorSettings.sqlFormatter.tabWidth,
useTabs: settingsStore.editorSettings.sqlFormatter.useTabs,
});
if (structured.kind === "json" || structured.kind === "xml") {
formatted = structured.formatted;
} else if (structured.kind === "unsupported") {
// Keep invalid structured text untouched — the SQL formatter would
// silently corrupt XML-looking content.
toast(t("toolbar.formatAutoDetectFailed"), 3000);
return;
} else {
formatted = await formatSqlForEditing(source, props.formatDialect ?? props.dialect ?? "generic", settingsStore.editorSettings.sqlFormatter);
}
}
}
if (view.value !== currentView || currentView.state !== originalState || currentView.state.sliceDoc(from, to) !== source) {
@@ -0,0 +1,66 @@
import { describe, expect, it } from "vitest";
import { detectAndFormatElasticsearchRequests } from "@/lib/elasticsearch/elasticsearchFormatter";
describe("elasticsearchFormatter", () => {
describe("detectAndFormatElasticsearchRequests", () => {
it("pretty-prints a single-line GET request with a JSON body", () => {
const source = `GET /master_base/_search {"query":{"bool":{"must":[{"term":{"masterId":"47526596395"}}]}}}`;
const result = detectAndFormatElasticsearchRequests(source, "elasticsearch", 2);
expect(result).toEqual({
kind: "elasticsearch",
formatted: `GET /master_base/_search
{
"query": {
"bool": {
"must": [
{
"term": {
"masterId": "47526596395"
}
}
]
}
}
}`,
});
});
it("leaves a request with no body as just the method/path line", () => {
const result = detectAndFormatElasticsearchRequests("GET /_cat/indices", "elasticsearch", 2);
expect(result).toEqual({ kind: "elasticsearch", formatted: "GET /_cat/indices" });
});
it("formats multiple requests separated by a blank line, keeping the separator", () => {
const source = `GET /a/_search {"query":{"match_all":{}}}\n\nPOST /b/_refresh`;
const result = detectAndFormatElasticsearchRequests(source, "easysearch", 2);
expect(result).toEqual({
kind: "elasticsearch",
formatted: `GET /a/_search\n{\n "query": {\n "match_all": {}\n }\n}\n\nPOST /b/_refresh`,
});
});
it("reports unsupported (and keeps caller from touching the text) for an invalid JSON body", () => {
const result = detectAndFormatElasticsearchRequests(`GET /a/_search {"query":`, "elasticsearch", 2);
expect(result).toEqual({ kind: "unsupported" });
});
it("reports not-elasticsearch for bare JSON with no method/path line", () => {
const result = detectAndFormatElasticsearchRequests(`{"query":{"match_all":{}}}`, "elasticsearch", 2);
expect(result).toEqual({ kind: "not-elasticsearch" });
});
it("reports not-elasticsearch for non-elasticsearch database types", () => {
const source = `GET /master_base/_search {"query":{"match_all":{}}}`;
expect(detectAndFormatElasticsearchRequests(source, "mysql", 2)).toEqual({ kind: "not-elasticsearch" });
expect(detectAndFormatElasticsearchRequests(source, undefined, 2)).toEqual({ kind: "not-elasticsearch" });
});
it("formats meilisearch requests too", () => {
const result = detectAndFormatElasticsearchRequests(`POST /indexes/movies/search {"q":"batman"}`, "meilisearch", 2);
expect(result).toEqual({
kind: "elasticsearch",
formatted: `POST /indexes/movies/search\n{\n "q": "batman"\n}`,
});
});
});
});
@@ -0,0 +1,45 @@
import { formatJsonSource } from "@/lib/common/safeJsonFormat";
import { elasticsearchRestRequestRanges } from "@/lib/sql/sqlStatementRanges";
import type { DatabaseType } from "@/types/database";
const ELASTICSEARCH_REQUEST_LINE = /^(GET|POST|PUT|PATCH|DELETE|HEAD)\s+(\S+)/i;
export type ElasticsearchFormatResult = { kind: "elasticsearch"; formatted: string } | { kind: "unsupported" } | { kind: "not-elasticsearch" };
function formatSingleElasticsearchRequest(requestText: string, indentSize: number): string | null {
const match = requestText.match(ELASTICSEARCH_REQUEST_LINE);
if (!match) return null;
const method = match[1].toUpperCase();
const rawPath = match[2];
const path = rawPath.startsWith("/") ? rawPath : `/${rawPath}`;
const body = requestText.slice(match[0].length).trim();
if (!body) return `${method} ${path}`;
try {
return `${method} ${path}\n${formatJsonSource(body, indentSize)}`;
} catch {
return null;
}
}
/**
* Formats Kibana-console-style Elasticsearch/Meilisearch REST requests
* (`METHOD /path {json body}`, possibly several in one document) by
* pretty-printing each request's JSON body and normalizing its method/path
* onto its own line. Text between requests (blank lines, comments) is kept
* as-is via {@link elasticsearchRestRequestRanges}.
*/
export function detectAndFormatElasticsearchRequests(text: string, databaseType: DatabaseType | undefined, indentSize: number): ElasticsearchFormatResult {
const ranges = elasticsearchRestRequestRanges(text, databaseType);
if (ranges.length === 0) return { kind: "not-elasticsearch" };
let result = "";
let cursor = 0;
for (const range of ranges) {
const formattedOne = formatSingleElasticsearchRequest(range.sql, indentSize);
if (formattedOne === null) return { kind: "unsupported" };
result += text.slice(cursor, range.from) + formattedOne;
cursor = range.to;
}
result += text.slice(cursor);
return { kind: "elasticsearch", formatted: result };
}