@lobehub/chat 1.36.46 → 1.37.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.
Files changed (78) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.ja-JP.md +8 -8
  3. package/README.md +8 -8
  4. package/README.zh-CN.md +8 -8
  5. package/changelog/v1.json +9 -0
  6. package/next.config.mjs +4 -1
  7. package/package.json +5 -3
  8. package/scripts/migrateClientDB/compile-migrations.ts +14 -0
  9. package/src/app/(main)/(mobile)/me/(home)/layout.tsx +2 -0
  10. package/src/app/(main)/chat/_layout/Desktop/index.tsx +3 -2
  11. package/src/app/(main)/chat/_layout/Mobile.tsx +5 -3
  12. package/src/app/(main)/chat/features/Migration/DBReader.ts +290 -0
  13. package/src/app/(main)/chat/features/Migration/UpgradeButton.tsx +4 -8
  14. package/src/app/(main)/chat/features/Migration/index.tsx +26 -15
  15. package/src/app/(main)/settings/_layout/Desktop/index.tsx +2 -0
  16. package/src/app/loading/Client/Content.tsx +11 -1
  17. package/src/app/loading/Client/Error.tsx +27 -0
  18. package/src/app/loading/stage.ts +8 -0
  19. package/src/components/FullscreenLoading/index.tsx +4 -3
  20. package/src/const/version.ts +1 -0
  21. package/src/database/client/db.test.ts +172 -0
  22. package/src/database/client/db.ts +246 -0
  23. package/src/database/client/migrations.json +289 -0
  24. package/src/features/InitClientDB/EnableModal.tsx +111 -0
  25. package/src/features/InitClientDB/ErrorResult.tsx +125 -0
  26. package/src/features/InitClientDB/InitIndicator.tsx +124 -0
  27. package/src/features/InitClientDB/PGliteSVG.tsx +22 -0
  28. package/src/features/InitClientDB/index.tsx +37 -0
  29. package/src/hooks/useCheckPluginsIsInstalled.ts +2 -2
  30. package/src/hooks/useFetchInstalledPlugins.ts +2 -2
  31. package/src/hooks/useFetchMessages.ts +2 -2
  32. package/src/hooks/useFetchSessions.ts +2 -2
  33. package/src/hooks/useFetchThreads.ts +2 -2
  34. package/src/hooks/useFetchTopics.ts +2 -2
  35. package/src/layout/GlobalProvider/StoreInitialization.tsx +2 -2
  36. package/src/services/baseClientService/index.ts +9 -0
  37. package/src/services/debug.ts +32 -34
  38. package/src/services/file/index.ts +6 -2
  39. package/src/services/file/pglite.test.ts +198 -0
  40. package/src/services/file/pglite.ts +84 -0
  41. package/src/services/file/type.ts +4 -3
  42. package/src/services/github.ts +17 -0
  43. package/src/services/import/index.ts +6 -2
  44. package/src/services/import/pglite.test.ts +997 -0
  45. package/src/services/import/pglite.ts +34 -0
  46. package/src/services/message/client.ts +2 -0
  47. package/src/services/message/index.ts +6 -2
  48. package/src/services/message/pglite.test.ts +430 -0
  49. package/src/services/message/pglite.ts +118 -0
  50. package/src/services/message/server.ts +9 -9
  51. package/src/services/message/type.ts +3 -4
  52. package/src/services/plugin/index.ts +6 -2
  53. package/src/services/plugin/pglite.test.ts +175 -0
  54. package/src/services/plugin/pglite.ts +51 -0
  55. package/src/services/session/client.ts +1 -1
  56. package/src/services/session/index.ts +6 -2
  57. package/src/services/session/pglite.test.ts +411 -0
  58. package/src/services/session/pglite.ts +184 -0
  59. package/src/services/session/type.ts +14 -1
  60. package/src/services/topic/index.ts +6 -3
  61. package/src/services/topic/pglite.test.ts +212 -0
  62. package/src/services/topic/pglite.ts +85 -0
  63. package/src/services/user/client.test.ts +0 -1
  64. package/src/services/user/index.ts +8 -2
  65. package/src/services/user/pglite.test.ts +98 -0
  66. package/src/services/user/pglite.ts +92 -0
  67. package/src/store/chat/slices/builtinTool/action.test.ts +3 -4
  68. package/src/store/global/actions/clientDb.ts +51 -0
  69. package/src/store/global/initialState.ts +13 -0
  70. package/src/store/global/selectors.ts +24 -3
  71. package/src/store/global/store.ts +3 -1
  72. package/src/store/session/slices/sessionGroup/reducer.test.ts +6 -6
  73. package/src/store/user/slices/common/action.ts +2 -4
  74. package/src/types/clientDB.ts +29 -0
  75. package/src/types/importer.ts +17 -5
  76. package/src/types/meta.ts +0 -9
  77. package/src/types/session/sessionGroup.ts +3 -3
  78. package/src/services/message/index.test.ts +0 -48
