fix: 修复连接树偶发只剩用户与权限

This commit is contained in:
B022MC authored and GitHub committed 2026-06-30 17:34:13 +08:00
1 parent 95be4fb195
commit cb8989153a
3 files changed
+169 -4

No files matched your search

+40 -4
View File
@@ -597,7 +597,30 @@ export const useConnectionStore = defineStore("connection", () => {
return pinnedTreeNodeIds.value.has(id); return pinnedTreeNodeIds.value.has(id);
} }
function isConnectionUtilityNode(node: TreeNode): boolean {
return node.type === "user-admin";
}
function connectionMetadataChildren(children: TreeNode[] | undefined): TreeNode[] {
return (children || []).filter((child) => !isConnectionUtilityNode(child));
}
function hasConnectionMetadataChildren(children: TreeNode[] | undefined): boolean {
return connectionMetadataChildren(children).length > 0;
}
function preserveExistingConnectionMetadataChildren(parent: TreeNode, children: TreeNode[]): TreeNode[] {
if (parent.type !== "connection" || hasConnectionMetadataChildren(children)) return children;
const existingMetadataChildren = connectionMetadataChildren(parent.children);
const nextUtilityChildren = children.filter(isConnectionUtilityNode);
if (existingMetadataChildren.length === 0 || nextUtilityChildren.length === 0) return children;
return [...existingMetadataChildren, ...nextUtilityChildren];
}
function setChildren(parent: TreeNode, children: TreeNode[]) { function setChildren(parent: TreeNode, children: TreeNode[]) {
children = preserveExistingConnectionMetadataChildren(parent, children);
if (parent.children && parent.children.length > 0) { if (parent.children && parent.children.length > 0) {
const oldMap = new Map(parent.children.map((c) => [c.id, c] as const)); const oldMap = new Map(parent.children.map((c) => [c.id, c] as const));
children = children.map((child) => { children = children.map((child) => {
@@ -637,7 +660,7 @@ export const useConnectionStore = defineStore("connection", () => {
} }
function withConnectionUtilityNodes(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] { function withConnectionUtilityNodes(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] {
const nonUtilityChildren = children.filter((child) => child.type !== "user-admin"); const nonUtilityChildren = connectionMetadataChildren(children);
const userAdminNode = buildUserAdminNode(connectionId, existingConnectionNode); const userAdminNode = buildUserAdminNode(connectionId, existingConnectionNode);
return [...nonUtilityChildren, userAdminNode].filter(Boolean) as TreeNode[]; return [...nonUtilityChildren, userAdminNode].filter(Boolean) as TreeNode[];
} }
@@ -865,6 +888,9 @@ export const useConnectionStore = defineStore("connection", () => {
const config = node.connectionId ? getConfig(node.connectionId) : undefined; const config = node.connectionId ? getConfig(node.connectionId) : undefined;
const cachedChildren = normalizeCataloglessDatabaseNodes(expandCachedObjectBrowserNodes(decoded.children)); const cachedChildren = normalizeCataloglessDatabaseNodes(expandCachedObjectBrowserNodes(decoded.children));
const childrenWithLinkedServers = node.type === "connection" && node.connectionId ? ensureSqlServerLinkedRootNode(node.connectionId, cachedChildren, config) : cachedChildren; const childrenWithLinkedServers = node.type === "connection" && node.connectionId ? ensureSqlServerLinkedRootNode(node.connectionId, cachedChildren, config) : cachedChildren;
if (node.type === "connection" && !hasConnectionMetadataChildren(childrenWithLinkedServers)) {
return { hit: false, isStale: false };
}
const normalizedChildren = sortSidebarTreeChildrenForParent(node, childrenWithLinkedServers, config?.db_type); const normalizedChildren = sortSidebarTreeChildrenForParent(node, childrenWithLinkedServers, config?.db_type);
setChildren(node, node.type === "connection" && node.connectionId ? withSavedSqlRoot(node.connectionId, normalizedChildren, node) : normalizedChildren); setChildren(node, node.type === "connection" && node.connectionId ? withSavedSqlRoot(node.connectionId, normalizedChildren, node) : normalizedChildren);
node.isExpanded = true; node.isExpanded = true;
@@ -875,9 +901,19 @@ export const useConnectionStore = defineStore("connection", () => {
await api.saveSchemaCache(cacheKey, encodeSchemaTreeCache(children)).catch(() => undefined); await api.saveSchemaCache(cacheKey, encodeSchemaTreeCache(children)).catch(() => undefined);
} }
async function savePersistedConnectionTreeChildren(cacheKey: string, children: TreeNode[]) {
const metadataChildren = connectionMetadataChildren(children);
if (metadataChildren.length === 0) return;
await savePersistedTreeChildren(cacheKey, metadataChildren);
}
function useCachedChildren(node: TreeNode, options?: LoadTreeOptions): boolean { function useCachedChildren(node: TreeNode, options?: LoadTreeOptions): boolean {
if (options?.force || !loadedTreeNodeChildrenIds.value.has(node.id)) return false; if (options?.force || !loadedTreeNodeChildrenIds.value.has(node.id)) return false;
if (node.type === "connection" && node.connectionId) { if (node.type === "connection" && node.connectionId) {
if (!hasConnectionMetadataChildren(node.children)) {
clearLoadedChildrenCache(node.id);
return false;
}
const normalizedChildren = sortSidebarTreeChildrenForParent(node, withSavedSqlRoot(node.connectionId, node.children || [], node), getConfig(node.connectionId)?.db_type); const normalizedChildren = sortSidebarTreeChildrenForParent(node, withSavedSqlRoot(node.connectionId, node.children || [], node), getConfig(node.connectionId)?.db_type);
setChildren(node, normalizedChildren); setChildren(node, normalizedChildren);
} }
@@ -1389,7 +1425,7 @@ export const useConnectionStore = defineStore("connection", () => {
const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, databases, schemas), node); const children = withSavedSqlRoot(connectionId, buildDuckDbConnectionTreeNodes(connectionId, databases, schemas), node);
if (isSidebarSearchQueryChanged(options)) return; if (isSidebarSearchQueryChanged(options)) return;
setChildren(node, children); setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children); await savePersistedConnectionTreeChildren(cacheKey, node.children || children);
} else if (config && connectionUsesVisibleSchemaFilter(config)) { } else if (config && connectionUsesVisibleSchemaFilter(config)) {
const schemaFilterConfig = config; const schemaFilterConfig = config;
const effectiveDb = schemaFilterConfig.database || ""; const effectiveDb = schemaFilterConfig.database || "";
@@ -1415,7 +1451,7 @@ export const useConnectionStore = defineStore("connection", () => {
})); }));
if (isSidebarSearchQueryChanged(options)) return; if (isSidebarSearchQueryChanged(options)) return;
setChildren(node, withSavedSqlRoot(connectionId, schemaNodes, node)); setChildren(node, withSavedSqlRoot(connectionId, schemaNodes, node));
await savePersistedTreeChildren(cacheKey, schemaNodes); await savePersistedConnectionTreeChildren(cacheKey, node.children || schemaNodes);
} else { } else {
const cacheKey = schemaCacheKey(connectionId, "databases"); const cacheKey = schemaCacheKey(connectionId, "databases");
if (!options?.force) { if (!options?.force) {
@@ -1458,7 +1494,7 @@ export const useConnectionStore = defineStore("connection", () => {
const children = withSavedSqlRoot(connectionId, databaseNodes, node); const children = withSavedSqlRoot(connectionId, databaseNodes, node);
if (isSidebarSearchQueryChanged(options)) return; if (isSidebarSearchQueryChanged(options)) return;
setChildren(node, children); setChildren(node, children);
await savePersistedTreeChildren(cacheKey, children); await savePersistedConnectionTreeChildren(cacheKey, node.children || children);
} }
node.isExpanded = true; node.isExpanded = true;
} catch (e) { } catch (e) {
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

@@ -34,6 +34,10 @@ function conn(id: string): ConnectionConfig {
}; };
} }
function jsonResponse(value: unknown): Response {
return new Response(JSON.stringify(value), { status: 200, headers: { "Content-Type": "application/json" } });
}
test("successful disconnect clears the connection error", async () => { test("successful disconnect clears the connection error", async () => {
const restoreStorage = installMemoryStorage(); const restoreStorage = installMemoryStorage();
const originalFetch = globalThis.fetch; const originalFetch = globalThis.fetch;
@@ -368,6 +372,131 @@ test("hanging database metadata load times out and clears loading state", async
} }
}); });
test("utility-only connection cache is ignored and replaced with live metadata children", async () => {
const restoreStorage = installMemoryStorage();
const originalFetch = globalThis.fetch;
const utilityOnlyCache = {
version: 2,
cachedAt: new Date().toISOString(),
children: [
{
id: "conn-1:__user_admin",
label: "tree.userAdmin",
type: "user-admin",
connectionId: "conn-1",
database: "",
},
],
};
let listDatabasesCalls = 0;
let savedPayload: any = null;
globalThis.fetch = (async (input, init) => {
const url = String(input);
if (url.startsWith("/api/schema/cache?")) {
return jsonResponse(utilityOnlyCache);
}
if (url.startsWith("/api/schema/databases?")) {
listDatabasesCalls++;
return jsonResponse([{ name: "app" }]);
}
if (url === "/api/schema/cache" && init?.method === "POST") {
savedPayload = JSON.parse(String(init.body ?? "{}")).payload;
return jsonResponse(null);
}
return new Response("unexpected request", { status: 500 });
}) as typeof fetch;
try {
setActivePinia(createPinia());
const store = useConnectionStore();
store.addEphemeralConnection({ ...conn("conn-1"), db_type: "mysql", port: 3306 });
store.treeNodes.push({
id: "conn-1",
label: "conn-1",
type: "connection",
connectionId: "conn-1",
children: [],
});
await store.loadDatabases("conn-1");
assert.equal(listDatabasesCalls, 1);
assert.deepEqual(
store.treeNodes[0].children?.map((child) => child.type),
["database", "user-admin"],
);
assert.deepEqual(
savedPayload?.children?.map((child: any) => child.type),
["database"],
);
} finally {
globalThis.fetch = originalFetch;
restoreStorage();
}
});
test("empty root metadata does not replace existing databases with only user admin", async () => {
const restoreStorage = installMemoryStorage();
const originalFetch = globalThis.fetch;
let savedPayload: any = null;
globalThis.fetch = (async (input, init) => {
const url = String(input);
if (url.startsWith("/api/schema/cache?")) {
return jsonResponse(null);
}
if (url.startsWith("/api/schema/databases?")) {
return jsonResponse([]);
}
if (url === "/api/schema/cache" && init?.method === "POST") {
savedPayload = JSON.parse(String(init.body ?? "{}")).payload;
return jsonResponse(null);
}
return new Response("unexpected request", { status: 500 });
}) as typeof fetch;
try {
setActivePinia(createPinia());
const store = useConnectionStore();
store.addEphemeralConnection({ ...conn("conn-1"), db_type: "mysql", port: 3306 });
store.treeNodes.push({
id: "conn-1",
label: "conn-1",
type: "connection",
connectionId: "conn-1",
children: [
{
id: "conn-1:app",
label: "app",
type: "database",
connectionId: "conn-1",
database: "app",
children: [],
},
],
});
await store.loadDatabases("conn-1");
assert.deepEqual(
store.treeNodes[0].children?.map((child) => child.type),
["database", "user-admin"],
);
assert.deepEqual(
store.treeNodes[0].children?.map((child) => child.id),
["conn-1:app", "conn-1:__user_admin"],
);
assert.deepEqual(
savedPayload?.children?.map((child: any) => child.id),
["conn-1:app"],
);
} finally {
globalThis.fetch = originalFetch;
restoreStorage();
}
});
test.each([ test.each([
["redis", "loadRedisDatabases", "/api/redis/list-databases", "Redis databases"], ["redis", "loadRedisDatabases", "/api/redis/list-databases", "Redis databases"],
["mq", "loadMqTenants", "/api/mq/tenants/list", "message queue tenants"], ["mq", "loadMqTenants", "/api/mq/tenants/list", "message queue tenants"],