feat(meilisearch): add native document database support

This commit is contained in:
t8y2 committed 2026-08-13 02:59:56 +08:00
1 parent 292d0c8b54
commit a740e2e9ce
79 files changed
+1801 -170

No files matched your search

@@ -1,7 +1,7 @@
import { computed, ref } from "vue";
import { createPinia, setActivePinia } from "pinia";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { requiresDatabaseSelection, useSqlExecution } from "../useSqlExecution";
import { isDangerousSql, requiresDatabaseSelection, supportsSqlTemplateParameters, useSqlExecution } from "../useSqlExecution";
import { useConnectionStore } from "@/stores/connectionStore";
import { useHistoryStore } from "@/stores/historyStore";
import { useQueryStore } from "@/stores/queryStore";
@@ -115,6 +115,13 @@ describe("requiresDatabaseSelection", () => {
});
});
describe("supportsSqlTemplateParameters", () => {
it("disables SQL parameter prompts for Meilisearch input", () => {
expect(supportsSqlTemplateParameters(connection("meilisearch"), "GET /indexes/:uid")).toBe(false);
expect(supportsSqlTemplateParameters(connection("meilisearch"), "plain :value")).toBe(false);
});
});
describe("useSqlExecution", () => {
beforeEach(() => {
installLocalStorage();
@@ -756,6 +763,20 @@ SELECT @value AS Message;`;
expect(executeCurrentSql).toHaveBeenCalledWith(sql, { skipRedisSafetyCheck: false, openInNewResultTab: true });
});
it("distinguishes read-only and mutating Meilisearch REST requests", () => {
expect(isDangerousSql("GET /health", "meilisearch")).toBe(false);
expect(isDangerousSql('POST /indexes/movies/documents/fetch\n{"limit":10}', "meilisearch")).toBe(false);
expect(isDangerousSql('POST /indexes/movies/search\n{"q":"space"}', "meilisearch")).toBe(false);
expect(isDangerousSql('POST /indexes\n{"uid":"movies"}', "meilisearch")).toBe(true);
expect(isDangerousSql("PUT /indexes/movies/settings", "meilisearch")).toBe(true);
expect(isDangerousSql("DELETE /indexes/movies/documents/001", "meilisearch")).toBe(true);
});
it("treats Elasticsearch PATCH requests as mutating", () => {
expect(isDangerousSql('PATCH /products/_settings\n{"index":{"refresh_interval":"5s"}}', "elasticsearch")).toBe(true);
expect(isDangerousSql('PATCH /products/_settings\n{"index":{"refresh_interval":"5s"}}', "easysearch")).toBe(true);
});
it("requires production confirmation even when ordinary danger prompts are disabled", async () => {
const activeTab = ref<QueryTab | undefined>(queryTab("prod_app"));
const activeConnection = ref<ConnectionConfig | undefined>({ ...connection("mysql"), production_databases: ["prod_app"] });
+1 -1
View File
@@ -19,7 +19,7 @@ const QUICK_OPEN_MAX_RESULTS = 200;
const INITIAL_SQL_LIBRARY_LIMIT = 20;
const INITIAL_SQL_FILE_LIMIT = 20;
const REMOTE_SEARCH_UNSUPPORTED_TYPES = new Set<ConnectionConfig["db_type"]>(["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "neo4j", "influxdb", "victoriametrics", "etcd", "zookeeper", "mq", "nacos", "consul"]);
const REMOTE_SEARCH_UNSUPPORTED_TYPES = new Set<ConnectionConfig["db_type"]>(["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "neo4j", "influxdb", "victoriametrics", "etcd", "zookeeper", "mq", "nacos", "consul"]);
export interface QuickOpenItem {
id: string;
@@ -60,19 +60,28 @@ export function stripSqlComments(sql: string): string {
const ELASTICSEARCH_TRANSIENT_DELETE_PATHS = [/^\/_search\/scroll\/?$/i, /^\/_pit\/?$/i, /^\/_async_search\/[^/?]+\/?$/i];
const ELASTICSEARCH_DESTRUCTIVE_POST_PATHS = [/(?:^|\/)_(?:delete_by_query|update_by_query|bulk)(?:\/|$)/i, /^\/_reindex(?:\/|$)/i, /^\/_aliases(?:\/|$)/i, /\/_restore(?:\/|$)/i];
function isDangerousElasticsearchRequest(method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD", path: string): boolean {
function isDangerousElasticsearchRequest(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD", path: string): boolean {
const pathname = path.split("?", 1)[0].replace(/\/+$/, "") || "/";
if (method === "DELETE") return !ELASTICSEARCH_TRANSIENT_DELETE_PATHS.some((pattern) => pattern.test(pathname));
if (method === "PUT") return true;
if (method === "PUT" || method === "PATCH") return true;
return method === "POST" && ELASTICSEARCH_DESTRUCTIVE_POST_PATHS.some((pattern) => pattern.test(pathname));
}
function isDangerousMeilisearchRequest(method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD", path: string): boolean {
if (method === "GET" || method === "HEAD") return false;
if (method === "DELETE" || method === "PUT" || method === "PATCH") return true;
const pathname = path.split("?", 1)[0].replace(/\/+$/, "") || "/";
return !(pathname === "/multi-search" || /\/search$/i.test(pathname) || /\/facet-search$/i.test(pathname) || /\/similar$/i.test(pathname) || /\/documents\/fetch$/i.test(pathname));
}
export function isDangerousSql(sql: string, databaseType?: DatabaseType): boolean {
if (databaseType === "elasticsearch" || databaseType === "easysearch") {
if (databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch") {
const requests = splitSqlStatementRanges(sql, databaseType)
.map((statement) => parseElasticsearchRestRequestTarget(statement.sql))
.filter((request): request is NonNullable<typeof request> => request !== null);
if (requests.length > 0) return requests.some((request) => isDangerousElasticsearchRequest(request.method, request.path));
if (requests.length > 0) {
return requests.some((request) => (databaseType === "meilisearch" ? isDangerousMeilisearchRequest(request.method, request.path) : isDangerousElasticsearchRequest(request.method, request.path)));
}
}
const cleaned = stripSqlComments(sql);
return cleaned.split(";").some((stmt) => DANGER_RE.test(stmt));
@@ -562,6 +571,7 @@ export function useSqlExecution(deps: {
export function supportsSqlTemplateParameters(connection: Pick<ConnectionConfig, "db_type"> | undefined, sql = ""): boolean {
if (!connection) return false;
if (connection.db_type === "meilisearch") return false;
if (connection.db_type === "elasticsearch" || connection.db_type === "easysearch") return !isElasticsearchRestRequestText(sql);
return connection.db_type !== "redis" && connection.db_type !== "mongodb" && connection.db_type !== "victoriametrics";
}