@@ -0,0 +1,184 @@
1
+ import { DeepPartial } from 'utility-types';
2
+
3
+ import { INBOX_SESSION_ID } from '@/const/session';
4
+ import { clientDB } from '@/database/client/db';
5
+ import { AgentItem } from '@/database/schemas';
6
+ import { SessionModel } from '@/database/server/models/session';
7
+ import { SessionGroupModel } from '@/database/server/models/sessionGroup';
8
+ import { BaseClientService } from '@/services/baseClientService';
9
+ import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent';
10
+ import { MetaData } from '@/types/meta';
11
+ import {
12
+ ChatSessionList,
13
+ LobeAgentSession,
14
+ LobeSessionType,
15
+ LobeSessions,
16
+ SessionGroupItem,
17
+ SessionGroups,
18
+ UpdateSessionParams,
19
+ } from '@/types/session';
20
+
21
+ import { ISessionService } from './type';
22
+
23
+ export class ClientService extends BaseClientService implements ISessionService {
24
+ private get sessionModel(): SessionModel {
25
+ return new SessionModel(clientDB as any, this.userId);
26
+ }
27
+
28
+ private get sessionGroupModel(): SessionGroupModel {
29
+ return new SessionGroupModel(clientDB as any, this.userId);
30
+ }
31
+
32
+ async createSession(type: LobeSessionType, data: Partial<LobeAgentSession>): Promise<string> {
33
+ const { config, group, meta, ...session } = data;
34
+
35
+ const item = await this.sessionModel.create({
36
+ config: { ...config, ...meta } as any,
37
+ session: { ...session, groupId: group },
38
+ type,
39
+ });
40
+ if (!item) {
41
+ throw new Error('session create Error');
42
+ }
43
+ return item.id;
44
+ }
45
+
46
+ async batchCreateSessions(importSessions: LobeSessions) {
47
+ // @ts-ignore
48
+ return this.sessionModel.batchCreate(importSessions);
49
+ }
50
+
51
+ async cloneSession(id: string, newTitle: string): Promise<string | undefined> {
52
+ const res = await this.sessionModel.duplicate(id, newTitle);
53
+
54
+ if (res) return res?.id;
55
+ }
56
+
57
+ async getGroupedSessions(): Promise<ChatSessionList> {
58
+ return this.sessionModel.queryWithGroups();
59
+ }
60
+
61
+ async getSessionConfig(id: string): Promise<LobeAgentConfig> {
62
+ const res = await this.sessionModel.findByIdOrSlug(id);
63
+
64
+ if (!res) throw new Error('Session not found');
65
+
66
+ return res.agent as LobeAgentConfig;
67
+ }
68
+
69
+ /**
70
+ * 这个方法要对应移除的
71
+ */
72
+ async getSessionsByType(type: 'agent' | 'group' | 'all' = 'all'): Promise<LobeSessions> {
73
+ switch (type) {
74
+ // TODO: add a filter to get only agents or agents
75
+ case 'group': {
76
+ // @ts-ignore
77
+ return this.sessionModel.query();
78
+ }
79
+ case 'agent': {
80
+ // @ts-ignore
81
+ return this.sessionModel.query();
82
+ }
83
+
84
+ case 'all': {
85
+ // @ts-ignore
86
+ return this.sessionModel.query();
87
+ }
88
+ }
89
+ }
90
+
91
+ async countSessions() {
92
+ return this.sessionModel.count();
93
+ }
94
+
95
+ async searchSessions(keyword: string) {
96
+ return this.sessionModel.queryByKeyword(keyword);
97
+ }
98
+
99
+ async updateSession(id: string, value: Partial<UpdateSessionParams>) {
100
+ return this.sessionModel.update(id, {
101
+ ...value,
102
+ groupId: value.group === 'default' ? null : value.group,
103
+ });
104
+ }
105
+
106
+ async updateSessionConfig(
107
+ activeId: string,
108
+ config: DeepPartial<LobeAgentConfig>,
109
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
110
+ _?: AbortSignal,
111
+ ) {
112
+ const session = await this.sessionModel.findByIdOrSlug(activeId);
113
+ if (!session || !config) return;
114
+
115
+ return this.sessionModel.updateConfig(session.agent.id, config as AgentItem);
116
+ }
117
+
118
+ async updateSessionMeta(
119
+ activeId: string,
120
+ meta: Partial<MetaData>,
121
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
122
+ _?: AbortSignal,
123
+ ) {
124
+ // inbox 不允许修改 meta
125
+ if (activeId === INBOX_SESSION_ID) return;
126
+
127
+ return this.sessionModel.update(activeId, meta);
128
+ }
129
+
130
+ async updateSessionChatConfig(
131
+ activeId: string,
132
+ config: DeepPartial<LobeAgentChatConfig>,
133
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
134
+ _?: AbortSignal,
135
+ ) {
136
+ return this.updateSessionConfig(activeId, { chatConfig: config });
137
+ }
138
+
139
+ async removeSession(id: string) {
140
+ return this.sessionModel.delete(id);
141
+ }
142
+
143
+ async removeAllSessions() {
144
+ return this.sessionModel.deleteAll();
145
+ }
146
+
147
+ // ************************************** //
148
+ // *********** SessionGroup *********** //
149
+ // ************************************** //
150
+
151
+ async createSessionGroup(name: string, sort?: number) {
152
+ const item = await this.sessionGroupModel.create({ name, sort });
153
+ if (!item) {
154
+ throw new Error('session group create Error');
155
+ }
156
+
157
+ return item.id;
158
+ }
159
+
160
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
161
+ async batchCreateSessionGroups(_groups: SessionGroups) {
162
+ return { added: 0, ids: [], skips: [], success: true };
163
+ }
164
+
165
+ async removeSessionGroup(id: string) {
166
+ return await this.sessionGroupModel.delete(id);
167
+ }
168
+
169
+ async updateSessionGroup(id: string, data: Partial<SessionGroupItem>) {
170
+ return this.sessionGroupModel.update(id, data);
171
+ }
172
+
173
+ async updateSessionGroupOrder(sortMap: { id: string; sort: number }[]) {
174
+ return this.sessionGroupModel.updateOrder(sortMap);
175
+ }
176
+
177
+ async getSessionGroups(): Promise<SessionGroupItem[]> {
178
+ return this.sessionGroupModel.query();
179
+ }
180
+
181
+ async removeSessionGroups() {
182
+ return this.sessionGroupModel.deleteAll();
183
+ }
184
+ }
@@ -16,13 +16,21 @@ import {
16
16
 
17
17
  export interface ISessionService {
18
18
  createSession(type: LobeSessionType, defaultValue: Partial<LobeAgentSession>): Promise<string>;
19
+
20
+ /**
21
+ * 需要废弃
22
+ * @deprecated
23
+ */
19
24
  batchCreateSessions(importSessions: LobeSessions): Promise<any>;
20
25
  cloneSession(id: string, newTitle: string): Promise<string | undefined>;
21
26
 
22
27
  getGroupedSessions(): Promise<ChatSessionList>;
28
+
29
+ /**
30
+ * @deprecated
31
+ */
23
32
  getSessionsByType(type: 'agent' | 'group' | 'all'): Promise<LobeSessions>;
24
33
  countSessions(): Promise<number>;
25
- hasSessions(): Promise<boolean>;
26
34
  searchSessions(keyword: string): Promise<LobeSessions>;
27
35
 
28
36
  updateSession(
@@ -53,6 +61,11 @@ export interface ISessionService {
53
61
  // ************************************** //
54
62
 
55
63
  createSessionGroup(name: string, sort?: number): Promise<string>;
64
+
65
+ /**
66
+ * 需要废弃
67
+ * @deprecated
68
+ */
56
69
  batchCreateSessionGroups(groups: SessionGroups): Promise<BatchTaskResult>;
57
70
 
58
71
  getSessionGroups(): Promise<SessionGroupItem[]>;
@@ -1,6 +1,9 @@
1
-
2
- import { ClientService } from './client';
1
+ import { ClientService as DeprecatedService } from './client';
2
+ import { ClientService } from './pglite';
3
3
  import { ServerService } from './server';
4
4
 
5
+ const clientService =
6
+ process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
7
+
5
8
  export const topicService =
6
- process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService();
9
+ process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : clientService;
@@ -0,0 +1,212 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { clientDB, initializeDB } from '@/database/client/db';
5
+ import { sessions, topics, users } from '@/database/schemas';
6
+ import { ChatTopic } from '@/types/topic';
7
+
8
+ import { ClientService } from './pglite';
9
+
10
+ // Mock data
11
+ const userId = 'topic-user-test';
12
+ const sessionId = 'topic-session';
13
+ const mockTopicId = 'mock-topic-id';
14
+
15
+ const mockTopic = {
16
+ id: mockTopicId,
17
+ title: 'Mock Topic',
18
+ };
19
+
20
+ const topicService = new ClientService(userId);
21
+
22
+ beforeEach(async () => {
23
+ await initializeDB();
24
+
25
+ await clientDB.delete(users);
26
+
27
+ // 创建测试数据
28
+ await clientDB.transaction(async (tx) => {
29
+ await tx.insert(users).values({ id: userId });
30
+ await tx.insert(sessions).values({ id: sessionId, userId });
31
+ await tx.insert(topics).values({ ...mockTopic, sessionId, userId });
32
+ });
33
+ });
34
+
35
+ describe('TopicService', () => {
36
+ describe('createTopic', () => {
37
+ it('should create a topic and return its id', async () => {
38
+ // Setup
39
+ const createParams = {
40
+ title: 'New Topic',
41
+ sessionId: sessionId,
42
+ };
43
+
44
+ // Execute
45
+ const topicId = await topicService.createTopic(createParams);
46
+
47
+ // Assert
48
+ expect(topicId).toBeDefined();
49
+ });
50
+
51
+ it('should throw an error if topic creation fails', async () => {
52
+ // Setup
53
+ const createParams = {
54
+ title: 'New Topic',
55
+ sessionId: 123 as any, // sessionId should be string
56
+ };
57
+
58
+ // Execute & Assert
59
+ await expect(topicService.createTopic(createParams)).rejects.toThrowError();
60
+ });
61
+ });
62
+
63
+ describe('getTopics', () => {
64
+ // Example for getTopics
65
+ it('should query topics with given parameters', async () => {
66
+ // Setup
67
+ const queryParams = { sessionId };
68
+
69
+ // Execute
70
+ const data = await topicService.getTopics(queryParams);
71
+
72
+ // Assert
73
+ expect(data[0]).toMatchObject(mockTopic);
74
+ });
75
+ });
76
+
77
+ describe('updateTopic', () => {
78
+ // Example for updateFavorite
79
+ it('should toggle favorite status of a topic', async () => {
80
+ // Execute
81
+ const result = await topicService.updateTopic(mockTopicId, { favorite: true });
82
+
83
+ // Assert
84
+ expect(result[0].favorite).toBeTruthy();
85
+ });
86
+
87
+ it('should update the title of a topic', async () => {
88
+ // Setup
89
+ const newTitle = 'Updated Topic Title';
90
+
91
+ // Execute
92
+ const result = await topicService.updateTopic(mockTopicId, { title: newTitle });
93
+
94
+ // Assert
95
+ expect(result[0].title).toEqual(newTitle);
96
+ });
97
+ });
98
+
99
+ describe('removeTopic', () => {
100
+ it('should remove a topic by id', async () => {
101
+ // Execute
102
+ await topicService.removeTopic(mockTopicId);
103
+ const result = await clientDB.query.topics.findFirst({ where: eq(topics.id, mockTopicId) });
104
+
105
+ // Assert
106
+ expect(result).toBeUndefined();
107
+ });
108
+ });
109
+
110
+ describe('removeTopics', () => {
111
+ it('should remove all topics with a given session id', async () => {
112
+ // Setup
113
+ const sessionId = 'session-id';
114
+
115
+ // Execute
116
+ await topicService.removeTopics(sessionId);
117
+ const result = await clientDB.query.topics.findMany({
118
+ where: eq(topics.sessionId, sessionId),
119
+ });
120
+
121
+ expect(result.length).toEqual(0);
122
+ });
123
+ });
124
+
125
+ describe('batchRemoveTopics', () => {
126
+ it('should batch remove topics', async () => {
127
+ await clientDB.insert(topics).values([{ id: 'topic-id-1', title: 'topic-title', userId }]);
128
+ // Setup
129
+ const topicIds = [mockTopicId, 'another-topic-id'];
130
+
131
+ // Execute
132
+ await topicService.batchRemoveTopics(topicIds);
133
+
134
+ const count = await clientDB.$count(topics);
135
+
136
+ // Assert
137
+ expect(count).toBe(1);
138
+ });
139
+ });
140
+
141
+ describe('removeAllTopic', () => {
142
+ it('should clear all topics from the table', async () => {
143
+ // Execute
144
+ await topicService.removeAllTopic();
145
+
146
+ const count = await clientDB.$count(topics);
147
+ // Assert
148
+ expect(count).toBe(0);
149
+ });
150
+ });
151
+
152
+ describe('batchCreateTopics', () => {
153
+ it('should batch create topics', async () => {
154
+ // Execute
155
+ const result = await topicService.batchCreateTopics([
156
+ { id: 'topic-id-1', title: 'topic-title' },
157
+ { id: 'topic-id-2', title: 'topic-title' },
158
+ ] as ChatTopic[]);
159
+
160
+ // Assert
161
+ expect(result.success).toBeTruthy();
162
+ expect(result.added).toBe(2);
163
+ });
164
+ });
165
+
166
+ describe('getAllTopics', () => {
167
+ it('should retrieve all topics', async () => {
168
+ await clientDB.insert(topics).values([
169
+ { id: 'topic-id-1', title: 'topic-title', userId },
170
+ { id: 'topic-id-2', title: 'topic-title', userId },
171
+ ]);
172
+ // Execute
173
+ const result = await topicService.getAllTopics();
174
+
175
+ // Assert
176
+ expect(result.length).toEqual(3);
177
+ });
178
+ });
179
+
180
+ describe('searchTopics', () => {
181
+ it('should return all topics that match the keyword', async () => {
182
+ // Setup
183
+ const keyword = 'Topic';
184
+
185
+ // Execute
186
+ const result = await topicService.searchTopics(keyword, sessionId);
187
+
188
+ // Assert
189
+ expect(result.length).toEqual(1);
190
+ });
191
+ it('should return empty topic if not match the keyword', async () => {
192
+ // Setup
193
+ const keyword = 'search';
194
+
195
+ // Execute
196
+ const result = await topicService.searchTopics(keyword, sessionId);
197
+
198
+ // Assert
199
+ expect(result.length).toEqual(0);
200
+ });
201
+ });
202
+
203
+ describe('countTopics', () => {
204
+ it('should return topic counts', async () => {
205
+ // Execute
206
+ const result = await topicService.countTopics();
207
+
208
+ // Assert
209
+ expect(result).toBe(1);
210
+ });
211
+ });
212
+ });
@@ -0,0 +1,85 @@
1
+ import { INBOX_SESSION_ID } from '@/const/session';
2
+ import { clientDB } from '@/database/client/db';
3
+ import { TopicModel } from '@/database/server/models/topic';
4
+ import { BaseClientService } from '@/services/baseClientService';
5
+ import { ChatTopic } from '@/types/topic';
6
+
7
+ import { CreateTopicParams, ITopicService, QueryTopicParams } from './type';
8
+
9
+ export class ClientService extends BaseClientService implements ITopicService {
10
+ private get topicModel(): TopicModel {
11
+ return new TopicModel(clientDB as any, this.userId);
12
+ }
13
+
14
+ async createTopic(params: CreateTopicParams): Promise<string> {
15
+ const item = await this.topicModel.create({
16
+ ...params,
17
+ sessionId: this.toDbSessionId(params.sessionId),
18
+ } as any);
19
+
20
+ if (!item) {
21
+ throw new Error('topic create Error');
22
+ }
23
+
24
+ return item.id;
25
+ }
26
+
27
+ async batchCreateTopics(importTopics: ChatTopic[]) {
28
+ const data = await this.topicModel.batchCreate(importTopics as any);
29
+
30
+ return { added: data.length, ids: [], skips: [], success: true };
31
+ }
32
+
33
+ async cloneTopic(id: string, newTitle?: string) {
34
+ const data = await this.topicModel.duplicate(id, newTitle);
35
+ return data.topic.id;
36
+ }
37
+
38
+ async getTopics(params: QueryTopicParams) {
39
+ const data = await this.topicModel.query({
40
+ ...params,
41
+ sessionId: this.toDbSessionId(params.sessionId),
42
+ });
43
+ return data as unknown as Promise<ChatTopic[]>;
44
+ }
45
+
46
+ async searchTopics(keyword: string, sessionId?: string) {
47
+ const data = await this.topicModel.queryByKeyword(keyword, this.toDbSessionId(sessionId));
48
+
49
+ return data as unknown as Promise<ChatTopic[]>;
50
+ }
51
+
52
+ async getAllTopics() {
53
+ const data = await this.topicModel.queryAll();
54
+
55
+ return data as unknown as Promise<ChatTopic[]>;
56
+ }
57
+
58
+ async countTopics() {
59
+ return this.topicModel.count();
60
+ }
61
+
62
+ async updateTopic(id: string, data: Partial<ChatTopic>) {
63
+ return this.topicModel.update(id, data as any);
64
+ }
65
+
66
+ async removeTopic(id: string) {
67
+ return this.topicModel.delete(id);
68
+ }
69
+
70
+ async removeTopics(sessionId: string) {
71
+ return this.topicModel.batchDeleteBySessionId(this.toDbSessionId(sessionId));
72
+ }
73
+
74
+ async batchRemoveTopics(topics: string[]) {
75
+ return this.topicModel.batchDelete(topics);
76
+ }
77
+
78
+ async removeAllTopic() {
79
+ return this.topicModel.deleteAll();
80
+ }
81
+
82
+ private toDbSessionId(sessionId?: string | null) {
83
+ return sessionId === INBOX_SESSION_ID ? null : sessionId;
84
+ }
85
+ }
@@ -4,7 +4,6 @@ import { Mock, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
  import { UserModel } from '@/database/_deprecated/models/user';
5
5
  import { UserPreference } from '@/types/user';
6
6
  import { UserSettings } from '@/types/user/settings';
7
- import { AsyncLocalStorage } from '@/utils/localStorage';
8
7
 
9
8
  import { ClientService } from './client';
10
9
 
@@ -1,5 +1,11 @@
1
- import { ClientService } from './client';
1
+ import { ClientService as DeprecatedService } from './client';
2
+ import { ClientService } from './pglite';
2
3
  import { ServerService } from './server';
3
4
 
5
+ const clientService =
6
+ process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
7
+
4
8
  export const userService =
5
- process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService();
9
+ process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : clientService;
10
+
11
+ export const userClientService = clientService;
@@ -0,0 +1,98 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { DeepPartial } from 'utility-types';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { clientDB, initializeDB } from '@/database/client/db';
6
+ import { userSettings, users } from '@/database/schemas';
7
+ import { UserPreference } from '@/types/user';
8
+ import { UserSettings } from '@/types/user/settings';
9
+
10
+ import { ClientService } from './pglite';
11
+
12
+ const mockUser = {
13
+ avatar: 'avatar.png',
14
+ settings: { themeMode: 'light' } as unknown as UserSettings,
15
+ uuid: 'user-id',
16
+ };
17
+
18
+ const mockPreference = {
19
+ useCmdEnterToSend: true,
20
+ } as UserPreference;
21
+ const clientService = new ClientService(mockUser.uuid);
22
+
23
+ beforeEach(async () => {
24
+ vi.clearAllMocks();
25
+
26
+ await initializeDB();
27
+ await clientDB.delete(users);
28
+
29
+ await clientDB.insert(users).values({ id: mockUser.uuid, avatar: 'avatar.png' });
30
+ await clientDB
31
+ .insert(userSettings)
32
+ .values({ id: mockUser.uuid, general: { themeMode: 'light' } });
33
+ });
34
+
35
+ describe('ClientService', () => {
36
+ it('should get user state correctly', async () => {
37
+ const spyOn = vi
38
+ .spyOn(clientService['preferenceStorage'], 'getFromLocalStorage')
39
+ .mockResolvedValue(mockPreference);
40
+
41
+ const userState = await clientService.getUserState();
42
+
43
+ expect(userState).toMatchObject({
44
+ avatar: mockUser.avatar,
45
+ isOnboard: true,
46
+ canEnablePWAGuide: false,
47
+ hasConversation: false,
48
+ canEnableTrace: false,
49
+ preference: mockPreference,
50
+ settings: { general: { themeMode: 'light' } },
51
+ userId: mockUser.uuid,
52
+ });
53
+ expect(spyOn).toHaveBeenCalledTimes(1);
54
+ });
55
+
56
+ it('should update user settings correctly', async () => {
57
+ const settingsPatch: DeepPartial<UserSettings> = { general: { themeMode: 'dark' } };
58
+
59
+ await clientService.updateUserSettings(settingsPatch);
60
+
61
+ const result = await clientDB.query.userSettings.findFirst({
62
+ where: eq(userSettings.id, mockUser.uuid),
63
+ });
64
+
65
+ expect(result).toMatchObject(settingsPatch);
66
+ });
67
+
68
+ it('should reset user settings correctly', async () => {
69
+ await clientService.resetUserSettings();
70
+
71
+ const result = await clientDB.query.userSettings.findFirst({
72
+ where: eq(userSettings.id, mockUser.uuid),
73
+ });
74
+
75
+ expect(result).toBeUndefined();
76
+ });
77
+
78
+ it('should update user avatar correctly', async () => {
79
+ const newAvatar = 'new-avatar.png';
80
+
81
+ await clientService.updateAvatar(newAvatar);
82
+ });
83
+
84
+ it('should update user preference correctly', async () => {
85
+ const newPreference = {
86
+ useCmdEnterToSend: false,
87
+ } as UserPreference;
88
+
89
+ const spyOn = vi
90
+ .spyOn(clientService['preferenceStorage'], 'saveToLocalStorage')
91
+ .mockResolvedValue(undefined);
92
+
93
+ await clientService.updatePreference(newPreference);
94
+
95
+ expect(spyOn).toHaveBeenCalledWith(newPreference);
96
+ expect(spyOn).toHaveBeenCalledTimes(1);
97
+ });
98
+ });