feat(saved-query): 支持已存查询后端持久化与连接重绑

- 后端新增 saved_queries.json 仓库,保存、导入、删除和重绑统一走 Wails 方法
- 启动时导入旧 lite-db-storage 中的 savedQueries 和连接快照,成功后清理旧字段
- 新增连接指纹匹配,唯一强匹配自动重绑,歧义场景保留为未匹配
- 侧边栏新增未匹配已存查询分组,并支持手动绑定到目标连接
- 前端保存、重命名、删除查询改为后端持久化,并补充浏览器 mock
- 补充后端与前端迁移回归测试
This commit is contained in:
Syngnat
2026-06-15 12:20:57 +08:00
parent 0b9f0448c8
commit eca9601ab0
20 changed files with 1870 additions and 66 deletions

View File

@@ -8,6 +8,7 @@ import {
startSecurityUpdateFromBootstrap,
} from './secureConfigBootstrap';
import { stripLegacyPersistedConnectionById } from './legacyConnectionStorage';
import { stripLegacySavedQueries } from './savedQueryPersistence';
const legacyPayload = JSON.stringify({
state: {
@@ -536,6 +537,69 @@ describe('secureConfigBootstrap', () => {
);
});
it('does not restore legacy saved queries when security cleanup runs after saved-query cleanup', async () => {
const args = createBaseArgs();
args.storage.setItem(LEGACY_PERSIST_KEY, JSON.stringify({
state: {
connections: [
{
id: 'legacy-1',
name: 'Legacy',
config: {
id: 'legacy-1',
type: 'postgres',
host: 'db.local',
port: 5432,
user: 'postgres',
password: 'secret',
},
},
],
globalProxy: {
enabled: true,
type: 'http',
host: '127.0.0.1',
port: 8080,
user: 'ops',
password: 'proxy-secret',
},
savedQueries: [
{
id: 'saved-1',
name: 'Orders',
sql: 'select * from orders',
connectionId: 'legacy-1',
dbName: 'app',
createdAt: 100,
},
],
},
}));
await startSecurityUpdateFromBootstrap({
...args,
backend: {
StartSecurityUpdate: vi.fn().mockImplementation(async () => {
args.storage.setItem(
LEGACY_PERSIST_KEY,
stripLegacySavedQueries(args.storage.getItem(LEGACY_PERSIST_KEY)),
);
return {
overallStatus: 'completed',
summary: { total: 3, updated: 3, pending: 0, skipped: 0, failed: 0 },
issues: [],
};
}),
GetSavedConnections: vi.fn().mockResolvedValue([]),
},
});
const cleaned = JSON.parse(args.storage.getItem(LEGACY_PERSIST_KEY) || '{}');
expect(cleaned.state.savedQueries).toBeUndefined();
expect(cleaned.state.connections).toEqual([]);
expect(cleaned.state.globalProxy).toBeUndefined();
});
it('refreshes backend config and strips source-side secrets when a later round finishes as completed', async () => {
const args = createBaseArgs();