feat(meilisearch): add native document database support
This commit is contained in:
79 files changed
+1801
-170
No files matched your search
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Meilisearch">
|
||||
<defs>
|
||||
<linearGradient id="m" x1="8" y1="56" x2="56" y2="8" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF5CAA"/>
|
||||
<stop offset="1" stop-color="#FFB03A"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path fill="url(#m)" d="M10 47.5 18.5 17a5 5 0 0 1 9.2-1.2L32 23l4.3-7.2a5 5 0 0 1 9.2 1.2L54 47.5a4.5 4.5 0 0 1-8.6 2.6L40.2 31l-4.3 7.1a4.5 4.5 0 0 1-7.8 0L23.8 31l-5.2 19.1a4.5 4.5 0 0 1-8.6-2.6Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 519 B |
@@ -2037,7 +2037,7 @@ async function handleQuickOpenSelect(item: any) {
|
||||
await connectionStore.loadConsulRoot(item.connectionId);
|
||||
} else if (config?.db_type === "mongodb") {
|
||||
await connectionStore.loadMongoDatabases(item.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch") {
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
await connectionStore.openElasticsearchConnectionTree(item.connectionId);
|
||||
} else if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate" || config?.db_type === "chromadb") {
|
||||
await connectionStore.loadVectorCollections(item.connectionId);
|
||||
@@ -2064,7 +2064,7 @@ async function handleQuickOpenSelect(item: any) {
|
||||
await connectionStore.loadConsulRoot(item.connectionId);
|
||||
} else if (config?.db_type === "mongodb") {
|
||||
await connectionStore.loadMongoDatabases(item.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch") {
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
await connectionStore.openElasticsearchConnectionTree(item.connectionId);
|
||||
} else if (config?.db_type === "qdrant" || config?.db_type === "milvus" || config?.db_type === "weaviate" || config?.db_type === "chromadb") {
|
||||
await connectionStore.loadVectorCollections(item.connectionId);
|
||||
|
||||
@@ -1045,6 +1045,13 @@ const driverProfiles: Record<
|
||||
label: "Easysearch",
|
||||
icon: "easysearch",
|
||||
},
|
||||
meilisearch: {
|
||||
type: "meilisearch",
|
||||
port: 7700,
|
||||
user: "",
|
||||
label: "Meilisearch",
|
||||
icon: "meilisearch",
|
||||
},
|
||||
hbase: { type: "hbase", port: 8080, user: "", label: "Apache HBase", icon: "hbase" },
|
||||
qdrant: { type: "qdrant", port: 6333, user: "", label: "Qdrant", icon: "qdrant" },
|
||||
milvus: { type: "milvus", port: 19530, user: "root", label: "Milvus", icon: "milvus" },
|
||||
@@ -2707,6 +2714,7 @@ const iconTypeMap: Record<string, string> = {
|
||||
oracle: "oracle",
|
||||
elasticsearch: "elasticsearch",
|
||||
easysearch: "easysearch",
|
||||
meilisearch: "meilisearch",
|
||||
hbase: "hbase",
|
||||
qdrant: "qdrant",
|
||||
milvus: "milvus",
|
||||
@@ -2793,6 +2801,7 @@ const dbOptions: DbOption[] = [
|
||||
{ value: "sqlserver", label: "SQL Server" },
|
||||
{ value: "elasticsearch", label: "Elasticsearch" },
|
||||
{ value: "easysearch", label: "Easysearch" },
|
||||
{ value: "meilisearch", label: "Meilisearch" },
|
||||
{ value: "hbase", label: "Apache HBase" },
|
||||
{ value: "qdrant", label: "Qdrant" },
|
||||
{ value: "milvus", label: "Milvus" },
|
||||
@@ -2900,7 +2909,7 @@ const dbCategoryDefinitions: Array<{
|
||||
{
|
||||
key: "document",
|
||||
titleKey: "connection.databaseCategoryDocument",
|
||||
optionValues: ["mongodb", "redis", "elasticsearch", "easysearch", "hbase", "manticoresearch", "cassandra"],
|
||||
optionValues: ["mongodb", "redis", "elasticsearch", "easysearch", "meilisearch", "hbase", "manticoresearch", "cassandra"],
|
||||
},
|
||||
{
|
||||
key: "graph_ai",
|
||||
@@ -3040,6 +3049,7 @@ const tlsCapableDatabaseTypes = new Set<DatabaseType>([
|
||||
"clickhouse",
|
||||
"elasticsearch",
|
||||
"easysearch",
|
||||
"meilisearch",
|
||||
"hbase",
|
||||
"qdrant",
|
||||
"milvus",
|
||||
@@ -3841,6 +3851,11 @@ function connectionConfigForSubmit(id: string, generatedName = ""): ConnectionCo
|
||||
config.username = config.username.trim();
|
||||
} else if (config.db_type === "elasticsearch") {
|
||||
config.external_config = buildElasticsearchExternalConfig(elasticsearchConnectionMode.value, elasticsearchKibanaBasePath.value, elasticsearchConnectivityCheckPath.value, elasticsearchIndexGroupingPattern.value);
|
||||
} else if (config.db_type === "meilisearch") {
|
||||
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)) {
|
||||
@@ -5570,7 +5585,7 @@ function openExternalUrl(url: string) {
|
||||
@dblclick="goToConnectionStep(opt.value)"
|
||||
>
|
||||
<span class="flex h-10 w-10 items-center justify-center rounded-xl bg-muted/60 transition group-hover:bg-background">
|
||||
<DatabaseIcon :db-type="iconTypeMap[opt.value]" class="h-6 w-6" />
|
||||
<DatabaseIcon :db-type="iconTypeMap[opt.value] || opt.value" class="h-6 w-6" />
|
||||
</span>
|
||||
<span class="flex min-h-8 max-w-full items-center justify-center">
|
||||
<span class="line-clamp-2 text-sm leading-4 font-medium">{{ opt.label }}</span>
|
||||
@@ -5589,7 +5604,7 @@ function openExternalUrl(url: string) {
|
||||
@click="onDbTypeChange(opt.value)"
|
||||
@dblclick="goToConnectionStep(opt.value)"
|
||||
>
|
||||
<DatabaseIcon :db-type="iconTypeMap[opt.value]" class="h-5 w-5 shrink-0" />
|
||||
<DatabaseIcon :db-type="iconTypeMap[opt.value] || opt.value" class="h-5 w-5 shrink-0" />
|
||||
<span class="min-w-0 flex-1 truncate text-sm font-medium">{{ opt.label }}</span>
|
||||
<span v-if="isDbSearchActive" class="text-xs text-muted-foreground">{{ category.title }}</span>
|
||||
</button>
|
||||
@@ -6956,13 +6971,13 @@ function openExternalUrl(url: string) {
|
||||
<Input v-model="form.informix_server" class="col-span-3" placeholder="ol_informix1170" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<div v-if="form.db_type !== 'meilisearch'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.user") }}</Label>
|
||||
<Input v-model="form.username" class="col-span-3" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ t("connection.password") }}</Label>
|
||||
<Label :class="connectionLabelClass">{{ form.db_type === "meilisearch" ? t("connection.mqAuthApiKey") : t("connection.password") }}</Label>
|
||||
<PasswordInput v-model="form.password" class="col-span-3" />
|
||||
</div>
|
||||
|
||||
@@ -6975,7 +6990,7 @@ function openExternalUrl(url: string) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="form.db_type !== 'hbase'" class="grid grid-cols-4 items-center gap-4">
|
||||
<div v-if="form.db_type !== 'hbase' && form.db_type !== 'meilisearch'" class="grid grid-cols-4 items-center gap-4">
|
||||
<Label :class="connectionLabelClass">{{ databaseLabel }}</Label>
|
||||
<Input v-model="form.database" class="col-span-3" :placeholder="databasePlaceholder" />
|
||||
</div>
|
||||
|
||||
@@ -118,7 +118,7 @@ const showModified = ref(true);
|
||||
|
||||
let syncPlanRequestId = 0;
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(connection.db_type)));
|
||||
const sqlConnections = computed(() => store.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(connection.db_type)));
|
||||
const selectedSourceTableNames = computed(() => sourceTables.value.filter((table) => selectedSourceTables.value.has(table)));
|
||||
const isBatchCompare = computed(() => selectedSourceTableNames.value.length > 1);
|
||||
const filteredSourceTables = computed(() => {
|
||||
|
||||
@@ -58,7 +58,7 @@ const targetSchemas = ref<string[]>([]);
|
||||
const sourceDbVersion = ref<string | null>(null);
|
||||
const targetDbVersion = ref<string | null>(null);
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((c: any) => !["mongodb", "redis", "elasticsearch", "easysearch", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
const sqlConnections = computed(() => store.connections.filter((c: any) => !["mongodb", "redis", "elasticsearch", "easysearch", "meilisearch", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
|
||||
const sourceConfig = computed(() => store.getConfig(props.sourceConnectionId));
|
||||
const targetConfig = computed(() => store.getConfig(props.targetConnectionId));
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
flattenDocumentFieldPathTree,
|
||||
searchDocumentFieldPathTree,
|
||||
documentFilterModeNeedsValue,
|
||||
documentFilterModeOptions,
|
||||
documentFilterModeOptionsFor,
|
||||
documentFilterValueTypeOptions,
|
||||
documentStoreProviderFor,
|
||||
elasticsearchBoolClauseOptions,
|
||||
@@ -60,12 +60,13 @@ import {
|
||||
parseDocumentStoreInputValue,
|
||||
parseDocumentStoreJsonDocument,
|
||||
planDocumentStoreIdentityMigration,
|
||||
prepareDocumentStoreWriteDocument,
|
||||
resolveDocumentStoreWriteRouting,
|
||||
serializeDocumentStoreId,
|
||||
stringifyDocumentStoreValue,
|
||||
documentStoreValueForGrid,
|
||||
} from "@/lib/app/documentJsonValues";
|
||||
import { applyDocumentStoreIdentityPlan, insertDocumentStoreDocument as insertDocumentStoreDocumentCore } from "@/lib/app/documentStoreSave";
|
||||
import { applyDocumentStoreIdentityPlan, formatMeilisearchDocumentOperationPreview, insertDocumentStoreDocument as insertDocumentStoreDocumentCore } from "@/lib/app/documentStoreSave";
|
||||
import RedisJsonEditor from "@/components/redis/RedisJsonEditor.vue";
|
||||
import { isLosslessJsonNumber, parseJsonPreservingLargeNumbers } from "@/lib/common/safeJsonFormat";
|
||||
import {
|
||||
@@ -299,8 +300,8 @@ const deleteDetails = computed(() => {
|
||||
if (pending.kind === "document") {
|
||||
const id = documents.value[pending.index]?._id ?? "";
|
||||
const displayId = mongoDocumentIdForGrid(id);
|
||||
if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch") {
|
||||
const product = props.databaseType === "easysearch" ? "Easysearch" : "Elasticsearch";
|
||||
if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch") {
|
||||
const product = props.databaseType === "easysearch" ? "Easysearch" : props.databaseType === "meilisearch" ? "Meilisearch" : "Elasticsearch";
|
||||
return `${product} index: ${props.collection}\nDocument _id: ${String(displayId)}`;
|
||||
}
|
||||
return t("dangerDialog.mongoDocumentDetails", { collection: props.collection, id: String(displayId) });
|
||||
@@ -335,7 +336,7 @@ const gridResult = computed<QueryResult>(() => {
|
||||
columns.map((col) => {
|
||||
const val = mongoDocumentDisplayValue(doc[col]);
|
||||
if (val === undefined || val === null) return null;
|
||||
if (col === "_id") return documentStoreProvider.value.kind === "elasticsearch" ? documentStoreValueForGrid(val, "elasticsearch") : mongoDocumentIdForGrid(val);
|
||||
if (col === "_id") return documentStoreProvider.value.kind === "mongodb" ? mongoDocumentIdForGrid(val) : documentStoreValueForGrid(val, documentStoreProvider.value.kind);
|
||||
if (typeof val === "object") return documentStoreValueForGrid(val, documentStoreProvider.value.kind);
|
||||
if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") return val;
|
||||
return String(val);
|
||||
@@ -437,6 +438,7 @@ const documentStructuredFilterCount = computed(() => {
|
||||
return count + (Array.isArray(rules) ? rules.length : 0);
|
||||
}, 0);
|
||||
});
|
||||
const currentDocumentFilterModeOptions = computed(() => documentFilterModeOptionsFor(documentStoreProvider.value.kind));
|
||||
const documentLoadingLabelKey = computed(() => (documentLoadCancelling.value ? "common.stopping" : "common.loading"));
|
||||
let documentLoadingTimer: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
@@ -765,28 +767,60 @@ async function gridSave(changes: DocumentGridChanges) {
|
||||
const cols = changes.columns;
|
||||
const idColIdx = cols.indexOf("_id");
|
||||
if (idColIdx < 0) throw new Error("No _id column");
|
||||
const isEs = documentStoreProvider.value.kind === "elasticsearch";
|
||||
const kind = documentStoreProvider.value.kind;
|
||||
const isPathIdentityStore = kind !== "mongodb";
|
||||
const isEs = kind === "elasticsearch";
|
||||
|
||||
if (kind === "meilisearch") {
|
||||
const updates: Array<{ id: string; docJson: string }> = [];
|
||||
const deleteIds: string[] = [];
|
||||
const inserts: string[] = [];
|
||||
|
||||
for (const [rowIdx, dirtyCols] of changes.dirtyRows) {
|
||||
const row = changes.rows[rowIdx];
|
||||
const id = row?.[idColIdx];
|
||||
const doc = documents.value[rowIdx];
|
||||
if (id == null || !doc) continue;
|
||||
const updated = buildPathIdentityUpdatedDocument(doc, dirtyCols, cols, kind);
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(updated, { kind, mode: "update" });
|
||||
updates.push({
|
||||
id: serializeDocumentStoreId(doc._id ?? id, kind),
|
||||
docJson: stringifyDocumentStoreValue(writeDocument, kind),
|
||||
});
|
||||
}
|
||||
|
||||
for (const rowIdx of changes.deletedRows) {
|
||||
const row = changes.rows[rowIdx];
|
||||
const id = row?.[idColIdx];
|
||||
if (id == null) continue;
|
||||
deleteIds.push(serializeDocumentStoreId(documents.value[rowIdx]?._id ?? id, kind));
|
||||
}
|
||||
|
||||
for (const newRow of changes.newRows) {
|
||||
const doc = buildPathIdentityInsertDocument(newRow, cols, kind);
|
||||
const idValue = newRow[idColIdx];
|
||||
if (idValue !== null && idValue !== undefined && idValue !== "") doc._id = parseDocumentStoreInputValue(idValue, kind);
|
||||
inserts.push(stringifyDocumentStoreValue(doc, kind));
|
||||
}
|
||||
|
||||
await api.documentSaveMeilisearchBatch(props.connectionId, props.collection, updates, deleteIds, inserts);
|
||||
await load();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [rowIdx, dirtyCols] of changes.dirtyRows) {
|
||||
const row = changes.rows[rowIdx];
|
||||
const id = row?.[idColIdx];
|
||||
if (id == null) continue;
|
||||
|
||||
if (isEs) {
|
||||
if (isPathIdentityStore) {
|
||||
const doc = documents.value[rowIdx];
|
||||
if (!doc) continue;
|
||||
const routing = documentRoutingFromDocument(doc);
|
||||
const updated = { ...doc };
|
||||
for (const [colIdx, newVal] of dirtyCols) {
|
||||
const col = cols[colIdx];
|
||||
if (col === "_id" || col === "_routing") continue;
|
||||
if (newVal === null) {
|
||||
delete updated[col];
|
||||
} else {
|
||||
updated[col] = parseDocumentStoreInputValue(newVal, "elasticsearch");
|
||||
}
|
||||
}
|
||||
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, String(id), stringifyDocumentStoreValue(updated, "elasticsearch"), routing);
|
||||
const routing = isEs ? documentRoutingFromDocument(doc) : undefined;
|
||||
const updated = buildPathIdentityUpdatedDocument(doc, dirtyCols, cols, kind);
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(updated, { kind, mode: "update" });
|
||||
const documentId = serializeDocumentStoreId(doc._id ?? id, kind);
|
||||
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, documentId, stringifyDocumentStoreValue(writeDocument, kind), routing);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -803,20 +837,21 @@ async function gridSave(changes: DocumentGridChanges) {
|
||||
const document = documents.value[rowIdx];
|
||||
const routing = isEs ? documentRoutingFromDocument(document) : undefined;
|
||||
const documentType = isEs ? documentTypeFromDocument(document) : undefined;
|
||||
const documentId = isEs ? id : (document?._id ?? id);
|
||||
await api.documentDeleteDocument(props.connectionId, props.database, props.collection, isEs ? String(documentId) : serializeMongoDocumentId(documentId), routing, documentType);
|
||||
const documentId = document?._id ?? id;
|
||||
await api.documentDeleteDocument(props.connectionId, props.database, props.collection, isPathIdentityStore ? serializeDocumentStoreId(documentId, kind) : serializeMongoDocumentId(documentId), routing, documentType);
|
||||
}
|
||||
|
||||
for (const [newRowIndex, newRow] of changes.newRows.entries()) {
|
||||
const newRowMeta = changes.newRowMeta[newRowIndex];
|
||||
const doc = isEs ? buildElasticsearchInsertDocument(newRow, cols) : buildMongoGridInsertDocument(newRow, cols, newRowMeta);
|
||||
if (isEs) {
|
||||
const id = documentIdFromGridValue(newRow[idColIdx]);
|
||||
const routing = documentRoutingFromGridRow(newRow, cols);
|
||||
const doc = isPathIdentityStore ? buildPathIdentityInsertDocument(newRow, cols, kind) : buildMongoGridInsertDocument(newRow, cols, newRowMeta);
|
||||
if (isPathIdentityStore) {
|
||||
const idValue = newRow[idColIdx];
|
||||
const id = idValue === null || idValue === undefined || idValue === "" ? null : serializeDocumentStoreId(parseDocumentStoreInputValue(idValue, kind), kind);
|
||||
const routing = isEs ? documentRoutingFromGridRow(newRow, cols) : undefined;
|
||||
if (id) {
|
||||
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, id, stringifyDocumentStoreValue(doc, "elasticsearch"), routing);
|
||||
await api.documentUpdateDocument(props.connectionId, props.database, props.collection, id, stringifyDocumentStoreValue(doc, kind), routing);
|
||||
} else {
|
||||
await api.documentInsertDocument(props.connectionId, props.database, props.collection, stringifyDocumentStoreValue(doc, "elasticsearch"), routing);
|
||||
await api.documentInsertDocument(props.connectionId, props.database, props.collection, stringifyDocumentStoreValue(doc, kind), routing);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -829,17 +864,28 @@ async function gridSave(changes: DocumentGridChanges) {
|
||||
await load();
|
||||
}
|
||||
|
||||
function buildElasticsearchInsertDocument(row: MongoInputValue[], columns: string[]): JsonRecord {
|
||||
function buildPathIdentityInsertDocument(row: MongoInputValue[], columns: string[], kind: Exclude<DocumentStoreKind, "mongodb">): JsonRecord {
|
||||
const doc: JsonRecord = {};
|
||||
for (let columnIndex = 0; columnIndex < columns.length; columnIndex += 1) {
|
||||
const column = columns[columnIndex];
|
||||
if (!column || column === "_id" || column === "_routing") continue;
|
||||
if (!column || column === "_id" || (kind === "elasticsearch" && column === "_routing")) continue;
|
||||
const value = row[columnIndex];
|
||||
if (value !== null) doc[column] = parseDocumentStoreInputValue(value, "elasticsearch");
|
||||
if (value !== null) doc[column] = parseDocumentStoreInputValue(value, kind);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
function buildPathIdentityUpdatedDocument(document: JsonRecord, changes: Map<number, MongoInputValue>, columns: string[], kind: Exclude<DocumentStoreKind, "mongodb">): JsonRecord {
|
||||
const updated = { ...document };
|
||||
for (const [columnIndex, newValue] of changes) {
|
||||
const column = columns[columnIndex];
|
||||
if (!column || column === "_id" || (kind === "elasticsearch" && column === "_routing")) continue;
|
||||
if (newValue === null) delete updated[column];
|
||||
else updated[column] = parseDocumentStoreInputValue(newValue, kind);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
function buildMongoGridInsertDocument(row: MongoInputValue[], columns: string[], meta?: GridNewRowMeta): Record<string, unknown> {
|
||||
const sourceIndex = meta?.sourceIndex;
|
||||
const sourceDocument = sourceIndex === undefined ? undefined : copyDocuments.value[sourceIndex];
|
||||
@@ -879,16 +925,27 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
|
||||
const idColIdx = columns.indexOf("_id");
|
||||
const stmts: string[] = [];
|
||||
const coll = props.collection;
|
||||
const isEs = documentStoreProvider.value.kind === "elasticsearch";
|
||||
const kind = documentStoreProvider.value.kind;
|
||||
const isPathIdentityStore = kind !== "mongodb";
|
||||
const isEs = kind === "elasticsearch";
|
||||
|
||||
for (const [rowIdx, dirtyCols] of dirtyRows) {
|
||||
const row = rows[rowIdx];
|
||||
const id = row?.[idColIdx];
|
||||
if (id == null) continue;
|
||||
if (isEs) {
|
||||
const updateDoc = buildElasticsearchPartialUpdateDocument(dirtyCols, columns);
|
||||
const routing = documentRoutingFromGridRow(row, columns);
|
||||
stmts.push(`POST /${coll}/_update/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}\n${stringifyDocumentStoreValue({ doc: updateDoc.$set ?? updateDoc }, "elasticsearch", 2)}`);
|
||||
if (isPathIdentityStore) {
|
||||
if (isEs) {
|
||||
const updateDoc = buildElasticsearchPartialUpdateDocument(dirtyCols, columns);
|
||||
const routing = documentRoutingFromGridRow(row, columns);
|
||||
stmts.push(`POST /${coll}/_update/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}\n${stringifyDocumentStoreValue({ doc: updateDoc.$set ?? updateDoc }, "elasticsearch", 2)}`);
|
||||
} else {
|
||||
const sourceDocument = documents.value[rowIdx];
|
||||
if (!sourceDocument) continue;
|
||||
const documentId = sourceDocument._id ?? id;
|
||||
const updated = buildPathIdentityUpdatedDocument(sourceDocument, dirtyCols, columns, "meilisearch");
|
||||
const writeDocument = prepareDocumentStoreWriteDocument(updated, { kind: "meilisearch", mode: "update" });
|
||||
stmts.push(formatMeilisearchDocumentOperationPreview({ action: "update", index: coll, id: documentId, document: writeDocument }));
|
||||
}
|
||||
} else {
|
||||
const updateDoc = buildMongoUpdateDocument(dirtyCols, columns, documents.value[rowIdx]);
|
||||
stmts.push(`db.${coll}.updateOne({_id: ${formatMongoShellLiteral(documents.value[rowIdx]?._id ?? id)}}, ${formatMongoShellLiteral(updateDoc)})`);
|
||||
@@ -899,7 +956,11 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
|
||||
const row = rows[rowIdx];
|
||||
const id = row?.[idColIdx];
|
||||
if (id == null) continue;
|
||||
if (isEs) {
|
||||
if (isPathIdentityStore) {
|
||||
if (!isEs) {
|
||||
stmts.push(formatMeilisearchDocumentOperationPreview({ action: "delete", index: coll, id: documents.value[rowIdx]?._id ?? id }));
|
||||
continue;
|
||||
}
|
||||
const routing = documentRoutingFromGridRow(row, columns);
|
||||
stmts.push(`DELETE /${coll}/_doc/${elasticsearchPathIdPreview(String(id))}${elasticsearchRoutingPreview(routing)}`);
|
||||
} else {
|
||||
@@ -908,8 +969,14 @@ async function previewDocumentChanges(changes: DocumentGridChanges): Promise<str
|
||||
}
|
||||
|
||||
for (const [newRowIndex, newRow] of newRows.entries()) {
|
||||
const doc = isEs ? buildElasticsearchInsertDocument(newRow, columns) : buildMongoGridInsertDocument(newRow, columns, newRowMeta[newRowIndex]);
|
||||
if (isEs) {
|
||||
const doc = isPathIdentityStore ? buildPathIdentityInsertDocument(newRow, columns, kind) : buildMongoGridInsertDocument(newRow, columns, newRowMeta[newRowIndex]);
|
||||
if (isPathIdentityStore) {
|
||||
if (!isEs) {
|
||||
const idValue = idColIdx >= 0 ? newRow[idColIdx] : null;
|
||||
const id = idValue === null || idValue === undefined || idValue === "" ? undefined : parseDocumentStoreInputValue(idValue, "meilisearch");
|
||||
stmts.push(formatMeilisearchDocumentOperationPreview({ action: id === undefined ? "insert" : "upsert", index: coll, id, document: doc }));
|
||||
continue;
|
||||
}
|
||||
const id = idColIdx >= 0 ? documentIdFromGridValue(newRow[idColIdx]) : null;
|
||||
if (id) {
|
||||
stmts.push(`PUT /${coll}/_doc/${elasticsearchPathIdPreview(id)}\n${stringifyDocumentStoreValue(doc, "elasticsearch", 2)}`);
|
||||
@@ -1515,7 +1582,7 @@ async function saveDoc() {
|
||||
|
||||
if (isNew.value) {
|
||||
const apis = documentStoreWriteApis();
|
||||
const explicitId = kind === "elasticsearch" ? documentIdFromGridValue(documentStoreValueForGrid(doc._id, "elasticsearch")) : null;
|
||||
const explicitId = kind === "mongodb" || doc._id === undefined || doc._id === null || doc._id === "" ? null : resolveDocumentStorePathId(doc._id);
|
||||
await insertDocumentStoreDocumentCore({
|
||||
kind,
|
||||
document: doc,
|
||||
@@ -1573,7 +1640,7 @@ async function saveDoc() {
|
||||
async function applyDeleteDoc(idx: number) {
|
||||
const doc = documents.value[idx];
|
||||
const id = doc._id;
|
||||
if (!id) return;
|
||||
if (id === undefined || id === null || id === "") return;
|
||||
error.value = "";
|
||||
try {
|
||||
await api.documentDeleteDocument(props.connectionId, props.database, props.collection, serializeDocumentStoreId(id, documentStoreProvider.value.kind), documentRoutingFromDocument(doc), documentStoreProvider.value.kind === "elasticsearch" ? documentTypeFromDocument(doc) : undefined);
|
||||
@@ -2086,7 +2153,7 @@ defineExpose({ focusSearch });
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem v-for="option in documentFilterModeOptions" :key="option.value" :value="option.value">
|
||||
<SelectItem v-for="option in currentDocumentFilterModeOptions" :key="option.value" :value="option.value">
|
||||
{{ t(option.labelKey) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
|
||||
+79
@@ -9,6 +9,7 @@ const backend = vi.hoisted(() => ({
|
||||
cancelQuery: vi.fn(),
|
||||
ensureConnected: vi.fn(),
|
||||
documentDeleteDocument: vi.fn(),
|
||||
documentSaveMeilisearchBatch: vi.fn(),
|
||||
}));
|
||||
|
||||
const documentJsonEditor = vi.hoisted(() => ({
|
||||
@@ -27,6 +28,11 @@ const dataGrid = vi.hoisted(() => ({
|
||||
| undefined
|
||||
>)
|
||||
| undefined,
|
||||
customSaveHandler: undefined as
|
||||
| {
|
||||
save: (changes: { dirtyRows: Map<number, Map<number, unknown>>; deletedRows: Set<number>; newRows: unknown[][]; newRowMeta: unknown[]; columns: string[]; rows: unknown[][] }) => Promise<void>;
|
||||
}
|
||||
| undefined,
|
||||
}));
|
||||
|
||||
const settings = vi.hoisted(() => ({
|
||||
@@ -56,6 +62,7 @@ vi.mock("@/lib/backend/api", () => ({
|
||||
documentFindDocuments: backend.documentFindDocuments,
|
||||
cancelQuery: backend.cancelQuery,
|
||||
documentDeleteDocument: backend.documentDeleteDocument,
|
||||
documentSaveMeilisearchBatch: backend.documentSaveMeilisearchBatch,
|
||||
}));
|
||||
|
||||
vi.mock("@/stores/connectionStore", () => ({
|
||||
@@ -82,9 +89,11 @@ vi.mock("@/components/grid/DataGrid.vue", async () => {
|
||||
database: { type: String, default: "" },
|
||||
columnLayoutScopeKey: { type: String, default: "" },
|
||||
fullExportResult: { type: Function, default: undefined },
|
||||
customSaveHandler: { type: Object, default: undefined },
|
||||
},
|
||||
setup(props, { expose, slots }) {
|
||||
dataGrid.fullExportResult = props.fullExportResult as typeof dataGrid.fullExportResult;
|
||||
dataGrid.customSaveHandler = props.customSaveHandler as typeof dataGrid.customSaveHandler;
|
||||
expose({
|
||||
visibleColumnCount: 2,
|
||||
displayableColumnCount: 2,
|
||||
@@ -278,9 +287,12 @@ beforeEach(async () => {
|
||||
backend.cancelQuery.mockReset();
|
||||
backend.ensureConnected.mockReset();
|
||||
backend.documentDeleteDocument.mockReset();
|
||||
backend.documentSaveMeilisearchBatch.mockReset();
|
||||
dataGrid.fullExportResult = undefined;
|
||||
dataGrid.customSaveHandler = undefined;
|
||||
documentJsonEditor.openSearch.mockClear();
|
||||
backend.documentDeleteDocument.mockResolvedValue(undefined);
|
||||
backend.documentSaveMeilisearchBatch.mockResolvedValue(0);
|
||||
settings.editorSettings.mongoViewMode = "table";
|
||||
settings.editorSettings.columnWidthDensity = "standard";
|
||||
settings.editorSettings.dataGridRenderMode = "canvas";
|
||||
@@ -757,4 +769,71 @@ describe("DocumentBrowser MongoDB filter value types", () => {
|
||||
expect(backend.documentDeleteDocument).not.toHaveBeenCalled();
|
||||
expect(document.body.textContent).toContain("dangerDialog.deleteMessage");
|
||||
});
|
||||
|
||||
it("saves Meilisearch grid changes in one batch request", async () => {
|
||||
app?.unmount();
|
||||
backend.getColumns.mockResolvedValue([
|
||||
{ name: "id", data_type: "string", is_primary_key: true },
|
||||
{ name: "title", data_type: "string" },
|
||||
{ name: "rating", data_type: "number" },
|
||||
{ name: "obsolete", data_type: "boolean" },
|
||||
]);
|
||||
backend.documentFindDocuments.mockResolvedValue({
|
||||
documents: [
|
||||
{ _id: "001", title: "One", rating: 1, obsolete: true },
|
||||
{ _id: 2, title: "Two", rating: 2, obsolete: false },
|
||||
{ _id: 3, title: "Three", rating: 3, obsolete: false },
|
||||
],
|
||||
raw_documents: [],
|
||||
total: 3,
|
||||
total_is_exact: true,
|
||||
});
|
||||
backend.documentSaveMeilisearchBatch.mockResolvedValue(4);
|
||||
app = createApp(DocumentBrowser, {
|
||||
connectionId: "meili-1",
|
||||
database: "default",
|
||||
collection: "movies",
|
||||
databaseType: "meilisearch",
|
||||
});
|
||||
app.mount(root!);
|
||||
await flushUi();
|
||||
|
||||
expect(dataGrid.customSaveHandler).toBeDefined();
|
||||
await dataGrid.customSaveHandler!.save({
|
||||
dirtyRows: new Map([
|
||||
[
|
||||
0,
|
||||
new Map([
|
||||
[1, "One revised"],
|
||||
[3, null],
|
||||
]),
|
||||
],
|
||||
]),
|
||||
deletedRows: new Set([2]),
|
||||
newRows: [
|
||||
["004", "Four", 4, false],
|
||||
[null, "Generated", 5, true],
|
||||
],
|
||||
newRowMeta: [{}, {}],
|
||||
columns: ["_id", "title", "rating", "obsolete"],
|
||||
rows: [
|
||||
["001", "One", 1, true],
|
||||
[2, "Two", 2, false],
|
||||
[3, "Three", 3, false],
|
||||
],
|
||||
});
|
||||
|
||||
expect(backend.documentSaveMeilisearchBatch).toHaveBeenCalledOnce();
|
||||
const [connectionId, collection, updates, deleteIds, inserts] = backend.documentSaveMeilisearchBatch.mock.calls[0]!;
|
||||
expect(connectionId).toBe("meili-1");
|
||||
expect(collection).toBe("movies");
|
||||
expect(updates).toHaveLength(1);
|
||||
expect(updates[0].id).toBe('__dbx_meilisearch_string_id__"001"');
|
||||
expect(JSON.parse(updates[0].docJson)).toEqual({ title: "One revised", rating: 1 });
|
||||
expect(deleteIds).toEqual(["3"]);
|
||||
expect(inserts.map((value: string) => JSON.parse(value))).toEqual([
|
||||
{ title: "Four", rating: 4, obsolete: false, _id: "004" },
|
||||
{ title: "Generated", rating: 5, obsolete: true },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1842,7 +1842,7 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
|
||||
function extendQueryEditorSelectionForView(currentView: EditorViewType): boolean {
|
||||
const databaseType = props.databaseType;
|
||||
const language = databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "victoriametrics" ? "text" : "sql";
|
||||
const language = databaseType === "redis" || databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "victoriametrics" ? "text" : "sql";
|
||||
return extendQueryEditorSelection(currentView, {
|
||||
databaseType,
|
||||
dialect: sqlBehaviorDialect(),
|
||||
@@ -2042,7 +2042,7 @@ function getInsertValueHintTableColumns(table: string, schema?: string, database
|
||||
|
||||
function requestInsertValueHintTableColumns(table: string, schema?: string, database?: string) {
|
||||
if (!props.connectionId || props.database == null) return;
|
||||
if (props.databaseType === "redis" || props.databaseType === "mongodb" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "victoriametrics") return;
|
||||
if (props.databaseType === "redis" || props.databaseType === "mongodb" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch" || props.databaseType === "victoriametrics") return;
|
||||
const cacheKey = insertHintCacheKey({ name: table, schema, database });
|
||||
const hasCachedColumns = props.databaseType === "sqlserver" ? cachedInsertValueHintColumnsByTable.has(cacheKey) : cachedColumnsByTable.has(cacheKey);
|
||||
if (hasCachedColumns || pendingInsertValueHintColumnLoads.has(cacheKey)) return;
|
||||
@@ -2763,7 +2763,7 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
if (props.databaseType === "mongodb" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "victoriametrics") {
|
||||
if (props.databaseType === "mongodb" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch" || props.databaseType === "meilisearch" || props.databaseType === "victoriametrics") {
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
@@ -3098,7 +3098,7 @@ function localCompletionSchemasForDatabaseDisambiguation(completionContext: Retu
|
||||
}
|
||||
|
||||
function shouldInsertSqlCompletionSpace(): boolean {
|
||||
return props.databaseType !== "mongodb" && props.databaseType !== "redis" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "victoriametrics";
|
||||
return props.databaseType !== "mongodb" && props.databaseType !== "redis" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "meilisearch" && props.databaseType !== "victoriametrics";
|
||||
}
|
||||
|
||||
function completionOptionForItem(item: QueryCompletionItem) {
|
||||
@@ -3285,6 +3285,7 @@ async function provideSqlCompletions(context: CompletionContext) {
|
||||
if (props.databaseType === "mongodb") {
|
||||
return provideMongoCompletions(currentState, position, explicit);
|
||||
}
|
||||
if (props.databaseType === "meilisearch") return null;
|
||||
if (props.databaseType === "elasticsearch" || props.databaseType === "easysearch") {
|
||||
if (!isSqlLikeCompletionStatement(fullDoc, position, sqlCompletionDialectOptions())) {
|
||||
return provideElasticsearchCompletions(currentState, position, explicit);
|
||||
@@ -3590,7 +3591,7 @@ function shouldStartSqlCompletionAfterInput(insertedText: string, removedText: s
|
||||
if (props.databaseType === "mongodb") {
|
||||
return !!(insertedText || removedText) && shouldAutoOpenMongoCompletion(fullDoc, position);
|
||||
}
|
||||
if (props.databaseType === "victoriametrics") return false;
|
||||
if (props.databaseType === "victoriametrics" || props.databaseType === "meilisearch") return false;
|
||||
if (props.databaseType === "redis" || props.databaseType === "elasticsearch" || props.databaseType === "easysearch") {
|
||||
// Preserve old character-based checks for non-SQL providers.
|
||||
if (!insertedText && removedText) {
|
||||
@@ -4787,7 +4788,8 @@ onMounted(async () => {
|
||||
sqlSignatureComp.of(buildSqlSignatureExtension()),
|
||||
diagnosticComp.of(buildSqlDiagnosticExtension()),
|
||||
createInsertValueHintsExtension({
|
||||
isEnabled: () => settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "victoriametrics",
|
||||
isEnabled: () =>
|
||||
settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch" && props.databaseType !== "easysearch" && props.databaseType !== "meilisearch" && props.databaseType !== "victoriametrics",
|
||||
getTableColumns: getInsertValueHintTableColumns,
|
||||
requestTableColumns: requestInsertValueHintTableColumns,
|
||||
}),
|
||||
|
||||
@@ -103,7 +103,7 @@ const exportElapsedText = computed(() => {
|
||||
return formatDataTransferDuration((exportFinishedAt.value ?? currentTime.value) - exportStartedAt.value);
|
||||
});
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
|
||||
const canExport = computed(() => {
|
||||
const hasContent = includeStructure.value || includeData.value || includeObjects.value;
|
||||
|
||||
@@ -30,4 +30,27 @@ describe("DatabaseIcon", () => {
|
||||
expect(container.querySelector("img")?.getAttribute("src")).toBe("/icons/database/phoenix.svg");
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("uses the Meilisearch asset", async () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const app = createApp(DatabaseIcon, { dbType: "meilisearch" });
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
|
||||
expect(container.querySelector("img")?.getAttribute("src")).toBe("/icons/database/meilisearch.svg");
|
||||
app.unmount();
|
||||
});
|
||||
|
||||
it("falls back to the generic icon when the database type is missing", async () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const app = createApp(DatabaseIcon);
|
||||
app.mount(container);
|
||||
await nextTick();
|
||||
|
||||
expect(container.querySelector("img")).toBeNull();
|
||||
expect(container.querySelector("svg")).not.toBeNull();
|
||||
app.unmount();
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { useTheme } from "@/composables/useTheme";
|
||||
import { webPath } from "@/lib/common/webPath";
|
||||
|
||||
const props = defineProps<{
|
||||
dbType: string;
|
||||
dbType?: string;
|
||||
}>();
|
||||
const { isDark } = useTheme();
|
||||
|
||||
@@ -27,6 +27,7 @@ const assetIcons: Record<string, string> = {
|
||||
tidb: "tidb",
|
||||
elasticsearch: "elasticsearch",
|
||||
easysearch: "easysearch",
|
||||
meilisearch: "meilisearch",
|
||||
oracle: "oracle",
|
||||
"oracle-10g": "oracle",
|
||||
"oracle-legacy": "oracle",
|
||||
@@ -110,7 +111,7 @@ const assetIcons: Record<string, string> = {
|
||||
dolt: "dolt",
|
||||
};
|
||||
|
||||
const normalizedType = computed(() => props.dbType.toLowerCase().replace(/[\s-]+/g, "_"));
|
||||
const normalizedType = computed(() => (props.dbType || "").toLowerCase().replace(/[\s-]+/g, "_"));
|
||||
const assetName = computed(() => assetIcons[normalizedType.value]);
|
||||
const useLightIconInDarkMode = computed(() => normalizedType.value === "easysearch" && isDark.value);
|
||||
const assetSrc = computed(() => {
|
||||
|
||||
@@ -103,6 +103,7 @@ const supportsExplain = computed(() => {
|
||||
dbType !== "mongodb" &&
|
||||
dbType !== "elasticsearch" &&
|
||||
dbType !== "easysearch" &&
|
||||
dbType !== "meilisearch" &&
|
||||
dbType !== "qdrant" &&
|
||||
dbType !== "milvus" &&
|
||||
dbType !== "weaviate" &&
|
||||
@@ -486,6 +487,7 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
v-if="
|
||||
activeConnection?.db_type !== 'elasticsearch' &&
|
||||
activeConnection?.db_type !== 'easysearch' &&
|
||||
activeConnection?.db_type !== 'meilisearch' &&
|
||||
activeConnection?.db_type !== 'qdrant' &&
|
||||
activeConnection?.db_type !== 'milvus' &&
|
||||
activeConnection?.db_type !== 'weaviate' &&
|
||||
|
||||
@@ -1161,7 +1161,7 @@ async function ensureTreeLoadedForTarget(target: ActiveTabSidebarTarget, opts?:
|
||||
await store.loadRedisDatabases(connId);
|
||||
} else if (config.db_type === "mongodb") {
|
||||
await store.loadMongoDatabases(connId);
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch") {
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "meilisearch") {
|
||||
await store.loadElasticsearchIndices(connId);
|
||||
} else if (config.db_type === "qdrant" || config.db_type === "milvus" || config.db_type === "weaviate" || config.db_type === "chromadb") {
|
||||
await store.loadVectorCollections(connId);
|
||||
|
||||
@@ -676,7 +676,7 @@ async function toggle() {
|
||||
await connectionStore.loadConsulRoot(node.connectionId);
|
||||
} else if (config?.db_type === "mongodb") {
|
||||
await connectionStore.loadMongoDatabases(node.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch") {
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
// Expand: list indices (like other db types list databases).
|
||||
await connectionStore.loadElasticsearchIndices(node.connectionId);
|
||||
} else if (config?.db_type === "milvus") {
|
||||
@@ -3534,7 +3534,7 @@ const canOpenSqlFileExecution = computed(() => {
|
||||
const canExportAllDatabases = computed(() => {
|
||||
if (activeNode.value.type !== "connection" || !activeNode.value.connectionId) return false;
|
||||
const dbType = connectionStore.getConfig(activeNode.value.connectionId)?.db_type;
|
||||
return !["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(dbType || "");
|
||||
return !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(dbType || "");
|
||||
});
|
||||
|
||||
const canOpenDiagram = computed(() => {
|
||||
|
||||
@@ -113,7 +113,7 @@ function resetPerFileState() {
|
||||
currentFileName.value = "";
|
||||
}
|
||||
|
||||
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
const sqlConnections = computed(() => store.connections.filter((c) => !["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(c.db_type)));
|
||||
|
||||
const selectedConnection = computed(() => sqlConnections.value.find((c) => c.id === connectionId.value));
|
||||
|
||||
|
||||
@@ -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"] });
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { documentFilterModeOptionsFor, documentStoreProviderFor } from "@/lib/app/documentStoreProvider";
|
||||
|
||||
describe("Meilisearch document store provider", () => {
|
||||
it("uses a dedicated provider and describes the DBX fetch operation", () => {
|
||||
const provider = documentStoreProviderFor("meilisearch");
|
||||
|
||||
expect(provider.kind).toBe("meilisearch");
|
||||
expect(
|
||||
provider.queryPreview({
|
||||
collection: "movies",
|
||||
filterJson: '{"status":"published"}',
|
||||
sortJson: '{"rating":-1}',
|
||||
skip: 20,
|
||||
limit: 10,
|
||||
}),
|
||||
).toBe('DBX MEILISEARCH FETCH DOCUMENTS\nindex: "movies"\noffset: 20\nlimit: 10\nfilter:\n{\n "status": "published"\n}\nsort:\n{\n "rating": -1\n}');
|
||||
});
|
||||
|
||||
it("does not offer structured contains modes by default", () => {
|
||||
expect(documentFilterModeOptionsFor("meilisearch").map((option) => option.value)).not.toEqual(expect.arrayContaining(["like", "not-like"]));
|
||||
expect(documentFilterModeOptionsFor("elasticsearch").map((option) => option.value)).toEqual(expect.arrayContaining(["like", "not-like"]));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { serializeDocumentStoreId } from "@/lib/app/documentJsonValues";
|
||||
import { applyDocumentStoreIdentityPlan, formatMeilisearchDocumentOperationPreview } from "@/lib/app/documentStoreSave";
|
||||
import { parseJsonPreservingLargeNumbers } from "@/lib/common/safeJsonFormat";
|
||||
|
||||
describe("Meilisearch document saves", () => {
|
||||
it("preserves the distinction between string and numeric document ids", () => {
|
||||
expect(serializeDocumentStoreId("001", "meilisearch")).toBe('__dbx_meilisearch_string_id__"001"');
|
||||
expect(serializeDocumentStoreId(1, "meilisearch")).toBe("1");
|
||||
expect(serializeDocumentStoreId(parseJsonPreservingLargeNumbers("9007199254740993"), "meilisearch")).toBe("9007199254740993");
|
||||
});
|
||||
|
||||
it("describes DBX write semantics without inventing a native primary-key field", () => {
|
||||
const preview = formatMeilisearchDocumentOperationPreview({
|
||||
action: "update",
|
||||
index: "movies",
|
||||
id: "001",
|
||||
document: { title: "Arrival", rating: 9 },
|
||||
});
|
||||
|
||||
expect(preview).toContain("DBX MEILISEARCH UPDATE DOCUMENT");
|
||||
expect(preview).toContain('index: "movies"');
|
||||
expect(preview).toContain('id: "001"');
|
||||
expect(preview).toContain('"title": "Arrival"');
|
||||
expect(preview).not.toContain("PUT /indexes/");
|
||||
});
|
||||
|
||||
it("writes the new identity before deleting the old document", async () => {
|
||||
const calls: string[] = [];
|
||||
const update = vi.fn(async (id: string) => {
|
||||
calls.push(`update:${id}`);
|
||||
return 1;
|
||||
});
|
||||
const remove = vi.fn(async (id: string) => {
|
||||
calls.push(`delete:${id}`);
|
||||
return 1;
|
||||
});
|
||||
|
||||
await applyDocumentStoreIdentityPlan({
|
||||
kind: "meilisearch",
|
||||
plan: {
|
||||
action: "rekey",
|
||||
writeId: serializeDocumentStoreId("002", "meilisearch"),
|
||||
deleteId: serializeDocumentStoreId("001", "meilisearch"),
|
||||
},
|
||||
document: { _id: "002", title: "Arrival" },
|
||||
apis: {
|
||||
insert: vi.fn(async () => ""),
|
||||
update,
|
||||
delete: remove,
|
||||
},
|
||||
});
|
||||
|
||||
expect(calls).toEqual(['update:__dbx_meilisearch_string_id__"002"', 'delete:__dbx_meilisearch_string_id__"001"']);
|
||||
expect(update).toHaveBeenCalledWith('__dbx_meilisearch_string_id__"002"', '{"title":"Arrival"}', undefined);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { connectionProfileForScheme, parseConnectionUrl } from "@/lib/connection/connectionUrl";
|
||||
|
||||
describe("Meilisearch connection URLs", () => {
|
||||
it("parses the dedicated scheme with the default port and API key", () => {
|
||||
expect(parseConnectionUrl("meilisearch://:secret@search.example.com")).toMatchObject({
|
||||
dbType: "meilisearch",
|
||||
driverProfile: "meilisearch",
|
||||
driverLabel: "Meilisearch",
|
||||
host: "search.example.com",
|
||||
port: 7700,
|
||||
username: "",
|
||||
password: "secret",
|
||||
ssl: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps Meilisearch selected for HTTPS URLs", () => {
|
||||
expect(parseConnectionUrl("https://search.example.com:8443?insecure=true", "meilisearch")).toMatchObject({
|
||||
dbType: "meilisearch",
|
||||
driverProfile: "meilisearch",
|
||||
port: 8443,
|
||||
urlParams: "insecure=true",
|
||||
ssl: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("exposes the Meilisearch scheme profile", () => {
|
||||
expect(connectionProfileForScheme("meilisearch")).toEqual({
|
||||
type: "meilisearch",
|
||||
profile: "meilisearch",
|
||||
label: "Meilisearch",
|
||||
defaultPort: 7700,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -41,6 +41,7 @@ describe("dataGridColumnNameCopy", () => {
|
||||
expect(supportsColumnNameQuoting("redis")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("elasticsearch")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("easysearch")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("meilisearch")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("qdrant")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("jdbc")).toBe(false);
|
||||
expect(supportsColumnNameQuoting("iotdb")).toBe(false);
|
||||
|
||||
@@ -6,6 +6,7 @@ describe("dataGridPreviewLabelKey", () => {
|
||||
expect(dataGridPreviewLabelKey("mongodb")).toBe("toolbar.previewQuery");
|
||||
expect(dataGridPreviewLabelKey("elasticsearch")).toBe("toolbar.previewQuery");
|
||||
expect(dataGridPreviewLabelKey("easysearch")).toBe("toolbar.previewQuery");
|
||||
expect(dataGridPreviewLabelKey("meilisearch")).toBe("toolbar.previewQuery");
|
||||
});
|
||||
|
||||
it("keeps the SQL wording for SQL databases", () => {
|
||||
|
||||
@@ -4,7 +4,9 @@ import { databaseRuntimeMode, usesAgentCursorForQuery } from "@/lib/database/dat
|
||||
describe("databaseDriverManifest", () => {
|
||||
it("uses agent cursor only for agent or external runtimes", () => {
|
||||
expect(databaseRuntimeMode("gaussdb")).toBe("native");
|
||||
expect(databaseRuntimeMode("meilisearch")).toBe("native");
|
||||
expect(usesAgentCursorForQuery("gaussdb")).toBe(false);
|
||||
expect(usesAgentCursorForQuery("meilisearch")).toBe(false);
|
||||
expect(usesAgentCursorForQuery("mysql")).toBe(false);
|
||||
expect(usesAgentCursorForQuery("jdbc")).toBe(true);
|
||||
expect(usesAgentCursorForQuery("prestosql")).toBe(true);
|
||||
|
||||
@@ -107,6 +107,7 @@ describe("supportsSqlInListPaste", () => {
|
||||
expect(supportsSqlInListPaste("mongodb")).toBe(false);
|
||||
expect(supportsSqlInListPaste("elasticsearch")).toBe(false);
|
||||
expect(supportsSqlInListPaste("easysearch")).toBe(false);
|
||||
expect(supportsSqlInListPaste("meilisearch")).toBe(false);
|
||||
expect(supportsSqlInListPaste("qdrant")).toBe(false);
|
||||
expect(supportsSqlInListPaste("milvus")).toBe(false);
|
||||
expect(supportsSqlInListPaste("weaviate")).toBe(false);
|
||||
|
||||
@@ -474,7 +474,7 @@ POST /orders/_search
|
||||
|
||||
HEAD /orders`;
|
||||
|
||||
for (const databaseType of ["elasticsearch", "easysearch"] as const) {
|
||||
for (const databaseType of ["elasticsearch", "easysearch", "meilisearch"] as const) {
|
||||
expect(rangeSqlTexts(splitSqlStatementRanges(sql, databaseType))).toEqual(["GET /_nodes/stats/jvm?pretty", 'POST /orders/_search\n{\n "query": { "match_all": {} }\n}', "HEAD /orders"]);
|
||||
expect(hasMultipleExecutionTargets(sql, databaseType)).toBe(true);
|
||||
}
|
||||
@@ -1447,6 +1447,7 @@ describe("supportsExecutionTargetPicker", () => {
|
||||
expect(supportsExecutionTargetPicker("mongodb")).toBe(false);
|
||||
expect(supportsExecutionTargetPicker("elasticsearch")).toBe(true);
|
||||
expect(supportsExecutionTargetPicker("easysearch")).toBe(true);
|
||||
expect(supportsExecutionTargetPicker("meilisearch")).toBe(true);
|
||||
expect(supportsExecutionTargetPicker("qdrant")).toBe(false);
|
||||
expect(supportsExecutionTargetPicker("milvus")).toBe(false);
|
||||
expect(supportsExecutionTargetPicker("weaviate")).toBe(false);
|
||||
|
||||
@@ -29,17 +29,20 @@ export function parseDocumentStoreInputValue(raw: MongoInputValue, kind: Documen
|
||||
}
|
||||
|
||||
export function stringifyDocumentStoreValue(value: unknown, kind: DocumentStoreKind, indent?: number): string {
|
||||
return kind === "elasticsearch" ? stringifyJsonPreservingLargeNumbers(value, indent) : JSON.stringify(value, null, indent ?? undefined);
|
||||
return kind === "mongodb" ? JSON.stringify(value, null, indent ?? undefined) : stringifyJsonPreservingLargeNumbers(value, indent);
|
||||
}
|
||||
|
||||
export function documentStoreValueForGrid(value: unknown, kind: DocumentStoreKind): MongoInputValue {
|
||||
if (kind === "elasticsearch" && isLosslessJsonNumber(value)) return value.raw;
|
||||
if (kind !== "mongodb" && isLosslessJsonNumber(value)) return value.raw;
|
||||
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
||||
return stringifyDocumentStoreValue(value, kind);
|
||||
}
|
||||
|
||||
export function serializeDocumentStoreId(value: unknown, kind: DocumentStoreKind): string {
|
||||
return kind === "elasticsearch" ? String(value) : serializeMongoDocumentId(value);
|
||||
if (kind !== "mongodb" && isLosslessJsonNumber(value)) return value.raw;
|
||||
if (kind === "elasticsearch") return String(value);
|
||||
if (kind === "meilisearch") return typeof value === "string" ? `__dbx_meilisearch_string_id__${JSON.stringify(value)}` : String(value);
|
||||
return serializeMongoDocumentId(value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ComposerTranslation } from "vue-i18n";
|
||||
import { normalizeJsonArgument } from "@dbx-app/mongo-shell";
|
||||
import { isElasticsearchCompatibleDatabaseType, type DatabaseType } from "@/types/database";
|
||||
import { isElasticsearchCompatibleDatabaseType, isMeilisearchDatabaseType, type DatabaseType } from "@/types/database";
|
||||
import { quoteUnquotedObjectKeys } from "@/lib/mongo/mongoShellCommand";
|
||||
import { formatMongoShellLiteral } from "@/lib/mongo/mongoDocumentValues";
|
||||
|
||||
export type DocumentStoreKind = "mongodb" | "elasticsearch";
|
||||
export type DocumentStoreKind = "mongodb" | "elasticsearch" | "meilisearch";
|
||||
export type DocumentFilterMode = "equals" | "not-equals" | "like" | "not-like" | "greater-than" | "greater-than-or-equal" | "less-than" | "less-than-or-equal" | "is-null" | "is-not-null";
|
||||
export type DocumentFilterValueType = "auto" | "string" | "number" | "boolean" | "object-id" | "date" | "int32" | "int64" | "decimal128" | "json";
|
||||
export type ElasticsearchBoolClause = "filter" | "must" | "should" | "must_not";
|
||||
@@ -64,6 +64,10 @@ export const documentFilterModeOptions: Array<{ value: DocumentFilterMode; label
|
||||
{ value: "is-not-null", labelKey: "grid.filterBuilderIsNotNull" },
|
||||
];
|
||||
|
||||
export function documentFilterModeOptionsFor(kind: DocumentStoreKind): Array<{ value: DocumentFilterMode; labelKey: string }> {
|
||||
return kind === "meilisearch" ? documentFilterModeOptions.filter((option) => option.value !== "like" && option.value !== "not-like") : documentFilterModeOptions;
|
||||
}
|
||||
|
||||
export const documentFilterValueTypeOptions: Array<{ value: DocumentFilterValueType; labelKey: string }> = [
|
||||
{ value: "auto", labelKey: "grid.filterBuilderValueTypeAuto" },
|
||||
{ value: "string", labelKey: "grid.filterBuilderValueTypeString" },
|
||||
@@ -130,8 +134,36 @@ const elasticsearchDocumentProvider: DocumentStoreProvider = {
|
||||
sortInputForColumn: mongoDocumentProvider.sortInputForColumn,
|
||||
};
|
||||
|
||||
const meilisearchDocumentProvider: DocumentStoreProvider = {
|
||||
kind: "meilisearch",
|
||||
filterInputLabel: "filter",
|
||||
sortInputLabel: "sort",
|
||||
documentsLabel: ({ total }) => `${total} Documents`,
|
||||
queryPreview: ({ collection, filterJson, sortJson, skip, limit }) => {
|
||||
const lines = ["DBX MEILISEARCH FETCH DOCUMENTS", `index: ${JSON.stringify(collection)}`, `offset: ${skip}`, `limit: ${limit}`];
|
||||
const filter = documentStorePreviewJson(filterJson);
|
||||
if (filter) lines.push("filter:", filter);
|
||||
const sort = documentStorePreviewJson(sortJson);
|
||||
if (sort) lines.push("sort:", sort);
|
||||
return lines.join("\n");
|
||||
},
|
||||
sortInputForColumn: mongoDocumentProvider.sortInputForColumn,
|
||||
};
|
||||
|
||||
function documentStorePreviewJson(value?: string): string | null {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed || trimmed === "{}") return null;
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
export function documentStoreProviderFor(databaseType?: DatabaseType): DocumentStoreProvider {
|
||||
return isElasticsearchCompatibleDatabaseType(databaseType) ? elasticsearchDocumentProvider : mongoDocumentProvider;
|
||||
if (isElasticsearchCompatibleDatabaseType(databaseType)) return elasticsearchDocumentProvider;
|
||||
if (isMeilisearchDatabaseType(databaseType)) return meilisearchDocumentProvider;
|
||||
return mongoDocumentProvider;
|
||||
}
|
||||
|
||||
export function defaultDocumentFilterRule(id: string, fieldName = ""): DocumentFilterRule {
|
||||
@@ -639,6 +671,7 @@ type DocumentQueryInputNormalizer = (input: string) => string | null;
|
||||
const documentQueryInputNormalizers: Record<DocumentStoreKind, DocumentQueryInputNormalizer> = {
|
||||
mongodb: normalizeJsonArgument,
|
||||
elasticsearch: quoteUnquotedObjectKeys,
|
||||
meilisearch: quoteUnquotedObjectKeys,
|
||||
};
|
||||
|
||||
function normalizeDocumentQueryObjectInput(input: string, kind?: DocumentStoreKind): string {
|
||||
|
||||
@@ -7,6 +7,13 @@ export type DocumentStoreWriteApis = {
|
||||
delete: (id: string, routing?: string) => Promise<number>;
|
||||
};
|
||||
|
||||
export function formatMeilisearchDocumentOperationPreview(options: { action: "insert" | "upsert" | "update" | "delete"; index: string; id?: unknown; document?: Record<string, unknown> }): string {
|
||||
const lines = [`DBX MEILISEARCH ${options.action.toUpperCase()} DOCUMENT`, `index: ${JSON.stringify(options.index)}`];
|
||||
if (options.id !== undefined) lines.push(`id: ${stringifyDocumentStoreValue(options.id, "meilisearch")}`);
|
||||
if (options.document) lines.push("document:", stringifyDocumentStoreValue(options.document, "meilisearch", 2));
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a document body under a known identity.
|
||||
* - `put`: Elasticsearch index-by-id / Mongo update-by-id (identity via path, not body).
|
||||
@@ -48,8 +55,8 @@ export async function applyDocumentStoreIdentityPlan(options: { kind: DocumentSt
|
||||
return;
|
||||
}
|
||||
|
||||
// Rekey write: ES uses put under the new id/routing; Mongo inserts a new document then deletes the old id.
|
||||
if (kind === "elasticsearch") {
|
||||
// Path-identity stores write under the new id first; Mongo inserts a new document.
|
||||
if (kind !== "mongodb") {
|
||||
await writeDocumentStoreDocument({
|
||||
kind,
|
||||
op: "put",
|
||||
@@ -71,10 +78,10 @@ export async function applyDocumentStoreIdentityPlan(options: { kind: DocumentSt
|
||||
await apis.delete(plan.deleteId, plan.deleteRouting);
|
||||
}
|
||||
|
||||
/** Insert a new document (optional explicit ES id uses put). */
|
||||
/** Insert a new document (optional explicit path identity uses put). */
|
||||
export async function insertDocumentStoreDocument(options: { kind: DocumentStoreKind; document: Record<string, unknown>; explicitId?: string | null; routing?: string; apis: Pick<DocumentStoreWriteApis, "insert" | "update"> }): Promise<void> {
|
||||
const { kind, document, explicitId, routing, apis } = options;
|
||||
if (kind === "elasticsearch" && explicitId) {
|
||||
if (kind !== "mongodb" && explicitId) {
|
||||
await writeDocumentStoreDocument({
|
||||
kind,
|
||||
op: "put",
|
||||
|
||||
@@ -699,6 +699,7 @@ export const documentUpdateDocument = forward("documentUpdateDocument");
|
||||
export const mongoUpdateDocument = forward("mongoUpdateDocument");
|
||||
export const mongoUpdateDocuments = forward("mongoUpdateDocuments");
|
||||
export const documentDeleteDocument = forward("documentDeleteDocument");
|
||||
export const documentSaveMeilisearchBatch = forward("documentSaveMeilisearchBatch");
|
||||
export const mongoDeleteDocument = forward("mongoDeleteDocument");
|
||||
export const mongoDeleteDocuments = forward("mongoDeleteDocuments");
|
||||
export const mongoFindOneAndUpdate = forward("mongoFindOneAndUpdate");
|
||||
|
||||
@@ -3668,6 +3668,16 @@ export async function documentDeleteDocument(connectionId: string, database: str
|
||||
});
|
||||
}
|
||||
|
||||
export async function documentSaveMeilisearchBatch(connectionId: string, collection: string, updates: Array<{ id: string; docJson: string }>, deleteIds: string[], inserts: string[]): Promise<number> {
|
||||
return post("/api/document-store/save-meilisearch-batch", {
|
||||
connectionId,
|
||||
collection,
|
||||
updates,
|
||||
deleteIds,
|
||||
inserts,
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoDeleteDocuments(connectionId: string, database: string, collection: string, filterJson: string, many: boolean): Promise<{ affected_rows: number }> {
|
||||
return post("/api/mongo/delete-documents", {
|
||||
connectionId,
|
||||
|
||||
@@ -3615,6 +3615,16 @@ export async function documentDeleteDocument(connectionId: string, database: str
|
||||
});
|
||||
}
|
||||
|
||||
export async function documentSaveMeilisearchBatch(connectionId: string, collection: string, updates: Array<{ id: string; docJson: string }>, deleteIds: string[], inserts: string[]): Promise<number> {
|
||||
return invoke("document_save_meilisearch_batch", {
|
||||
connectionId,
|
||||
collection,
|
||||
updates,
|
||||
deleteIds,
|
||||
inserts,
|
||||
});
|
||||
}
|
||||
|
||||
export async function mongoDeleteDocuments(connectionId: string, database: string, collection: string, filterJson: string, many: boolean): Promise<{ affected_rows: number }> {
|
||||
const affectedRows = await invoke<number>("mongo_delete_documents", {
|
||||
connectionId,
|
||||
|
||||
@@ -129,6 +129,7 @@ export function connectionDisplayUrlScheme(connection: Pick<ConnectionConfig, "d
|
||||
return "mssql";
|
||||
case "elasticsearch":
|
||||
case "easysearch":
|
||||
case "meilisearch":
|
||||
case "qdrant":
|
||||
case "milvus":
|
||||
case "weaviate":
|
||||
@@ -207,6 +208,7 @@ export function connectionUrlPlaceholder(dbType: DatabaseType): string {
|
||||
|
||||
case "elasticsearch":
|
||||
case "easysearch":
|
||||
case "meilisearch":
|
||||
case "qdrant":
|
||||
case "milvus":
|
||||
case "weaviate":
|
||||
|
||||
@@ -53,6 +53,7 @@ const SCHEME_PROFILES: Record<string, ConnectionProfile> = {
|
||||
oracle: { type: "oracle", profile: "oracle", label: "Oracle", defaultPort: 1521 },
|
||||
elasticsearch: { type: "elasticsearch", profile: "elasticsearch", label: "Elasticsearch", defaultPort: 9200 },
|
||||
easysearch: { type: "easysearch", profile: "easysearch", label: "Easysearch", defaultPort: 9200 },
|
||||
meilisearch: { type: "meilisearch", profile: "meilisearch", label: "Meilisearch", defaultPort: 7700 },
|
||||
qdrant: { type: "qdrant", profile: "qdrant", label: "Qdrant", defaultPort: 6333 },
|
||||
milvus: { type: "milvus", profile: "milvus", label: "Milvus", defaultPort: 19530 },
|
||||
weaviate: { type: "weaviate", profile: "weaviate", label: "Weaviate", defaultPort: 8080 },
|
||||
@@ -82,6 +83,7 @@ const HTTP_SELECTED_PROFILES: Record<string, ConnectionProfile> = {
|
||||
clickhouse: SCHEME_PROFILES.clickhouse,
|
||||
elasticsearch: SCHEME_PROFILES.elasticsearch,
|
||||
easysearch: SCHEME_PROFILES.easysearch,
|
||||
meilisearch: SCHEME_PROFILES.meilisearch,
|
||||
qdrant: SCHEME_PROFILES.qdrant,
|
||||
milvus: SCHEME_PROFILES.milvus,
|
||||
weaviate: SCHEME_PROFILES.weaviate,
|
||||
|
||||
@@ -5,7 +5,7 @@ const DRAFT_VISIBLE_DATABASES_PREFIX = "__visible_draft_";
|
||||
|
||||
// Turso and Cloudflare D1 target one fixed SQLite-compatible `main` namespace;
|
||||
// non-database services expose their own root objects rather than database namespaces.
|
||||
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos", "consul"]);
|
||||
const UNSUPPORTED_VISIBLE_DATABASE_TYPES = new Set<DatabaseType>(["turso", "cloudflare-d1", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "mq", "nacos", "consul"]);
|
||||
|
||||
type VisibleDatabaseConnectionFields = Pick<
|
||||
ConnectionConfig,
|
||||
|
||||
@@ -25,7 +25,7 @@ export function isColumnNameCopySeparator(value: unknown): value is ColumnNameCo
|
||||
}
|
||||
|
||||
// SQL 执行能力是产品能力,不代表数据库支持 SQL 标识符引用。
|
||||
const NON_SQL_DATABASE_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos", "victoriametrics"]);
|
||||
const NON_SQL_DATABASE_TYPES = new Set<DatabaseType>(["redis", "mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos", "victoriametrics"]);
|
||||
// quoteTableIdentifier 对 jdbc/iotdb 原样返回(无引用字符可用)。
|
||||
const UNQUOTABLE_DATABASE_TYPES = new Set<DatabaseType>(["jdbc", "iotdb"]);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
const NON_SQL_PREVIEW_DATABASE_TYPES: ReadonlySet<DatabaseType> = new Set<DatabaseType>(["mongodb", "elasticsearch", "easysearch"]);
|
||||
const NON_SQL_PREVIEW_DATABASE_TYPES: ReadonlySet<DatabaseType> = new Set<DatabaseType>(["mongodb", "elasticsearch", "easysearch", "meilisearch"]);
|
||||
|
||||
export function dataGridPreviewLabelKey(databaseType?: DatabaseType): "toolbar.previewQuery" | "toolbar.previewSql" {
|
||||
return databaseType && NON_SQL_PREVIEW_DATABASE_TYPES.has(databaseType) ? "toolbar.previewQuery" : "toolbar.previewSql";
|
||||
|
||||
@@ -30,6 +30,7 @@ export const DATABASE_NAMESPACE_CREATION_MATRIX = {
|
||||
oracle: { deferred: "Oracle schemas are users; database creation is not a normal connected DDL action" },
|
||||
elasticsearch: { deferred: "index creation is not modeled as database creation" },
|
||||
easysearch: { deferred: "index creation is not modeled as database creation" },
|
||||
meilisearch: { deferred: "index creation is not modeled as database creation" },
|
||||
hbase: { deferred: "namespace creation needs dedicated HBase namespace options" },
|
||||
qdrant: { deferred: "collection creation is separate from database creation" },
|
||||
milvus: { deferred: "collection/database lifecycle needs a dedicated vector workflow" },
|
||||
|
||||
@@ -30,6 +30,7 @@ export const DATABASE_PROPERTY_EDITING_MATRIX = {
|
||||
oracle: { deferred: "Oracle database properties are instance/user/tablespace administration" },
|
||||
elasticsearch: { deferred: "index settings are not database properties" },
|
||||
easysearch: { deferred: "index settings are not database properties" },
|
||||
meilisearch: { deferred: "index settings are not database properties" },
|
||||
hbase: { deferred: "namespace and table properties need a dedicated HBase workflow" },
|
||||
qdrant: { deferred: "collection settings are not database properties" },
|
||||
milvus: { deferred: "collection/database settings need a dedicated vector workflow" },
|
||||
|
||||
@@ -109,6 +109,7 @@ const SQL_EXECUTION_TARGET_REGISTRY: Partial<Record<DatabaseType, SqlExecutionTa
|
||||
// safely apply a selected index/collection/database to the actual request.
|
||||
elasticsearch: { scope: "connection" },
|
||||
easysearch: { scope: "connection" },
|
||||
meilisearch: { scope: "connection" },
|
||||
qdrant: { scope: "connection" },
|
||||
milvus: { scope: "connection" },
|
||||
weaviate: { scope: "connection" },
|
||||
|
||||
@@ -40,7 +40,7 @@ export function sortSidebarTreeChildrenForParent(parent: Pick<TreeNode, "type">,
|
||||
const regularChildren = normalized.filter((child) => child.type !== "user-admin" && child.type !== "dameng-users" && child.type !== "dameng-roles" && child.type !== "saved-sql-root");
|
||||
const withConnectionUtilityOrder = (children: TreeNode[]) => [...savedSqlNodes, ...children, ...userAdminNodes];
|
||||
|
||||
if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "qdrant" || databaseType === "milvus" || databaseType === "weaviate" || databaseType === "chromadb") {
|
||||
if (databaseType === "mongodb" || databaseType === "elasticsearch" || databaseType === "easysearch" || databaseType === "meilisearch" || databaseType === "qdrant" || databaseType === "milvus" || databaseType === "weaviate" || databaseType === "chromadb") {
|
||||
return withConnectionUtilityOrder(sortByLabel(regularChildren));
|
||||
}
|
||||
|
||||
|
||||
@@ -285,6 +285,7 @@ export function shouldRunSqlSemanticDiagnostics(sql: string, cursor: number, opt
|
||||
options.databaseType === "mongodb" ||
|
||||
options.databaseType === "elasticsearch" ||
|
||||
options.databaseType === "easysearch" ||
|
||||
options.databaseType === "meilisearch" ||
|
||||
options.databaseType === "qdrant" ||
|
||||
options.databaseType === "milvus" ||
|
||||
options.databaseType === "weaviate" ||
|
||||
@@ -304,6 +305,7 @@ export function isSqlSemanticDiagnosticInputContext(sql: string, cursor: number,
|
||||
options.databaseType === "mongodb" ||
|
||||
options.databaseType === "elasticsearch" ||
|
||||
options.databaseType === "easysearch" ||
|
||||
options.databaseType === "meilisearch" ||
|
||||
options.databaseType === "qdrant" ||
|
||||
options.databaseType === "milvus" ||
|
||||
options.databaseType === "weaviate" ||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { SqlExecutionCandidate } from "@/lib/sql/sqlExecutionTarget";
|
||||
import { cursorBelongsToTrailingStatementDelimiter } from "@/lib/sql/statementDelimiter";
|
||||
import { splitMongoCommandRanges } from "@/lib/mongo/mongoShellCommand";
|
||||
import { readSqlBracedParameterAt, type SqlParameterOptions } from "@/lib/sql/sqlParameters";
|
||||
import { isElasticsearchCompatibleDatabaseType, type DatabaseType } from "@/types/database";
|
||||
import { isElasticsearchCompatibleDatabaseType, isMeilisearchDatabaseType, type DatabaseType } from "@/types/database";
|
||||
|
||||
/**
|
||||
* A contiguous range of SQL text expressed as document offsets plus the
|
||||
@@ -14,18 +14,22 @@ export interface SqlTextRange {
|
||||
sql: string;
|
||||
}
|
||||
|
||||
const ELASTICSEARCH_REST_REQUEST = /^(?:GET|POST|PUT|DELETE|HEAD)\s+\S+/i;
|
||||
const ELASTICSEARCH_REST_REQUEST = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD)\s+\S+/i;
|
||||
|
||||
function isHttpJsonRestDatabaseType(databaseType?: DatabaseType): boolean {
|
||||
return isElasticsearchCompatibleDatabaseType(databaseType) || isMeilisearchDatabaseType(databaseType);
|
||||
}
|
||||
|
||||
export function elasticsearchRestRequestRanges(sql: string, databaseType?: DatabaseType): SqlTextRange[] {
|
||||
if (!isElasticsearchCompatibleDatabaseType(databaseType)) return [];
|
||||
if (!isHttpJsonRestDatabaseType(databaseType)) return [];
|
||||
const requests = splitSqlStatementRanges(sql, databaseType);
|
||||
return requests.length > 0 && requests.every((request) => ELASTICSEARCH_REST_REQUEST.test(request.sql)) ? requests : [];
|
||||
}
|
||||
|
||||
const NON_SQL_EXECUTION_TARGET_TYPES: ReadonlySet<DatabaseType> = new Set(["mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "neo4j", "victoriametrics"]);
|
||||
const NON_SQL_EXECUTION_TARGET_TYPES: ReadonlySet<DatabaseType> = new Set(["mongodb", "elasticsearch", "easysearch", "meilisearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "neo4j", "victoriametrics"]);
|
||||
|
||||
export function supportsExecutionTargetPicker(databaseType?: DatabaseType): boolean {
|
||||
return !!databaseType && (databaseType === "redis" || isElasticsearchCompatibleDatabaseType(databaseType) || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType));
|
||||
return !!databaseType && (databaseType === "redis" || isHttpJsonRestDatabaseType(databaseType) || !NON_SQL_EXECUTION_TARGET_TYPES.has(databaseType));
|
||||
}
|
||||
|
||||
export function hasMultipleExecutionTargets(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): boolean {
|
||||
@@ -58,11 +62,11 @@ interface ElasticsearchRequestLineCandidate {
|
||||
}
|
||||
|
||||
export interface ElasticsearchRestRequestTarget {
|
||||
method: "GET" | "POST" | "PUT" | "DELETE" | "HEAD";
|
||||
method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
|
||||
path: string;
|
||||
}
|
||||
|
||||
const ELASTICSEARCH_REST_REQUEST_LINE = /^\s*(?:GET|POST|PUT|DELETE|HEAD)\s+\S+/i;
|
||||
const ELASTICSEARCH_REST_REQUEST_LINE = /^\s*(?:GET|POST|PUT|PATCH|DELETE|HEAD)\s+\S+/i;
|
||||
|
||||
function leadingElasticsearchPreambleEnd(value: string): number {
|
||||
let offset = 0;
|
||||
@@ -145,7 +149,7 @@ function elasticsearchRequestLineCandidates(lines: ElasticsearchRequestLine[]):
|
||||
|
||||
export function parseElasticsearchRestRequestTarget(value: string): ElasticsearchRestRequestTarget | null {
|
||||
const requestLine = stripLeadingElasticsearchComments(value).split("\n", 1)[0]?.trim() ?? "";
|
||||
const match = requestLine.match(/^(GET|POST|PUT|DELETE|HEAD)\s+(\S+)/i);
|
||||
const match = requestLine.match(/^(GET|POST|PUT|PATCH|DELETE|HEAD)\s+(\S+)/i);
|
||||
if (!match) return null;
|
||||
return {
|
||||
method: match[1].toUpperCase() as ElasticsearchRestRequestTarget["method"],
|
||||
@@ -284,7 +288,7 @@ const SAP_HANA_SCRIPT_BLOCK_TERMINATORS = new Set(["IF", "FOR", "WHILE"]);
|
||||
* whitespace are excluded so editor highlights stay tight).
|
||||
*/
|
||||
export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): RawStatement[] {
|
||||
if (isElasticsearchCompatibleDatabaseType(databaseType)) {
|
||||
if (isHttpJsonRestDatabaseType(databaseType)) {
|
||||
const requests = splitElasticsearchRestRequestRanges(sql);
|
||||
if (requests) return requests;
|
||||
}
|
||||
@@ -594,7 +598,7 @@ export function statementRangeAtCursor(sql: string, cursorPos: number, databaseT
|
||||
// Cursor in indentation or inter-statement whitespace immediately before
|
||||
// the statement should still target that statement, while the returned
|
||||
// execution range remains tight around the SQL text itself.
|
||||
if (pos >= statement.hitFrom && pos < statement.from && (sql.slice(pos, statement.from).trim() === "" || (isElasticsearchCompatibleDatabaseType(databaseType) && isElasticsearchRequestPreamble(sql.slice(statement.hitFrom, statement.from))))) {
|
||||
if (pos >= statement.hitFrom && pos < statement.from && (sql.slice(pos, statement.from).trim() === "" || (isHttpJsonRestDatabaseType(databaseType) && isElasticsearchRequestPreamble(sql.slice(statement.hitFrom, statement.from))))) {
|
||||
const previous = statements[index - 1];
|
||||
if (previous && isCursorInTrailingDelimiterGap(sql, previous.to, pos)) {
|
||||
const previousSoftRanges = splitStatementRangeAtSoftStarts(sql, previous, databaseType, parameterOptions);
|
||||
|
||||
@@ -43,6 +43,12 @@ const capabilityByType: Partial<Record<DatabaseType, Partial<TableMetadataCapabi
|
||||
triggers: false,
|
||||
ddl: false,
|
||||
},
|
||||
meilisearch: {
|
||||
indexes: false,
|
||||
foreignKeys: false,
|
||||
triggers: false,
|
||||
ddl: false,
|
||||
},
|
||||
hbase: {
|
||||
indexes: false,
|
||||
foreignKeys: false,
|
||||
|
||||
@@ -1004,6 +1004,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
"mongodb-legacy": MONGO_LEGACY_DRIVER_LABEL,
|
||||
elasticsearch: "Elasticsearch",
|
||||
easysearch: "Easysearch",
|
||||
meilisearch: "Meilisearch",
|
||||
qdrant: "Qdrant",
|
||||
milvus: "Milvus",
|
||||
weaviate: "Weaviate",
|
||||
@@ -2813,7 +2814,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await loadConsulRoot(connectionId);
|
||||
} else if (config.db_type === "mongodb") {
|
||||
await loadMongoDatabases(connectionId);
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch") {
|
||||
} else if (config.db_type === "elasticsearch" || config.db_type === "easysearch" || config.db_type === "meilisearch") {
|
||||
// Reload: list indices.
|
||||
await loadElasticsearchIndices(connectionId);
|
||||
} else if (config.db_type === "milvus") {
|
||||
@@ -3385,7 +3386,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
async function loadConnectedConnectionRootForSidebarSearch(connectionId: string) {
|
||||
if (!connectedIds.value.has(connectionId)) return;
|
||||
const config = getConfig(connectionId);
|
||||
if (!config || ["redis", "etcd", "zookeeper", "consul", "mongodb", "elasticsearch", "easysearch", "milvus", "qdrant", "weaviate", "chromadb", "mq", "nacos"].includes(config.db_type)) return;
|
||||
if (!config || ["redis", "etcd", "zookeeper", "consul", "mongodb", "elasticsearch", "easysearch", "meilisearch", "milvus", "qdrant", "weaviate", "chromadb", "mq", "nacos"].includes(config.db_type)) return;
|
||||
const node = findConnectionNode(connectionId);
|
||||
if (!node || node.type !== "connection" || node.isLoading || hasConnectionMetadataChildren(node.children)) return;
|
||||
const scope = { kind: "connection-databases" as const, connectionId, driverProfile: metadataDriverProfile(config) };
|
||||
@@ -5420,7 +5421,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
await loadConsulRoot(node.connectionId);
|
||||
} else if (config?.db_type === "mongodb") {
|
||||
await loadMongoDatabases(node.connectionId);
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch") {
|
||||
} else if (config?.db_type === "elasticsearch" || config?.db_type === "easysearch" || config?.db_type === "meilisearch") {
|
||||
await loadElasticsearchIndices(node.connectionId);
|
||||
} else if (config?.db_type === "milvus") {
|
||||
await loadMilvusDatabases(node.connectionId);
|
||||
|
||||
@@ -16,6 +16,7 @@ export type DatabaseType =
|
||||
| "oracle"
|
||||
| "elasticsearch"
|
||||
| "easysearch"
|
||||
| "meilisearch"
|
||||
| "hbase"
|
||||
| "qdrant"
|
||||
| "milvus"
|
||||
@@ -78,6 +79,10 @@ export function isElasticsearchCompatibleDatabaseType(dbType?: DatabaseType): bo
|
||||
return dbType === "elasticsearch" || dbType === "easysearch";
|
||||
}
|
||||
|
||||
export function isMeilisearchDatabaseType(dbType?: DatabaseType): boolean {
|
||||
return dbType === "meilisearch";
|
||||
}
|
||||
|
||||
export interface SqlSnippet {
|
||||
id: string;
|
||||
label: string;
|
||||
|
||||
Reference in new issue
Block a user