feat(xugu): expose public synonyms in an isolated scope
This commit is contained in:
1 parent
ab46f3fc72
commit
ea9a27ce11
11 files changed
+678
-39
No files matched your search
@@ -1226,7 +1226,7 @@ function findDatabaseNode(nodes: TreeNode[], connId: string, database: string):
|
||||
|
||||
function findSchemaNode(nodes: TreeNode[], connId: string, database: string, schema: string): TreeNode | null {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "schema" && node.connectionId === connId && sameTreeName(node.database, database) && sameTreeName(node.label, schema)) {
|
||||
if (node.type === "schema" && node.connectionId === connId && sameTreeName(node.database, database) && sameTreeName(node.schema || node.label, schema)) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
|
||||
@@ -73,6 +73,7 @@ import { sidebarConnectionVisibleFilterMenu } from "@/lib/sidebar/sidebarVisible
|
||||
import { objectTypesForGroupNode } from "@/lib/table/tableTree";
|
||||
import { loadSidebarObjectGroup } from "@/lib/sidebar/sidebarObjectGroupRouting";
|
||||
import { isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
|
||||
import { isXuguPublicSynonymTreeNode } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
import { mysqlObjectTemplateForGroup } from "@/lib/sidebar/mysqlObjectTemplates";
|
||||
import { buildTableDeleteTemplate, buildTableInsertTemplate, buildTableSelectTemplate, buildTableUpdateTemplate } from "@/lib/table/tableSqlTemplates";
|
||||
import { driverStoreFocusForInstallError } from "@/lib/connection/agentDriverInstallHint";
|
||||
@@ -2578,7 +2579,7 @@ const canCreateSchema = computed(() => {
|
||||
const canDropSchema = computed(() => {
|
||||
const config = activeNode.value.connectionId ? connectionStore.getConfig(activeNode.value.connectionId) : undefined;
|
||||
const dbType = effectiveDatabaseTypeForConnection(config);
|
||||
return activeNode.value.type === "schema" && !isSqlServerLinkedNode(activeNode.value) && (usesTreeSchemaMode(dbType) || dbType === "dameng") && !connectionUsesDatabaseObjectTreeMode(config);
|
||||
return activeNode.value.type === "schema" && !isXuguPublicSynonymTreeNode(config?.db_type, activeNode.value.type, activeNode.value.schema) && !isSqlServerLinkedNode(activeNode.value) && (usesTreeSchemaMode(dbType) || dbType === "dameng") && !connectionUsesDatabaseObjectTreeMode(config);
|
||||
});
|
||||
|
||||
const canEditSchemaComment = computed(() => {
|
||||
@@ -3228,6 +3229,9 @@ async function confirmCreateSchema() {
|
||||
}
|
||||
|
||||
function dropSchema() {
|
||||
const node = sidebarDangerTarget.value ?? activeNode.value;
|
||||
const config = node.connectionId ? connectionStore.getConfig(node.connectionId) : undefined;
|
||||
if (isXuguPublicSynonymTreeNode(config?.db_type, node.type, node.schema)) return;
|
||||
void refreshDropSchemaPreviewSql();
|
||||
showDropSchemaConfirm.value = true;
|
||||
}
|
||||
@@ -3236,8 +3240,9 @@ async function confirmDropSchema() {
|
||||
const node = sidebarDangerTarget.value ?? activeNode.value;
|
||||
if (!node.connectionId || !node.database) return;
|
||||
try {
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const config = connectionStore.getConfig(node.connectionId);
|
||||
if (isXuguPublicSynonymTreeNode(config?.db_type, node.type, node.schema)) return;
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
const dbType = effectiveDatabaseTypeForConnection(config);
|
||||
let dropExecutionSchema: string | undefined;
|
||||
if (dbType === "dameng") {
|
||||
@@ -4309,6 +4314,17 @@ function buildDatabaseSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
const { node, items } = context;
|
||||
// 4. Database / Schema
|
||||
if (node.type === "database" || node.type === "schema") {
|
||||
if (isXuguPublicSynonymTreeNode(currentDatabaseType(), node.type, node.schema)) {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
label: t("contextMenu.refreshChildren"),
|
||||
action: refresh,
|
||||
icon: RefreshCw,
|
||||
shortcut: shortcutRefresh,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (currentDatabaseType() === "hbase") {
|
||||
items.push({ label: t("contextMenu.copyName"), action: copyName, icon: Copy, shortcut: shortcutCopyName.value });
|
||||
if (canCreateTable.value) {
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const runtimeSource = readFileSync(fileURLToPath(new URL("../SidebarTreeRuntimeHost.vue", import.meta.url)), "utf8");
|
||||
|
||||
function functionSource(name: string): string {
|
||||
const start = runtimeSource.indexOf(`function ${name}(`);
|
||||
expect(start, `${name} not found`).toBeGreaterThan(-1);
|
||||
const next = runtimeSource.indexOf("\nfunction ", start + 1);
|
||||
return runtimeSource.slice(start, next === -1 ? undefined : next);
|
||||
}
|
||||
|
||||
describe("Xugu public synonym actions", () => {
|
||||
it("returns a read-only schema menu before default and destructive actions", () => {
|
||||
const menu = functionSource("buildDatabaseSidebarMenu");
|
||||
const readOnlyGuard = menu.indexOf("isXuguPublicSynonymTreeNode");
|
||||
|
||||
expect(readOnlyGuard).toBeGreaterThan(-1);
|
||||
expect(readOnlyGuard).toBeLessThan(menu.indexOf("contextMenu.setDefaultSchema"));
|
||||
expect(readOnlyGuard).toBeLessThan(menu.indexOf("contextMenu.dropSchema"));
|
||||
});
|
||||
|
||||
it("guards both the drop request and confirmed execution", () => {
|
||||
expect(functionSource("dropSchema")).toContain("isXuguPublicSynonymTreeNode");
|
||||
expect(functionSource("confirmDropSchema")).toContain("isXuguPublicSynonymTreeNode");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isXuguPublicSynonymScope, isXuguPublicSynonymTreeNode, XUGU_PUBLIC_SYNONYM_SCOPE, xuguSchemaDisplayName } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
|
||||
describe("Xugu public synonym scope", () => {
|
||||
it("uses an independent protocol identity", () => {
|
||||
expect(XUGU_PUBLIC_SYNONYM_SCOPE).not.toBe("GUEST");
|
||||
expect(XUGU_PUBLIC_SYNONYM_SCOPE.startsWith("\u0000")).toBe(true);
|
||||
expect(isXuguPublicSynonymScope(XUGU_PUBLIC_SYNONYM_SCOPE)).toBe(true);
|
||||
expect(isXuguPublicSynonymScope("GUEST")).toBe(false);
|
||||
expect(isXuguPublicSynonymScope("__DBX_XUGU_PUBLIC_SYNONYMS__")).toBe(false);
|
||||
});
|
||||
|
||||
it("only replaces the reserved key for display", () => {
|
||||
expect(xuguSchemaDisplayName(XUGU_PUBLIC_SYNONYM_SCOPE)).toBe("Public synonyms");
|
||||
expect(xuguSchemaDisplayName("GUEST")).toBe("GUEST");
|
||||
expect(xuguSchemaDisplayName("AppSchema")).toBe("AppSchema");
|
||||
});
|
||||
|
||||
it("identifies only the synthetic Xugu schema tree node", () => {
|
||||
expect(isXuguPublicSynonymTreeNode("xugu", "schema", XUGU_PUBLIC_SYNONYM_SCOPE)).toBe(true);
|
||||
expect(isXuguPublicSynonymTreeNode("xugu", "schema", "__DBX_XUGU_PUBLIC_SYNONYMS__")).toBe(false);
|
||||
expect(isXuguPublicSynonymTreeNode("postgres", "schema", XUGU_PUBLIC_SYNONYM_SCOPE)).toBe(false);
|
||||
expect(isXuguPublicSynonymTreeNode("xugu", "database", XUGU_PUBLIC_SYNONYM_SCOPE)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Xugu public synonyms do not belong to a user schema. The agent exposes
|
||||
* them through this reserved protocol scope so the schema tree can keep a
|
||||
* real schema named GUEST independent from database-global aliases.
|
||||
*/
|
||||
export const XUGU_PUBLIC_SYNONYM_SCOPE = "\u0000DBX_XUGU_PUBLIC_SYNONYMS";
|
||||
export const XUGU_PUBLIC_SYNONYM_SCOPE_LABEL = "Public synonyms";
|
||||
|
||||
export function isXuguPublicSynonymScope(schema: string | null | undefined): boolean {
|
||||
return schema === XUGU_PUBLIC_SYNONYM_SCOPE;
|
||||
}
|
||||
|
||||
export function xuguSchemaDisplayName(schema: string): string {
|
||||
return isXuguPublicSynonymScope(schema) ? XUGU_PUBLIC_SYNONYM_SCOPE_LABEL : schema;
|
||||
}
|
||||
|
||||
export function isXuguPublicSynonymTreeNode(databaseType: string | null | undefined, nodeType: string, schema: string | null | undefined): boolean {
|
||||
return databaseType === "xugu" && nodeType === "schema" && isXuguPublicSynonymScope(schema);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConnectionConfig, TreeNode } from "@/types/database";
|
||||
import { XUGU_PUBLIC_SYNONYM_SCOPE } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
|
||||
function installLocalStorage() {
|
||||
const data = new Map<string, string>();
|
||||
@@ -24,6 +25,18 @@ function postgresConnection(): ConnectionConfig {
|
||||
};
|
||||
}
|
||||
|
||||
function xuguConnection(): ConnectionConfig {
|
||||
return {
|
||||
...postgresConnection(),
|
||||
id: "xugu-1",
|
||||
name: "Xugu",
|
||||
db_type: "xugu",
|
||||
port: 5138,
|
||||
username: "SYSDBA",
|
||||
database: "SYSTEM",
|
||||
};
|
||||
}
|
||||
|
||||
describe("connectionStore default schema", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
@@ -86,4 +99,26 @@ describe("connectionStore default schema", () => {
|
||||
expect(schemaLabels()).toEqual(["archive", "public"]);
|
||||
expect(saveConnections).toHaveBeenLastCalledWith([expect.objectContaining({ id: connection.id, default_schema: undefined })]);
|
||||
});
|
||||
|
||||
it("does not persist the synthetic Xugu public-synonym scope as a default schema", async () => {
|
||||
const saveConnections = vi.fn().mockResolvedValue(undefined);
|
||||
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
|
||||
vi.doMock("@/lib/backend/api", () => ({
|
||||
deleteSchemaCachePrefix: vi.fn().mockResolvedValue(undefined),
|
||||
loadSchemaCache: vi.fn().mockResolvedValue(null),
|
||||
saveConnections,
|
||||
saveSchemaCache: vi.fn().mockResolvedValue(undefined),
|
||||
saveSidebarLayout: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
const { useConnectionStore } = await import("@/stores/connectionStore");
|
||||
const store = useConnectionStore();
|
||||
const connection = xuguConnection();
|
||||
store.addEphemeralConnection(connection);
|
||||
|
||||
await store.setDefaultSchema(connection.id, XUGU_PUBLIC_SYNONYM_SCOPE);
|
||||
|
||||
expect(store.getConfig(connection.id)?.default_schema).toBeUndefined();
|
||||
expect(saveConnections).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -113,6 +113,7 @@ import { appendAgentDriverUpdateHint, hasAgentDriverUpdate, hasInstalledAgentVer
|
||||
import { appendConnectionErrorHints } from "@/lib/connection/connectionErrorHints";
|
||||
import { appendVisibleDatabaseSelection } from "@/lib/connection/connectionVisibleDatabases";
|
||||
import { buildXuguTypeMemberNodes, isXuguTypeMemberContainer } from "@/lib/sidebar/xuguTypeMembers";
|
||||
import { isXuguPublicSynonymScope, xuguSchemaDisplayName, XUGU_PUBLIC_SYNONYM_SCOPE } from "@/lib/sidebar/xuguPublicSynonyms";
|
||||
import { filterNacosNamespacesForSidebar, normalizeNacosNamespacesForDisplay } from "@/lib/nacos/nacosNamespaceVisibility";
|
||||
import { buildPackageMemberNodes, markPackageNodesExpandable } from "@/lib/sidebar/packageMembers";
|
||||
import { configuredDatabaseProductName, connectionConfigFingerprint, normalizeDatabaseConnectionInfo } from "@/lib/connection/connectionDatabaseInfo";
|
||||
@@ -2623,7 +2624,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
async function setDefaultSchema(connectionId: string, schema: string) {
|
||||
const config = getConfig(connectionId);
|
||||
const defaultSchema = schema.trim();
|
||||
if (!config || !defaultSchema || config.default_schema === defaultSchema) return;
|
||||
if (!config || !defaultSchema || config.default_schema === defaultSchema || (config.db_type === "xugu" && isXuguPublicSynonymScope(defaultSchema))) return;
|
||||
await updateConnection({
|
||||
...config,
|
||||
default_schema: defaultSchema,
|
||||
@@ -3947,7 +3948,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
if (useCachedChildren(node, options, load)) return;
|
||||
const config = getConfig(connectionId);
|
||||
const showSystemSchemas = config?.show_system_schemas === true;
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, "schemas-v3");
|
||||
const cacheVersion = ownerAwareMetadataCacheVersion(config, config?.db_type === "xugu" ? "schemas-v4" : "schemas-v3");
|
||||
const cacheKey = schemaCacheKey(connectionId, database, cacheVersion, showSystemSchemas ? "show-system" : "hide-system");
|
||||
if (!options?.force) {
|
||||
const cached = await loadPersistedTreeChildren(node, cacheKey, load);
|
||||
@@ -3966,13 +3967,19 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
{ showSystemSchemas },
|
||||
),
|
||||
);
|
||||
// The public-synonym scope is a protocol namespace, not a user
|
||||
// schema. Keep it discoverable even when a visible-schema filter is
|
||||
// configured, while preserving the raw key for object routing.
|
||||
if (config?.db_type === "xugu" && schemas.some((schema) => isXuguPublicSynonymScope(schema.name))) {
|
||||
visibleSchemaNames.add(XUGU_PUBLIC_SYNONYM_SCOPE);
|
||||
}
|
||||
const children: TreeNode[] = schemas
|
||||
.filter((schema) => visibleSchemaNames.has(schema.name))
|
||||
.map((schema) => {
|
||||
const s = schema.name;
|
||||
return {
|
||||
id: `${connectionId}:${database}:${s}`,
|
||||
label: s,
|
||||
label: config?.db_type === "xugu" ? xuguSchemaDisplayName(s) : s,
|
||||
type: "schema" as const,
|
||||
connectionId,
|
||||
database,
|
||||
|
||||
Reference in new issue
Block a user