* feat(mq): implement Apache Pulsar message queue admin - Add complete MQ admin adapter layer with version detection * Pulsar adapter with multi-version API support * OAuth2 token caching and authentication * JWT token signing for Pulsar clients * Transport layer (SSH/Proxy) integration - Implement comprehensive admin operations * Tenant, namespace, topic management * Subscription and consumer/producer monitoring * Rate limits, backlog quotas, retention policies * Permissions and role-based access control * Raw admin API for advanced operations - Add desktop UI components * MQ admin console with tabbed interface * Tenant, namespace, topic panels * Subscription management and cursor operations * Monitoring panel with stats visualization * Policy configuration UI - Add web routes for MQ admin operations - Add secret store support for MQ auth credentials - Add comprehensive test coverage * fix: del local test files * fix(mq): mark message queue as unsupported mcpMode in driver manifest MQ admin connections do not use the MCP query bridge - they use a separate REST admin API. Mark mcpMode as 'unsupported' to align with the test expectations and reflect the actual architecture. Fixes CI test: driver-manifest.test.ts * fix(mq): fix TypeScript errors in MQ components - Fix typo in SubscriptionsPanel.vue: messagformatError(e)Id -> message.messageId - Use formatError() helper in RawApiPanel.vue instead of accessing e?.message - Fix undefined 'tab' reference in ConnectionTree.vue: use config.db_type instead Fixes frontend CI TypeScript check errors. * fix(mcp): add 'mq' to MCP server connection type description The MCP server's connection type description must include all database types from the driver manifest. Add 'mq' to the list to fix the test: 'add connection database type description includes every manifest database type' Fixes: packages/mcp-server/tests/driver-manifest.test.ts * fix(mq): add missing ConnectionConfig fields for Rust compilation - Add gbase_server and keepalive_interval_secs fields to all ConnectionConfig test constructors in mq/config.rs, mq/service.rs, and storage.rs - Import uses_bare_mysql_pool in connection.rs tests module Fixes compilation errors from upstream ConnectionConfig changes. * chore(ui): simplify Pulsar icon to logo-only version Remove text from Pulsar icon (127x30 -> 35x30), keeping only the wave logo symbol for a cleaner, more consistent appearance with other database icons. * fix(mq): return removed pools from drain_connection_pools --------- Co-authored-by: t8y2 <1156263951@qq.com>
361 lines
9.8 KiB
TypeScript
361 lines
9.8 KiB
TypeScript
import { strict as assert } from "node:assert";
|
|
import { test } from "vitest";
|
|
import { restoreOpenTabsState, serializeOpenTabs } from "../../apps/desktop/src/lib/openTabsPersistence.ts";
|
|
import type { QueryTab } from "../../apps/desktop/src/types/database.ts";
|
|
|
|
function queryTab(overrides: Partial<QueryTab> = {}): QueryTab {
|
|
return {
|
|
id: "tab-1",
|
|
title: "Query 1",
|
|
connectionId: "conn-1",
|
|
database: "app",
|
|
schema: "public",
|
|
sql: "select * from users",
|
|
pinned: true,
|
|
isExecuting: false,
|
|
isCancelling: false,
|
|
isExplaining: false,
|
|
mode: "query",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
test("serializes unsaved query tabs with editor context", () => {
|
|
const saved = serializeOpenTabs([queryTab()]);
|
|
|
|
assert.deepEqual(saved, [
|
|
{
|
|
id: "tab-1",
|
|
title: "Query 1",
|
|
connectionId: "conn-1",
|
|
database: "app",
|
|
schema: "public",
|
|
sql: "select * from users",
|
|
savedSqlId: undefined,
|
|
pinned: true,
|
|
mode: "query",
|
|
objectBrowser: undefined,
|
|
objectSource: undefined,
|
|
tableMeta: undefined,
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("serializes object source query tabs with save context", () => {
|
|
const saved = serializeOpenTabs([
|
|
queryTab({
|
|
title: "fn_add",
|
|
customTitle: true,
|
|
objectSource: {
|
|
schema: "public",
|
|
name: "fn_add",
|
|
objectType: "FUNCTION",
|
|
},
|
|
}),
|
|
]);
|
|
|
|
assert.deepEqual(saved[0]?.objectSource, {
|
|
schema: "public",
|
|
name: "fn_add",
|
|
objectType: "FUNCTION",
|
|
});
|
|
assert.equal(saved[0]?.customTitle, true);
|
|
});
|
|
|
|
test("serializes table tabs with reload context", () => {
|
|
const saved = serializeOpenTabs([
|
|
queryTab({
|
|
mode: "data",
|
|
lastExecutedSql: "select * from users limit 100 offset 100",
|
|
resultPageLimit: 100,
|
|
resultPageOffset: 100,
|
|
whereInput: "active = true",
|
|
orderByInput: "created_at DESC",
|
|
tableMeta: { schema: "public", tableName: "users", columns: [], primaryKeys: [] },
|
|
}),
|
|
]);
|
|
|
|
assert.deepEqual(
|
|
{
|
|
mode: saved[0]?.mode,
|
|
lastExecutedSql: saved[0]?.lastExecutedSql,
|
|
resultPageLimit: saved[0]?.resultPageLimit,
|
|
resultPageOffset: saved[0]?.resultPageOffset,
|
|
whereInput: saved[0]?.whereInput,
|
|
orderByInput: saved[0]?.orderByInput,
|
|
tableMeta: saved[0]?.tableMeta,
|
|
},
|
|
{
|
|
mode: "data",
|
|
lastExecutedSql: "select * from users limit 100 offset 100",
|
|
resultPageLimit: 100,
|
|
resultPageOffset: 100,
|
|
whereInput: "active = true",
|
|
orderByInput: "created_at DESC",
|
|
tableMeta: { schema: "public", tableName: "users", columns: [], primaryKeys: [] },
|
|
},
|
|
);
|
|
});
|
|
|
|
test("serializes MQ tabs with selected tenant context", () => {
|
|
const saved = serializeOpenTabs([
|
|
queryTab({
|
|
mode: "mq",
|
|
database: "",
|
|
mqTenant: "public",
|
|
}),
|
|
]);
|
|
|
|
assert.equal(saved[0]?.mqTenant, "public");
|
|
});
|
|
|
|
test("serializes evicted result cache handles", () => {
|
|
const saved = serializeOpenTabs([
|
|
queryTab({
|
|
resultEvicted: true,
|
|
resultCacheKey: "tab:tab-1:result",
|
|
}),
|
|
]);
|
|
|
|
assert.equal(saved[0]?.resultEvicted, true);
|
|
assert.equal(saved[0]?.resultCacheKey, "tab:tab-1:result");
|
|
});
|
|
|
|
test("does not persist table data result cache handles across restarts", () => {
|
|
const saved = serializeOpenTabs([
|
|
queryTab({
|
|
mode: "data",
|
|
resultEvicted: true,
|
|
resultCacheKey: "tab:tab-1:result",
|
|
}),
|
|
]);
|
|
|
|
assert.equal(saved[0]?.resultEvicted, undefined);
|
|
assert.equal(saved[0]?.resultCacheKey, undefined);
|
|
});
|
|
|
|
test("restores evicted result cache handles as disk-backed runtime state", () => {
|
|
const raw = JSON.stringify([queryTab({ resultEvicted: true, resultCacheKey: "tab:tab-1:result" })]);
|
|
|
|
const restored = restoreOpenTabsState(raw, "tab-1");
|
|
|
|
assert.equal(restored.tabs[0]?.resultEvicted, true);
|
|
assert.equal(restored.tabs[0]?.resultCacheKey, "tab:tab-1:result");
|
|
assert.equal(restored.tabs[0]?.resultCacheState, "disk");
|
|
});
|
|
|
|
test("serializes query result run metadata without row payloads", () => {
|
|
const saved = serializeOpenTabs([
|
|
queryTab({
|
|
activeResultRunId: "run-2",
|
|
resultRuns: [
|
|
{
|
|
id: "run-1",
|
|
title: "Run 1",
|
|
sequence: 1,
|
|
sql: "select 1",
|
|
createdAt: 100,
|
|
result: {
|
|
columns: ["id"],
|
|
rows: [[1]],
|
|
affected_rows: 0,
|
|
execution_time_ms: 1,
|
|
},
|
|
resultCacheKey: "tab:tab-1:run:run-1",
|
|
resultCacheState: "disk",
|
|
resultEvicted: true,
|
|
},
|
|
],
|
|
}),
|
|
]);
|
|
|
|
assert.deepEqual(saved[0]?.resultRuns, [
|
|
{
|
|
id: "run-1",
|
|
title: "Run 1",
|
|
sequence: 1,
|
|
sql: "select 1",
|
|
createdAt: 100,
|
|
activeResultIndex: undefined,
|
|
resultCacheKey: "tab:tab-1:run:run-1",
|
|
resultEvicted: true,
|
|
},
|
|
]);
|
|
assert.equal(JSON.stringify(saved).includes("[[1]]"), false);
|
|
assert.equal(saved[0]?.activeResultRunId, "run-2");
|
|
});
|
|
|
|
test("restores query result run metadata as disk-backed runtime state", () => {
|
|
const raw = JSON.stringify([
|
|
{
|
|
...queryTab(),
|
|
activeResultRunId: "run-1",
|
|
resultRuns: [
|
|
{
|
|
id: "run-1",
|
|
title: "Run 1",
|
|
sequence: 1,
|
|
sql: "select 1",
|
|
createdAt: 100,
|
|
resultCacheKey: "tab:tab-1:run:run-1",
|
|
resultEvicted: true,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
const restored = restoreOpenTabsState(raw, "tab-1");
|
|
|
|
assert.equal(restored.tabs[0]?.activeResultRunId, "run-1");
|
|
assert.equal(restored.tabs[0]?.resultRuns?.[0]?.id, "run-1");
|
|
assert.equal(restored.tabs[0]?.resultRuns?.[0]?.resultCacheState, "disk");
|
|
assert.equal(restored.tabs[0]?.resultRuns?.[0]?.result, undefined);
|
|
});
|
|
|
|
test("ignores legacy table data result cache handles on restore", () => {
|
|
const raw = JSON.stringify([queryTab({ mode: "data", resultEvicted: true, resultCacheKey: "tab:tab-1:result" })]);
|
|
|
|
const restored = restoreOpenTabsState(raw, "tab-1");
|
|
|
|
assert.equal(restored.tabs[0]?.resultEvicted, undefined);
|
|
assert.equal(restored.tabs[0]?.resultCacheKey, undefined);
|
|
assert.equal(restored.tabs[0]?.resultCacheState, undefined);
|
|
});
|
|
|
|
test("restores unsaved query tabs and active tab after restart", () => {
|
|
const raw = JSON.stringify([queryTab({ id: "tab-1", sql: "select 1" }), queryTab({ id: "tab-2", title: "Query 2", sql: "select 2" })]);
|
|
|
|
const restored = restoreOpenTabsState(raw, "tab-2");
|
|
|
|
assert.deepEqual(
|
|
restored.tabs.map((tab) => ({ id: tab.id, sql: tab.sql, isExecuting: tab.isExecuting })),
|
|
[
|
|
{ id: "tab-1", sql: "select 1", isExecuting: false },
|
|
{ id: "tab-2", sql: "select 2", isExecuting: false },
|
|
],
|
|
);
|
|
assert.equal(restored.activeTabId, "tab-2");
|
|
});
|
|
|
|
test("restores object source save context", () => {
|
|
const raw = JSON.stringify([
|
|
queryTab({
|
|
customTitle: true,
|
|
objectSource: {
|
|
schema: "public",
|
|
name: "fn_add",
|
|
objectType: "FUNCTION",
|
|
},
|
|
}),
|
|
]);
|
|
|
|
const restored = restoreOpenTabsState(raw, "tab-1");
|
|
|
|
assert.deepEqual(restored.tabs[0]?.objectSource, {
|
|
schema: "public",
|
|
name: "fn_add",
|
|
objectType: "FUNCTION",
|
|
});
|
|
assert.equal(restored.tabs[0]?.customTitle, true);
|
|
});
|
|
|
|
test("restores data and structure tabs with table state", () => {
|
|
const raw = JSON.stringify([
|
|
queryTab({
|
|
id: "data",
|
|
title: "public.users",
|
|
mode: "data",
|
|
sql: 'SELECT * FROM "public"."users" LIMIT 50 OFFSET 50;',
|
|
lastExecutedSql: 'SELECT * FROM "public"."users" LIMIT 50 OFFSET 50;',
|
|
resultPageLimit: 50,
|
|
resultPageOffset: 50,
|
|
whereInput: "id > 10",
|
|
orderByInput: "id DESC",
|
|
tableMeta: {
|
|
schema: "public",
|
|
tableName: "users",
|
|
columns: [
|
|
{
|
|
name: "id",
|
|
data_type: "integer",
|
|
is_nullable: false,
|
|
column_default: null,
|
|
is_primary_key: true,
|
|
extra: null,
|
|
},
|
|
],
|
|
primaryKeys: ["id"],
|
|
},
|
|
}),
|
|
queryTab({
|
|
id: "structure",
|
|
title: "Edit users",
|
|
mode: "structure",
|
|
sql: "",
|
|
structureTableName: "users",
|
|
}),
|
|
]);
|
|
|
|
const restored = restoreOpenTabsState(raw, "data");
|
|
|
|
assert.deepEqual(
|
|
restored.tabs.map((tab) => ({ id: tab.id, mode: tab.mode })),
|
|
[
|
|
{ id: "data", mode: "data" },
|
|
{ id: "structure", mode: "structure" },
|
|
],
|
|
);
|
|
assert.equal(restored.activeTabId, "data");
|
|
assert.equal(restored.tabs[0]?.tableMeta?.tableName, "users");
|
|
assert.equal(restored.tabs[0]?.resultPageLimit, 50);
|
|
assert.equal(restored.tabs[0]?.resultPageOffset, 50);
|
|
assert.equal(restored.tabs[0]?.whereInput, "id > 10");
|
|
assert.equal(restored.tabs[0]?.orderByInput, "id DESC");
|
|
assert.equal(restored.tabs[1]?.structureTableName, "users");
|
|
});
|
|
|
|
test("restores MQ tabs with selected tenant context", () => {
|
|
const raw = JSON.stringify([
|
|
queryTab({
|
|
mode: "mq",
|
|
database: "",
|
|
mqTenant: "public",
|
|
}),
|
|
]);
|
|
|
|
const restored = restoreOpenTabsState(raw, "tab-1");
|
|
|
|
assert.equal(restored.tabs[0]?.mqTenant, "public");
|
|
});
|
|
|
|
test("query-only restore keeps legacy query tabs without a mode", () => {
|
|
const raw = JSON.stringify([
|
|
{
|
|
id: "legacy",
|
|
title: "Query 1",
|
|
connectionId: "conn-1",
|
|
database: "app",
|
|
sql: "select now()",
|
|
},
|
|
queryTab({ id: "data", title: "users", mode: "data" }),
|
|
]);
|
|
|
|
const restored = restoreOpenTabsState(raw, "data", { queryOnly: true });
|
|
|
|
assert.deepEqual(
|
|
restored.tabs.map((tab) => ({ id: tab.id, mode: tab.mode })),
|
|
[{ id: "legacy", mode: "query" }],
|
|
);
|
|
assert.equal(restored.activeTabId, "legacy");
|
|
});
|
|
|
|
test("ignores invalid persisted tab payloads", () => {
|
|
const restored = restoreOpenTabsState(JSON.stringify([{ id: "missing-fields" }, queryTab()]), "missing-fields");
|
|
|
|
assert.deepEqual(
|
|
restored.tabs.map((tab) => tab.id),
|
|
["tab-1"],
|
|
);
|
|
assert.equal(restored.activeTabId, "tab-1");
|
|
});
|