@lobehub/chat 1.46.7 → 1.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +2 -1
- package/src/app/(main)/discover/(detail)/provider/[slug]/features/ProviderConfig.tsx +2 -2
- package/src/app/(main)/settings/hooks/useCategory.tsx +3 -3
- package/src/app/(main)/settings/provider/(detail)/[id]/ClientMode.tsx +25 -0
- package/src/app/(main)/settings/provider/(detail)/[id]/page.tsx +2 -1
- package/src/app/(main)/settings/provider/ProviderMenu/SortProviderModal/index.tsx +0 -1
- package/src/database/client/migrations.json +11 -0
- package/src/database/repositories/tableViewer/index.test.ts +256 -0
- package/src/database/repositories/tableViewer/index.ts +251 -0
- package/src/database/server/models/aiProvider.ts +2 -2
- package/src/features/DevPanel/FloatPanel.tsx +136 -0
- package/src/features/DevPanel/PostgresViewer/DataTable/Table.tsx +157 -0
- package/src/features/DevPanel/PostgresViewer/DataTable/TableCell.tsx +34 -0
- package/src/features/DevPanel/PostgresViewer/DataTable/index.tsx +67 -0
- package/src/features/DevPanel/PostgresViewer/Schema.tsx +196 -0
- package/src/features/DevPanel/PostgresViewer/TableColumns.tsx +67 -0
- package/src/features/DevPanel/PostgresViewer/index.tsx +19 -0
- package/src/features/DevPanel/PostgresViewer/useTableColumns.ts +13 -0
- package/src/features/DevPanel/index.tsx +12 -0
- package/src/features/ModelSwitchPanel/index.tsx +4 -2
- package/src/hooks/useEnabledChatModels.ts +2 -2
- package/src/hooks/useModelContextWindowTokens.ts +2 -2
- package/src/hooks/useModelHasContextWindowToken.ts +2 -2
- package/src/hooks/useModelSupportToolUse.ts +2 -2
- package/src/hooks/useModelSupportVision.ts +2 -2
- package/src/layout/GlobalProvider/index.tsx +2 -2
- package/src/services/_auth.ts +2 -2
- package/src/services/aiModel/client.ts +60 -0
- package/src/services/aiModel/index.test.ts +10 -0
- package/src/services/aiModel/index.ts +5 -0
- package/src/services/aiModel/server.ts +47 -0
- package/src/services/aiModel/type.ts +30 -0
- package/src/services/aiProvider/client.ts +64 -0
- package/src/services/aiProvider/index.test.ts +10 -0
- package/src/services/aiProvider/index.ts +5 -0
- package/src/services/aiProvider/server.ts +43 -0
- package/src/services/aiProvider/type.ts +26 -0
- package/src/services/chat.ts +5 -5
- package/src/services/tableViewer/client.ts +16 -0
- package/src/services/tableViewer/index.ts +3 -0
- package/src/store/aiInfra/slices/aiProvider/action.ts +2 -2
- package/src/types/serverConfig.ts +6 -0
- package/src/types/tableViewer.ts +30 -0
- package/tests/utils.tsx +46 -0
- package/src/features/DebugUI/Content.tsx +0 -34
- package/src/features/DebugUI/index.tsx +0 -20
- package/src/services/aiModel.ts +0 -52
- package/src/services/aiProvider.ts +0 -47
package/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
# Changelog
|
4
4
|
|
5
|
+
## [Version 1.47.0](https://github.com/lobehub/lobe-chat/compare/v1.46.7...v1.47.0)
|
6
|
+
|
7
|
+
<sup>Released on **2025-01-17**</sup>
|
8
|
+
|
9
|
+
#### ✨ Features
|
10
|
+
|
11
|
+
- **misc**: Support new ai provider in client pglite.
|
12
|
+
|
13
|
+
<br/>
|
14
|
+
|
15
|
+
<details>
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
17
|
+
|
18
|
+
#### What's improved
|
19
|
+
|
20
|
+
- **misc**: Support new ai provider in client pglite, closes [#5488](https://github.com/lobehub/lobe-chat/issues/5488) ([08f505f](https://github.com/lobehub/lobe-chat/commit/08f505f))
|
21
|
+
|
22
|
+
</details>
|
23
|
+
|
24
|
+
<div align="right">
|
25
|
+
|
26
|
+
[](#readme-top)
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
5
30
|
### [Version 1.46.7](https://github.com/lobehub/lobe-chat/compare/v1.46.6...v1.46.7)
|
6
31
|
|
7
32
|
<sup>Released on **2025-01-17**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@lobehub/chat",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.47.0",
|
4
4
|
"description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
5
5
|
"keywords": [
|
6
6
|
"framework",
|
@@ -206,6 +206,7 @@
|
|
206
206
|
"react-layout-kit": "^1.9.1",
|
207
207
|
"react-lazy-load": "^4.0.1",
|
208
208
|
"react-pdf": "^9.2.1",
|
209
|
+
"react-rnd": "^10.4.14",
|
209
210
|
"react-scan": "^0.0.54",
|
210
211
|
"react-virtuoso": "^4.12.3",
|
211
212
|
"react-wrap-balancer": "^1.1.1",
|
@@ -10,7 +10,7 @@ import { memo } from 'react';
|
|
10
10
|
import { useTranslation } from 'react-i18next';
|
11
11
|
import { FlexboxProps } from 'react-layout-kit';
|
12
12
|
|
13
|
-
import {
|
13
|
+
import { isDeprecatedEdition } from '@/const/version';
|
14
14
|
import { DiscoverProviderItem } from '@/types/discover';
|
15
15
|
|
16
16
|
const useStyles = createStyles(({ css }) => ({
|
@@ -32,7 +32,7 @@ const ProviderConfig = memo<ProviderConfigProps>(({ data, identifier }) => {
|
|
32
32
|
|
33
33
|
const router = useRouter();
|
34
34
|
const openSettings = () => {
|
35
|
-
router.push(
|
35
|
+
router.push(isDeprecatedEdition ? '/settings/llm' : `/settings/provider/${identifier}`);
|
36
36
|
};
|
37
37
|
|
38
38
|
const icon = <Icon icon={SquareArrowOutUpRight} size={{ fontSize: 16 }} />;
|
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { Flexbox } from 'react-layout-kit';
|
8
8
|
|
9
9
|
import type { MenuProps } from '@/components/Menu';
|
10
|
-
import {
|
10
|
+
import { isDeprecatedEdition } from '@/const/version';
|
11
11
|
import { SettingsTabs } from '@/store/global/initialState';
|
12
12
|
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
13
13
|
|
@@ -53,7 +53,7 @@ export const useCategory = () => {
|
|
53
53
|
},
|
54
54
|
showLLM &&
|
55
55
|
// TODO: Remove /llm when v2.0
|
56
|
-
|
56
|
+
(isDeprecatedEdition
|
57
57
|
? {
|
58
58
|
icon: <Icon icon={Brain} />,
|
59
59
|
key: SettingsTabs.LLM,
|
@@ -71,7 +71,7 @@ export const useCategory = () => {
|
|
71
71
|
{t('tab.provider')}
|
72
72
|
</Link>
|
73
73
|
),
|
74
|
-
},
|
74
|
+
}),
|
75
75
|
|
76
76
|
enableSTT && {
|
77
77
|
icon: <Icon icon={Mic2} />,
|
@@ -0,0 +1,25 @@
|
|
1
|
+
'use client';
|
2
|
+
|
3
|
+
import { memo } from 'react';
|
4
|
+
import { Flexbox } from 'react-layout-kit';
|
5
|
+
import useSWR from 'swr';
|
6
|
+
|
7
|
+
import { aiProviderService } from '@/services/aiProvider';
|
8
|
+
|
9
|
+
import ModelList from '../../features/ModelList';
|
10
|
+
import ProviderConfig from '../../features/ProviderConfig';
|
11
|
+
|
12
|
+
const ClientMode = memo<{ id: string }>(({ id }) => {
|
13
|
+
const { data, isLoading } = useSWR('get-client-provider', () =>
|
14
|
+
aiProviderService.getAiProviderById(id),
|
15
|
+
);
|
16
|
+
|
17
|
+
return (
|
18
|
+
<Flexbox gap={24} paddingBlock={8}>
|
19
|
+
{!isLoading && data && <ProviderConfig {...data} />}
|
20
|
+
<ModelList id={id} />
|
21
|
+
</Flexbox>
|
22
|
+
);
|
23
|
+
});
|
24
|
+
|
25
|
+
export default ClientMode;
|
@@ -8,6 +8,7 @@ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
|
8
8
|
import { PagePropsWithId } from '@/types/next';
|
9
9
|
import { getUserAuth } from '@/utils/server/auth';
|
10
10
|
|
11
|
+
import ClientMode from './ClientMode';
|
11
12
|
import ProviderDetail from './index';
|
12
13
|
|
13
14
|
const Page = async (props: PagePropsWithId) => {
|
@@ -33,7 +34,7 @@ const Page = async (props: PagePropsWithId) => {
|
|
33
34
|
return <ProviderDetail {...userCard} />;
|
34
35
|
}
|
35
36
|
|
36
|
-
return <
|
37
|
+
return <ClientMode id={params.id} />;
|
37
38
|
};
|
38
39
|
|
39
40
|
export default Page;
|
@@ -282,5 +282,16 @@
|
|
282
282
|
"bps": true,
|
283
283
|
"folderMillis": 1731858381716,
|
284
284
|
"hash": "d8263bfefe296ed366379c7b7fc65195d12e6a1c0a9f1c96097ea28f2123fe50"
|
285
|
+
},
|
286
|
+
{
|
287
|
+
"sql": [
|
288
|
+
"CREATE TABLE \"ai_models\" (\n\t\"id\" varchar(150) NOT NULL,\n\t\"display_name\" varchar(200),\n\t\"description\" text,\n\t\"organization\" varchar(100),\n\t\"enabled\" boolean,\n\t\"provider_id\" varchar(64) NOT NULL,\n\t\"type\" varchar(20) DEFAULT 'chat' NOT NULL,\n\t\"sort\" integer,\n\t\"user_id\" text NOT NULL,\n\t\"pricing\" jsonb,\n\t\"parameters\" jsonb DEFAULT '{}'::jsonb,\n\t\"config\" jsonb,\n\t\"abilities\" jsonb DEFAULT '{}'::jsonb,\n\t\"context_window_tokens\" integer,\n\t\"source\" varchar(20),\n\t\"released_at\" varchar(10),\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"ai_models_id_provider_id_user_id_pk\" PRIMARY KEY(\"id\",\"provider_id\",\"user_id\")\n);\n",
|
289
|
+
"\nCREATE TABLE \"ai_providers\" (\n\t\"id\" varchar(64) NOT NULL,\n\t\"name\" text,\n\t\"user_id\" text NOT NULL,\n\t\"sort\" integer,\n\t\"enabled\" boolean,\n\t\"fetch_on_client\" boolean,\n\t\"check_model\" text,\n\t\"logo\" text,\n\t\"description\" text,\n\t\"key_vaults\" text,\n\t\"source\" varchar(20),\n\t\"settings\" jsonb,\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"ai_providers_id_user_id_pk\" PRIMARY KEY(\"id\",\"user_id\")\n);\n",
|
290
|
+
"\nALTER TABLE \"ai_models\" ADD CONSTRAINT \"ai_models_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;",
|
291
|
+
"\nALTER TABLE \"ai_providers\" ADD CONSTRAINT \"ai_providers_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;"
|
292
|
+
],
|
293
|
+
"bps": true,
|
294
|
+
"folderMillis": 1735834653361,
|
295
|
+
"hash": "845a692ceabbfc3caf252a97d3e19a213bc0c433df2689900135f9cfded2cf49"
|
285
296
|
}
|
286
297
|
]
|
@@ -0,0 +1,256 @@
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { clientDB, initializeDB } from '@/database/client/db';
|
4
|
+
|
5
|
+
import { TableViewerRepo } from './index';
|
6
|
+
|
7
|
+
const userId = 'user-table-viewer';
|
8
|
+
const repo = new TableViewerRepo(clientDB as any, userId);
|
9
|
+
|
10
|
+
// Mock database execution
|
11
|
+
const mockExecute = vi.fn();
|
12
|
+
const mockDB = {
|
13
|
+
execute: mockExecute,
|
14
|
+
};
|
15
|
+
|
16
|
+
beforeEach(async () => {
|
17
|
+
await initializeDB();
|
18
|
+
vi.clearAllMocks();
|
19
|
+
});
|
20
|
+
|
21
|
+
describe('TableViewerRepo', () => {
|
22
|
+
describe('getAllTables', () => {
|
23
|
+
it('should return all tables with counts', async () => {
|
24
|
+
const result = await repo.getAllTables();
|
25
|
+
|
26
|
+
expect(result.length).toEqual(39);
|
27
|
+
expect(result[0]).toEqual({ name: 'agents', count: 0, type: 'BASE TABLE' });
|
28
|
+
});
|
29
|
+
|
30
|
+
it('should handle custom schema', async () => {
|
31
|
+
const result = await repo.getAllTables('custom_schema');
|
32
|
+
expect(result).toBeDefined();
|
33
|
+
});
|
34
|
+
});
|
35
|
+
|
36
|
+
describe('getTableDetails', () => {
|
37
|
+
it('should return table column details', async () => {
|
38
|
+
const tableName = 'test_table';
|
39
|
+
const mockColumns = {
|
40
|
+
rows: [
|
41
|
+
{
|
42
|
+
column_name: 'id',
|
43
|
+
data_type: 'uuid',
|
44
|
+
is_nullable: 'NO',
|
45
|
+
column_default: null,
|
46
|
+
is_primary_key: true,
|
47
|
+
foreign_key: null,
|
48
|
+
},
|
49
|
+
],
|
50
|
+
};
|
51
|
+
|
52
|
+
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
53
|
+
mockExecute.mockResolvedValueOnce(mockColumns);
|
54
|
+
|
55
|
+
const result = await testRepo.getTableDetails(tableName);
|
56
|
+
|
57
|
+
expect(result).toEqual([
|
58
|
+
{
|
59
|
+
name: 'id',
|
60
|
+
type: 'uuid',
|
61
|
+
nullable: false,
|
62
|
+
defaultValue: null,
|
63
|
+
isPrimaryKey: true,
|
64
|
+
foreignKey: null,
|
65
|
+
},
|
66
|
+
]);
|
67
|
+
});
|
68
|
+
});
|
69
|
+
|
70
|
+
describe('getTableData', () => {
|
71
|
+
it('should return paginated data with filters', async () => {
|
72
|
+
const tableName = 'test_table';
|
73
|
+
const pagination = {
|
74
|
+
page: 1,
|
75
|
+
pageSize: 10,
|
76
|
+
sortBy: 'id',
|
77
|
+
sortOrder: 'desc' as const,
|
78
|
+
};
|
79
|
+
const filters = [
|
80
|
+
{
|
81
|
+
column: 'name',
|
82
|
+
operator: 'contains' as const,
|
83
|
+
value: 'test',
|
84
|
+
},
|
85
|
+
];
|
86
|
+
|
87
|
+
const mockData = {
|
88
|
+
rows: [{ id: 1, name: 'test' }],
|
89
|
+
};
|
90
|
+
const mockCount = {
|
91
|
+
rows: [{ total: 1 }],
|
92
|
+
};
|
93
|
+
|
94
|
+
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
95
|
+
mockExecute.mockResolvedValueOnce(mockData).mockResolvedValueOnce(mockCount);
|
96
|
+
|
97
|
+
const result = await testRepo.getTableData(tableName, pagination, filters);
|
98
|
+
|
99
|
+
expect(result).toEqual({
|
100
|
+
data: mockData.rows,
|
101
|
+
pagination: {
|
102
|
+
page: 1,
|
103
|
+
pageSize: 10,
|
104
|
+
total: 1,
|
105
|
+
},
|
106
|
+
});
|
107
|
+
});
|
108
|
+
});
|
109
|
+
|
110
|
+
describe('updateRow', () => {
|
111
|
+
it('should update and return row data', async () => {
|
112
|
+
const tableName = 'test_table';
|
113
|
+
const id = '123';
|
114
|
+
const primaryKeyColumn = 'id';
|
115
|
+
const data = { name: 'updated' };
|
116
|
+
|
117
|
+
const mockResult = {
|
118
|
+
rows: [{ id: '123', name: 'updated' }],
|
119
|
+
};
|
120
|
+
|
121
|
+
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
122
|
+
mockExecute.mockResolvedValueOnce(mockResult);
|
123
|
+
|
124
|
+
const result = await testRepo.updateRow(tableName, id, primaryKeyColumn, data);
|
125
|
+
|
126
|
+
expect(result).toEqual(mockResult.rows[0]);
|
127
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
128
|
+
});
|
129
|
+
});
|
130
|
+
|
131
|
+
describe('deleteRow', () => {
|
132
|
+
it('should delete a row', async () => {
|
133
|
+
const tableName = 'test_table';
|
134
|
+
const id = '123';
|
135
|
+
const primaryKeyColumn = 'id';
|
136
|
+
|
137
|
+
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
138
|
+
mockExecute.mockResolvedValueOnce({ rows: [] });
|
139
|
+
|
140
|
+
await testRepo.deleteRow(tableName, id, primaryKeyColumn);
|
141
|
+
|
142
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
143
|
+
});
|
144
|
+
});
|
145
|
+
|
146
|
+
describe('insertRow', () => {
|
147
|
+
it('should insert and return new row data', async () => {
|
148
|
+
const tableName = 'test_table';
|
149
|
+
const data = { name: 'new row' };
|
150
|
+
|
151
|
+
const mockResult = {
|
152
|
+
rows: [{ id: '123', name: 'new row' }],
|
153
|
+
};
|
154
|
+
|
155
|
+
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
156
|
+
mockExecute.mockResolvedValueOnce(mockResult);
|
157
|
+
|
158
|
+
const result = await testRepo.insertRow(tableName, data);
|
159
|
+
|
160
|
+
expect(result).toEqual(mockResult.rows[0]);
|
161
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
162
|
+
});
|
163
|
+
});
|
164
|
+
|
165
|
+
describe('getTableCount', () => {
|
166
|
+
it('should return table count', async () => {
|
167
|
+
const tableName = 'test_table';
|
168
|
+
const mockResult = {
|
169
|
+
rows: [{ total: 42 }],
|
170
|
+
};
|
171
|
+
|
172
|
+
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
173
|
+
mockExecute.mockResolvedValueOnce(mockResult);
|
174
|
+
|
175
|
+
const result = await testRepo.getTableCount(tableName);
|
176
|
+
|
177
|
+
expect(result).toBe(42);
|
178
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
179
|
+
});
|
180
|
+
});
|
181
|
+
|
182
|
+
describe('batchDelete', () => {
|
183
|
+
it('should delete multiple rows', async () => {
|
184
|
+
const tableName = 'test_table';
|
185
|
+
const ids = ['1', '2', '3'];
|
186
|
+
const primaryKeyColumn = 'id';
|
187
|
+
|
188
|
+
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
189
|
+
mockExecute.mockResolvedValueOnce({ rows: [] });
|
190
|
+
|
191
|
+
await testRepo.batchDelete(tableName, ids, primaryKeyColumn);
|
192
|
+
|
193
|
+
expect(mockExecute).toHaveBeenCalledTimes(1);
|
194
|
+
});
|
195
|
+
});
|
196
|
+
|
197
|
+
describe('exportTableData', () => {
|
198
|
+
it('should export table data with default pagination', async () => {
|
199
|
+
const tableName = 'test_table';
|
200
|
+
const mockData = {
|
201
|
+
rows: [{ id: 1, name: 'test' }],
|
202
|
+
};
|
203
|
+
const mockCount = {
|
204
|
+
rows: [{ total: 1 }],
|
205
|
+
};
|
206
|
+
|
207
|
+
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
208
|
+
mockExecute.mockResolvedValueOnce(mockData).mockResolvedValueOnce(mockCount);
|
209
|
+
|
210
|
+
const result = await testRepo.exportTableData(tableName);
|
211
|
+
|
212
|
+
expect(result).toEqual({
|
213
|
+
data: mockData.rows,
|
214
|
+
pagination: {
|
215
|
+
page: 1,
|
216
|
+
pageSize: 1000,
|
217
|
+
total: 1,
|
218
|
+
},
|
219
|
+
});
|
220
|
+
});
|
221
|
+
|
222
|
+
it('should export table data with custom pagination and filters', async () => {
|
223
|
+
const tableName = 'test_table';
|
224
|
+
const pagination = { page: 2, pageSize: 50 };
|
225
|
+
const filters = [
|
226
|
+
{
|
227
|
+
column: 'status',
|
228
|
+
operator: 'equals' as const,
|
229
|
+
value: 'active',
|
230
|
+
},
|
231
|
+
];
|
232
|
+
|
233
|
+
const mockData = {
|
234
|
+
rows: [{ id: 1, status: 'active' }],
|
235
|
+
};
|
236
|
+
const mockCount = {
|
237
|
+
rows: [{ total: 1 }],
|
238
|
+
};
|
239
|
+
|
240
|
+
mockExecute.mockResolvedValueOnce(mockData).mockResolvedValueOnce(mockCount);
|
241
|
+
|
242
|
+
const testRepo = new TableViewerRepo(mockDB as any, userId);
|
243
|
+
|
244
|
+
const result = await testRepo.exportTableData(tableName, pagination, filters);
|
245
|
+
|
246
|
+
expect(result).toEqual({
|
247
|
+
data: mockData.rows,
|
248
|
+
pagination: {
|
249
|
+
page: 2,
|
250
|
+
pageSize: 50,
|
251
|
+
total: 1,
|
252
|
+
},
|
253
|
+
});
|
254
|
+
});
|
255
|
+
});
|
256
|
+
});
|
@@ -0,0 +1,251 @@
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
2
|
+
import pMap from 'p-map';
|
3
|
+
|
4
|
+
import { LobeChatDatabase } from '@/database/type';
|
5
|
+
import {
|
6
|
+
FilterCondition,
|
7
|
+
PaginationParams,
|
8
|
+
TableBasicInfo,
|
9
|
+
TableColumnInfo,
|
10
|
+
} from '@/types/tableViewer';
|
11
|
+
|
12
|
+
export class TableViewerRepo {
|
13
|
+
private userId: string;
|
14
|
+
private db: LobeChatDatabase;
|
15
|
+
|
16
|
+
constructor(db: LobeChatDatabase, userId: string) {
|
17
|
+
this.userId = userId;
|
18
|
+
this.db = db;
|
19
|
+
}
|
20
|
+
|
21
|
+
/**
|
22
|
+
* 获取数据库中所有的表
|
23
|
+
*/
|
24
|
+
async getAllTables(schema = 'public'): Promise<TableBasicInfo[]> {
|
25
|
+
const query = sql`
|
26
|
+
SELECT
|
27
|
+
table_name as name,
|
28
|
+
table_type as type
|
29
|
+
FROM information_schema.tables
|
30
|
+
WHERE table_schema = ${schema}
|
31
|
+
ORDER BY table_name;
|
32
|
+
`;
|
33
|
+
|
34
|
+
const tables = await this.db.execute(query);
|
35
|
+
|
36
|
+
const tableNames = tables.rows.map((row) => row.name) as string[];
|
37
|
+
|
38
|
+
const counts = await pMap(tableNames, async (name) => this.getTableCount(name), {
|
39
|
+
concurrency: 10,
|
40
|
+
});
|
41
|
+
|
42
|
+
return tables.rows.map((row, index) => ({
|
43
|
+
count: counts[index],
|
44
|
+
name: row.name,
|
45
|
+
type: row.type,
|
46
|
+
})) as TableBasicInfo[];
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* 获取指定表的详细结构信息
|
51
|
+
*/
|
52
|
+
async getTableDetails(tableName: string): Promise<TableColumnInfo[]> {
|
53
|
+
const query = sql`
|
54
|
+
SELECT
|
55
|
+
c.column_name,
|
56
|
+
c.data_type,
|
57
|
+
c.is_nullable,
|
58
|
+
c.column_default,
|
59
|
+
-- 主键信息
|
60
|
+
(
|
61
|
+
SELECT true
|
62
|
+
FROM information_schema.table_constraints tc
|
63
|
+
JOIN information_schema.key_column_usage kcu
|
64
|
+
ON tc.constraint_name = kcu.constraint_name
|
65
|
+
WHERE tc.table_name = c.table_name
|
66
|
+
AND kcu.column_name = c.column_name
|
67
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
68
|
+
) is_primary_key,
|
69
|
+
-- 外键信息
|
70
|
+
(
|
71
|
+
SELECT json_build_object(
|
72
|
+
'table', ccu.table_name,
|
73
|
+
'column', ccu.column_name
|
74
|
+
)
|
75
|
+
FROM information_schema.table_constraints tc
|
76
|
+
JOIN information_schema.key_column_usage kcu
|
77
|
+
ON tc.constraint_name = kcu.constraint_name
|
78
|
+
JOIN information_schema.constraint_column_usage ccu
|
79
|
+
ON ccu.constraint_name = tc.constraint_name
|
80
|
+
WHERE tc.table_name = c.table_name
|
81
|
+
AND kcu.column_name = c.column_name
|
82
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
83
|
+
) foreign_key
|
84
|
+
FROM information_schema.columns c
|
85
|
+
WHERE c.table_name = ${tableName}
|
86
|
+
AND c.table_schema = 'public'
|
87
|
+
ORDER BY c.ordinal_position;
|
88
|
+
`;
|
89
|
+
|
90
|
+
const columns = await this.db.execute(query);
|
91
|
+
|
92
|
+
return columns.rows.map((col: any) => ({
|
93
|
+
defaultValue: col.column_default,
|
94
|
+
foreignKey: col.foreign_key,
|
95
|
+
isPrimaryKey: !!col.is_primary_key,
|
96
|
+
name: col.column_name,
|
97
|
+
nullable: col.is_nullable === 'YES',
|
98
|
+
type: col.data_type,
|
99
|
+
}));
|
100
|
+
}
|
101
|
+
|
102
|
+
/**
|
103
|
+
* 获取表数据,支持分页、排序和筛选
|
104
|
+
*/
|
105
|
+
async getTableData(tableName: string, pagination: PaginationParams, filters?: FilterCondition[]) {
|
106
|
+
const offset = (pagination.page - 1) * pagination.pageSize;
|
107
|
+
|
108
|
+
// 构建基础查询
|
109
|
+
let baseQuery = sql`SELECT * FROM ${sql.identifier(tableName)}`;
|
110
|
+
|
111
|
+
// 添加筛选条件
|
112
|
+
if (filters && filters.length > 0) {
|
113
|
+
const whereConditions = filters.map((filter) => {
|
114
|
+
const column = sql.identifier(filter.column);
|
115
|
+
|
116
|
+
switch (filter.operator) {
|
117
|
+
case 'equals': {
|
118
|
+
return sql`${column} = ${filter.value}`;
|
119
|
+
}
|
120
|
+
case 'contains': {
|
121
|
+
return sql`${column} ILIKE ${`%${filter.value}%`}`;
|
122
|
+
}
|
123
|
+
case 'startsWith': {
|
124
|
+
return sql`${column} ILIKE ${`${filter.value}%`}`;
|
125
|
+
}
|
126
|
+
case 'endsWith': {
|
127
|
+
return sql`${column} ILIKE ${`%${filter.value}`}`;
|
128
|
+
}
|
129
|
+
default: {
|
130
|
+
return sql`1=1`;
|
131
|
+
}
|
132
|
+
}
|
133
|
+
});
|
134
|
+
|
135
|
+
baseQuery = sql`${baseQuery} WHERE ${sql.join(whereConditions, sql` AND `)}`;
|
136
|
+
}
|
137
|
+
|
138
|
+
// 添加排序
|
139
|
+
if (pagination.sortBy) {
|
140
|
+
const direction = pagination.sortOrder === 'desc' ? sql`DESC` : sql`ASC`;
|
141
|
+
baseQuery = sql`${baseQuery} ORDER BY ${sql.identifier(pagination.sortBy)} ${direction}`;
|
142
|
+
}
|
143
|
+
|
144
|
+
// 添加分页
|
145
|
+
const query = sql`${baseQuery} LIMIT ${pagination.pageSize} OFFSET ${offset}`;
|
146
|
+
|
147
|
+
// 获取总数
|
148
|
+
const countQuery = sql`SELECT COUNT(*) as total FROM ${sql.identifier(tableName)}`;
|
149
|
+
|
150
|
+
// 并行执行查询
|
151
|
+
const [data, count] = await Promise.all([this.db.execute(query), this.db.execute(countQuery)]);
|
152
|
+
|
153
|
+
return {
|
154
|
+
data: data.rows,
|
155
|
+
pagination: {
|
156
|
+
page: pagination.page,
|
157
|
+
pageSize: pagination.pageSize,
|
158
|
+
total: Number(count.rows[0].total),
|
159
|
+
},
|
160
|
+
};
|
161
|
+
}
|
162
|
+
|
163
|
+
/**
|
164
|
+
* 更新表中的一行数据
|
165
|
+
*/
|
166
|
+
async updateRow(
|
167
|
+
tableName: string,
|
168
|
+
id: string,
|
169
|
+
primaryKeyColumn: string,
|
170
|
+
data: Record<string, any>,
|
171
|
+
) {
|
172
|
+
const setColumns = Object.entries(data).map(([key, value]) => {
|
173
|
+
return sql`${sql.identifier(key)} = ${value}`;
|
174
|
+
});
|
175
|
+
|
176
|
+
const query = sql`
|
177
|
+
UPDATE ${sql.identifier(tableName)}
|
178
|
+
SET ${sql.join(setColumns, sql`, `)}
|
179
|
+
WHERE ${sql.identifier(primaryKeyColumn)} = ${id}
|
180
|
+
RETURNING *
|
181
|
+
`;
|
182
|
+
|
183
|
+
const result = await this.db.execute(query);
|
184
|
+
return result.rows[0];
|
185
|
+
}
|
186
|
+
|
187
|
+
/**
|
188
|
+
* 删除表中的一行数据
|
189
|
+
*/
|
190
|
+
async deleteRow(tableName: string, id: string, primaryKeyColumn: string) {
|
191
|
+
const query = sql`
|
192
|
+
DELETE FROM ${sql.identifier(tableName)}
|
193
|
+
WHERE ${sql.identifier(primaryKeyColumn)} = ${id}
|
194
|
+
`;
|
195
|
+
|
196
|
+
await this.db.execute(query);
|
197
|
+
}
|
198
|
+
|
199
|
+
/**
|
200
|
+
* 插入新行数据
|
201
|
+
*/
|
202
|
+
async insertRow(tableName: string, data: Record<string, any>) {
|
203
|
+
const columns = Object.keys(data).map((key) => sql.identifier(key));
|
204
|
+
const values = Object.values(data);
|
205
|
+
|
206
|
+
const query = sql`
|
207
|
+
INSERT INTO ${sql.identifier(tableName)}
|
208
|
+
(${sql.join(columns, sql`, `)})
|
209
|
+
VALUES (${sql.join(
|
210
|
+
values.map((v) => sql`${v}`),
|
211
|
+
sql`, `,
|
212
|
+
)})
|
213
|
+
RETURNING *
|
214
|
+
`;
|
215
|
+
|
216
|
+
const result = await this.db.execute(query);
|
217
|
+
return result.rows[0];
|
218
|
+
}
|
219
|
+
|
220
|
+
/**
|
221
|
+
* 获取表的总记录数
|
222
|
+
*/
|
223
|
+
async getTableCount(tableName: string): Promise<number> {
|
224
|
+
const query = sql`SELECT COUNT(*) as total FROM ${sql.identifier(tableName)}`;
|
225
|
+
const result = await this.db.execute(query);
|
226
|
+
return Number(result.rows[0].total);
|
227
|
+
}
|
228
|
+
|
229
|
+
/**
|
230
|
+
* 批量删除数据
|
231
|
+
*/
|
232
|
+
async batchDelete(tableName: string, ids: string[], primaryKeyColumn: string) {
|
233
|
+
const query = sql`
|
234
|
+
DELETE FROM ${sql.identifier(tableName)}
|
235
|
+
WHERE ${sql.identifier(primaryKeyColumn)} = ANY(${ids})
|
236
|
+
`;
|
237
|
+
|
238
|
+
await this.db.execute(query);
|
239
|
+
}
|
240
|
+
|
241
|
+
/**
|
242
|
+
* 导出表数据(支持分页导出)
|
243
|
+
*/
|
244
|
+
async exportTableData(
|
245
|
+
tableName: string,
|
246
|
+
pagination?: PaginationParams,
|
247
|
+
filters?: FilterCondition[],
|
248
|
+
) {
|
249
|
+
return this.getTableData(tableName, pagination || { page: 1, pageSize: 1000 }, filters);
|
250
|
+
}
|
251
|
+
}
|