feat(mongodb): improve GridFS manager filtering and sorting

* docs: add GridFS manager filter and sort design

* feat: improve GridFS manager filtering and sorting
This commit is contained in:
LSD authored and GitHub committed 2026-07-06 17:17:33 +08:00
1 parent 26647ff755
commit fa5e55a376
14 files changed
+1294 -40

No files matched your search

+4 -4
View File
@@ -1893,12 +1893,12 @@ export async function documentFindDocuments(connectionId: string, database: stri
return post("/api/document-store/find-documents", { connectionId, database, collection, skip, limit, filter, projection, sort, executionId });
}
export async function documentListGridFsFiles(connectionId: string, database: string, bucket: string): Promise<MongoGridFsFileInfo[]> {
return post("/api/document-store/list-gridfs-files", { connectionId, database, bucket });
export async function documentListGridFsFiles(connectionId: string, database: string, bucket: string, filter?: string, sort?: string): Promise<MongoGridFsFileInfo[]> {
return post("/api/document-store/list-gridfs-files", { connectionId, database, bucket, filter, sort });
}
export async function documentListGridFsBuckets(connectionId: string, database: string): Promise<MongoGridFsBucketInfo[]> {
return post("/api/document-store/list-gridfs-buckets", { connectionId, database });
export async function documentListGridFsBuckets(connectionId: string, database: string, filter?: string, sort?: string): Promise<MongoGridFsBucketInfo[]> {
return post("/api/document-store/list-gridfs-buckets", { connectionId, database, filter, sort });
}
export async function documentCreateGridFsBucket(connectionId: string, database: string, bucket: string): Promise<void> {
+4 -4
View File
@@ -1626,12 +1626,12 @@ export async function documentFindDocuments(connectionId: string, database: stri
return invoke("document_find_documents", { connectionId, database, collection, skip, limit, filter, projection, sort, executionId });
}
export async function documentListGridFsFiles(connectionId: string, database: string, bucket: string): Promise<MongoGridFsFileInfo[]> {
return invoke("document_list_gridfs_files", { connectionId, database, bucket });
export async function documentListGridFsFiles(connectionId: string, database: string, bucket: string, filter?: string, sort?: string): Promise<MongoGridFsFileInfo[]> {
return invoke("document_list_gridfs_files", { connectionId, database, bucket, filter, sort });
}
export async function documentListGridFsBuckets(connectionId: string, database: string): Promise<MongoGridFsBucketInfo[]> {
return invoke("document_list_gridfs_buckets", { connectionId, database });
export async function documentListGridFsBuckets(connectionId: string, database: string, filter?: string, sort?: string): Promise<MongoGridFsBucketInfo[]> {
return invoke("document_list_gridfs_buckets", { connectionId, database, filter, sort });
}
export async function documentCreateGridFsBucket(connectionId: string, database: string, bucket: string): Promise<void> {
@@ -0,0 +1,139 @@
import { buildDocumentFilterCondition, combineDocumentFilterConditions, defaultDocumentFilterRule, type DocumentFilterRule } from "@/lib/app/documentStoreProvider";
import { formatMongoShellLiteral } from "@/lib/mongo/mongoDocumentValues";
import { quoteUnquotedObjectKeys } from "@/lib/mongo/mongoShellCommand";
export type GridFsBucketSortField = "name" | "fileCount" | "totalBytes";
export type GridFsBucketSortDirection = "asc" | "desc";
export type GridFsSortDirectionState = "none" | "asc" | "desc";
export type GridFsFileField = "_id" | "filename" | "contentType" | "length" | "chunkSize" | "uploadDate" | "md5";
export type GridFsFileFieldDisplayOption = { fieldName: GridFsFileField; label: string; labelKey?: never } | { fieldName: GridFsFileField; labelKey: string; label?: never };
export type GridFsBucketSort = {
field: GridFsBucketSortField;
direction: GridFsBucketSortDirection;
};
export const gridFsFileFilterFieldOptions = ["_id", "filename", "contentType", "length", "chunkSize", "uploadDate", "md5"] as const satisfies readonly GridFsFileField[];
export const gridFsFileFieldDisplayOptions = [
{ fieldName: "_id", label: "ID" },
{ fieldName: "filename", labelKey: "gridfsBrowser.name" },
{ fieldName: "contentType", labelKey: "gridfsBrowser.contentType" },
{ fieldName: "length", labelKey: "gridfsBrowser.totalSize" },
{ fieldName: "chunkSize", labelKey: "gridfsBrowser.chunkSize" },
{ fieldName: "uploadDate", labelKey: "gridfsBrowser.uploadDate" },
{ fieldName: "md5", label: "MD5" },
] as const satisfies readonly GridFsFileFieldDisplayOption[];
export function gridFsFilesQueryPreview(options: { bucket: string; filterJson?: string; sortJson?: string }): string {
const parts = [`db.getCollection(${JSON.stringify(`${options.bucket}.files`)}).find(${mongoShellPreviewLiteral(options.filterJson || "{}")})`];
if (options.sortJson?.trim()) parts.push(`.sort(${mongoShellPreviewLiteral(options.sortJson)})`);
return parts.join("");
}
export function currentGridFsBucketFilter(input: string): string | undefined {
const trimmed = input.trim();
return trimmed || undefined;
}
export function createGridFsFileFilterRule(id: string): DocumentFilterRule {
return defaultDocumentFilterRule(id, gridFsFileFilterFieldOptions[0] ?? "");
}
export function gridFsFileFieldDisplayOption(fieldName: string): GridFsFileFieldDisplayOption | null {
return gridFsFileFieldDisplayOptions.find((option) => option.fieldName === fieldName) ?? null;
}
export function buildGridFsFilesStructuredFilter(rules: DocumentFilterRule[]): Record<string, unknown> | null {
const items = rules
.map((rule) => ({
rule,
condition: buildDocumentFilterCondition(rule, { kind: "mongodb" }),
}))
.filter((item): item is { rule: DocumentFilterRule; condition: Record<string, unknown> } => !!item.condition);
return combineDocumentFilterConditions(
items.map((item) => item.condition),
items.map((item) => item.rule),
);
}
export function parseGridFsBucketSort(input?: string): GridFsBucketSort | null {
const trimmed = input?.trim();
if (!trimmed) return null;
const parsed = JSON.parse(quoteUnquotedObjectKeys(trimmed)) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error("GridFS bucket sort must be a JSON object");
}
const entries = Object.entries(parsed);
if (entries.length !== 1) {
throw new Error("GridFS bucket sort must contain exactly one field");
}
const [field, rawDirection] = entries[0]!;
if (field !== "name" && field !== "fileCount" && field !== "totalBytes") {
throw new Error("Unsupported GridFS bucket sort field");
}
const direction = normalizeGridFsBucketSortDirection(rawDirection);
if (!direction) {
throw new Error("GridFS bucket sort direction must be 1, -1, 'asc', or 'desc'");
}
return { field, direction };
}
export function currentGridFsBucketSort(input: string): string | undefined {
const sort = parseGridFsBucketSort(input);
if (!sort) return undefined;
return JSON.stringify({ [sort.field]: sort.direction === "desc" ? -1 : 1 });
}
export function currentGridFsFileSortDirection(input: string, column: string): GridFsSortDirectionState {
const sort = parseSingleFieldSort(input);
if (!sort || sort.field !== column) return "none";
return sort.direction;
}
export function currentGridFsBucketSortDirection(input: string, column: GridFsBucketSortField): GridFsSortDirectionState {
try {
const sort = parseGridFsBucketSort(input);
if (!sort || sort.field !== column) return "none";
return sort.direction;
} catch {
return "none";
}
}
export function gridFsBucketSortInputForColumn(column: string, direction: GridFsBucketSortDirection | null): string {
if (!direction) return "";
if (column !== "name" && column !== "fileCount" && column !== "totalBytes") return "";
return JSON.stringify({ [column]: direction === "desc" ? -1 : 1 });
}
function normalizeGridFsBucketSortDirection(value: unknown): GridFsBucketSortDirection | null {
if (value === -1 || value === "-1" || value === "desc") return "desc";
if (value === 1 || value === "1" || value === "asc") return "asc";
return null;
}
function parseSingleFieldSort(input?: string): { field: string; direction: GridFsBucketSortDirection } | null {
const trimmed = input?.trim();
if (!trimmed) return null;
try {
const parsed = JSON.parse(quoteUnquotedObjectKeys(trimmed)) as unknown;
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
const entries = Object.entries(parsed);
if (entries.length !== 1) return null;
const [field, rawDirection] = entries[0]!;
const direction = normalizeGridFsBucketSortDirection(rawDirection);
if (!direction) return null;
return { field, direction };
} catch {
return null;
}
}
function mongoShellPreviewLiteral(json: string): string {
const trimmed = json.trim();
if (!trimmed) return "{}";
try {
return formatMongoShellLiteral(JSON.parse(quoteUnquotedObjectKeys(trimmed)));
} catch {
return trimmed;
}
}
@@ -0,0 +1,104 @@
import type { Component } from "vue";
import { Archive, Braces, Columns3, Database, Eye, FileCode, FolderClosed, FolderOpen, Key, Link, ListTree, Network, Package, Plus, ScrollText, Server, Table, TableProperties, UsersRound, Zap } from "@lucide/vue";
import type { ColumnInfo, TreeNode } from "@/types/database";
export type TreeNodeIconInfo = {
icon: Component;
colorClass: string;
};
export function getTreeNodeIconInfo(node: TreeNode): TreeNodeIconInfo | null {
switch (node.type) {
case "schema":
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-amber-500" };
case "connection":
case "database":
case "mongo-db":
return { icon: Database, colorClass: "text-yellow-500" };
case "linked-server-root":
return { icon: Network, colorClass: "text-blue-500" };
case "linked-server":
return { icon: Server, colorClass: "text-blue-400" };
case "linked-server-catalog":
case "linked-server-schema":
case "mq-tenant":
return { icon: FolderOpen, colorClass: "text-sky-400" };
case "nacos-namespace":
case "etcd-root":
return { icon: FolderOpen, colorClass: "text-sky-500" };
case "zookeeper-root":
return { icon: Database, colorClass: "text-blue-500" };
case "table":
return { icon: Table, colorClass: "text-green-500" };
case "view":
return { icon: Eye, colorClass: "text-purple-500" };
case "materialized_view":
return { icon: Eye, colorClass: "text-indigo-500" };
case "column":
return { icon: Columns3, colorClass: (node.meta as ColumnInfo | undefined)?.is_primary_key ? "text-orange-400" : "text-muted-foreground" };
case "group-columns":
return { icon: ListTree, colorClass: "text-green-400" };
case "group-indexes":
case "index":
return { icon: Key, colorClass: "text-amber-500" };
case "group-fkeys":
return { icon: Link, colorClass: "text-blue-400" };
case "fkey":
return { icon: Link, colorClass: "text-blue-300" };
case "group-triggers":
return { icon: Zap, colorClass: "text-orange-400" };
case "trigger":
return { icon: Zap, colorClass: "text-orange-300" };
case "object-browser":
return { icon: TableProperties, colorClass: "text-primary" };
case "user-admin":
return { icon: UsersRound, colorClass: "text-primary" };
case "redis-db":
return { icon: Database, colorClass: "text-red-400" };
case "mongo-gridfs":
case "mongo-buckets":
return { icon: Archive, colorClass: "text-cyan-500" };
case "mongo-bucket":
return { icon: Archive, colorClass: "text-cyan-400" };
case "mongo-collection":
return { icon: Table, colorClass: "text-green-400" };
case "vector-collection":
return { icon: TableProperties, colorClass: "text-cyan-400" };
case "elasticsearch-index":
return { icon: Table, colorClass: "text-emerald-400" };
case "procedure":
return { icon: ScrollText, colorClass: "text-blue-500" };
case "function":
return { icon: Braces, colorClass: "text-amber-500" };
case "sequence":
return { icon: ListTree, colorClass: "text-emerald-500" };
case "package":
return { icon: Package, colorClass: "text-cyan-500" };
case "package-body":
return { icon: FileCode, colorClass: "text-cyan-400" };
case "group-tables":
return { icon: Table, colorClass: "text-green-500" };
case "group-views":
return { icon: Eye, colorClass: "text-purple-500" };
case "group-materialized-views":
return { icon: Eye, colorClass: "text-indigo-500" };
case "group-procedures":
return { icon: ScrollText, colorClass: "text-blue-500" };
case "group-functions":
return { icon: Braces, colorClass: "text-amber-500" };
case "group-sequences":
return { icon: ListTree, colorClass: "text-emerald-500" };
case "group-packages":
return { icon: Package, colorClass: "text-cyan-500" };
case "group-partitions":
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-green-400" };
case "group-extensions":
return { icon: Package, colorClass: "text-violet-500" };
case "extension":
return { icon: Package, colorClass: "text-violet-400" };
case "load-more":
return { icon: Plus, colorClass: "text-primary" };
default:
return { icon: Database, colorClass: "text-muted-foreground" };
}
}