@lobehub/chat 0.162.25 → 0.163.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 (84) hide show
  1. package/.github/workflows/release.yml +21 -2
  2. package/.github/workflows/sync.yml +1 -1
  3. package/.github/workflows/test.yml +35 -4
  4. package/CHANGELOG.md +25 -0
  5. package/LICENSE +38 -21
  6. package/codecov.yml +11 -0
  7. package/drizzle.config.ts +29 -0
  8. package/next.config.mjs +3 -0
  9. package/package.json +24 -4
  10. package/scripts/migrateServerDB/index.ts +30 -0
  11. package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +2 -1
  12. package/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +95 -88
  13. package/src/app/(main)/chat/settings/features/HeaderContent.tsx +37 -31
  14. package/src/app/api/webhooks/clerk/__tests__/fixtures/createUser.json +73 -0
  15. package/src/app/api/webhooks/clerk/route.ts +159 -0
  16. package/src/app/api/webhooks/clerk/validateRequest.ts +22 -0
  17. package/src/app/trpc/edge/[trpc]/route.ts +1 -1
  18. package/src/app/trpc/lambda/[trpc]/route.ts +26 -0
  19. package/src/config/auth.ts +2 -0
  20. package/src/config/db.ts +13 -1
  21. package/src/database/server/core/db.ts +44 -0
  22. package/src/database/server/core/dbForTest.ts +45 -0
  23. package/src/database/server/index.ts +1 -0
  24. package/src/database/server/migrations/0000_init.sql +439 -0
  25. package/src/database/server/migrations/0001_add_client_id.sql +9 -0
  26. package/src/database/server/migrations/0002_amusing_puma.sql +9 -0
  27. package/src/database/server/migrations/meta/0000_snapshot.json +1583 -0
  28. package/src/database/server/migrations/meta/0001_snapshot.json +1636 -0
  29. package/src/database/server/migrations/meta/0002_snapshot.json +1630 -0
  30. package/src/database/server/migrations/meta/_journal.json +27 -0
  31. package/src/database/server/models/__tests__/file.test.ts +140 -0
  32. package/src/database/server/models/__tests__/message.test.ts +847 -0
  33. package/src/database/server/models/__tests__/plugin.test.ts +172 -0
  34. package/src/database/server/models/__tests__/session.test.ts +595 -0
  35. package/src/database/server/models/__tests__/topic.test.ts +623 -0
  36. package/src/database/server/models/__tests__/user.test.ts +173 -0
  37. package/src/database/server/models/_template.ts +44 -0
  38. package/src/database/server/models/file.ts +51 -0
  39. package/src/database/server/models/message.ts +378 -0
  40. package/src/database/server/models/plugin.ts +63 -0
  41. package/src/database/server/models/session.ts +290 -0
  42. package/src/database/server/models/sessionGroup.ts +69 -0
  43. package/src/database/server/models/topic.ts +265 -0
  44. package/src/database/server/models/user.ts +138 -0
  45. package/src/database/server/modules/DataImporter/__tests__/fixtures/messages.json +1101 -0
  46. package/src/database/server/modules/DataImporter/__tests__/index.test.ts +954 -0
  47. package/src/database/server/modules/DataImporter/index.ts +333 -0
  48. package/src/database/server/schemas/_id.ts +15 -0
  49. package/src/database/server/schemas/lobechat.ts +601 -0
  50. package/src/database/server/utils/idGenerator.test.ts +39 -0
  51. package/src/database/server/utils/idGenerator.ts +26 -0
  52. package/src/features/User/UserPanel/useMenu.tsx +43 -37
  53. package/src/libs/trpc/client.ts +52 -3
  54. package/src/server/files/s3.ts +21 -1
  55. package/src/server/keyVaultsEncrypt/index.test.ts +62 -0
  56. package/src/server/keyVaultsEncrypt/index.ts +93 -0
  57. package/src/server/mock.ts +1 -1
  58. package/src/server/routers/{index.ts → edge/index.ts} +3 -3
  59. package/src/server/routers/lambda/file.ts +49 -0
  60. package/src/server/routers/lambda/importer.ts +54 -0
  61. package/src/server/routers/lambda/index.ts +28 -0
  62. package/src/server/routers/lambda/message.ts +165 -0
  63. package/src/server/routers/lambda/plugin.ts +100 -0
  64. package/src/server/routers/lambda/session.ts +194 -0
  65. package/src/server/routers/lambda/sessionGroup.ts +77 -0
  66. package/src/server/routers/lambda/topic.ts +134 -0
  67. package/src/server/routers/lambda/user.ts +57 -0
  68. package/src/services/file/index.ts +4 -7
  69. package/src/services/file/server.ts +45 -0
  70. package/src/services/import/index.ts +4 -1
  71. package/src/services/import/server.ts +115 -0
  72. package/src/services/message/index.ts +4 -8
  73. package/src/services/message/server.ts +93 -0
  74. package/src/services/plugin/index.ts +4 -9
  75. package/src/services/plugin/server.ts +46 -0
  76. package/src/services/session/index.ts +4 -8
  77. package/src/services/session/server.ts +148 -0
  78. package/src/services/topic/index.ts +4 -9
  79. package/src/services/topic/server.ts +68 -0
  80. package/src/services/user/index.ts +4 -9
  81. package/src/services/user/server.ts +28 -0
  82. package/tests/setup-db.ts +7 -0
  83. package/vitest.config.ts +2 -1
  84. package/vitest.server.config.ts +23 -0
@@ -0,0 +1,290 @@
1
+ import { Column, asc, count, inArray, like, sql } from 'drizzle-orm';
2
+ import { and, desc, eq, isNull, not, or } from 'drizzle-orm/expressions';
3
+
4
+ import { appEnv } from '@/config/app';
5
+ import { INBOX_SESSION_ID } from '@/const/session';
6
+ import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
7
+ import { serverDB } from '@/database/server/core/db';
8
+ import { parseAgentConfig } from '@/server/globalConfig/parseDefaultAgent';
9
+ import { ChatSessionList, LobeAgentSession } from '@/types/session';
10
+ import { merge } from '@/utils/merge';
11
+
12
+ import {
13
+ AgentItem,
14
+ NewAgent,
15
+ NewSession,
16
+ SessionItem,
17
+ agents,
18
+ agentsToSessions,
19
+ sessionGroups,
20
+ sessions,
21
+ } from '../schemas/lobechat';
22
+ import { idGenerator } from '../utils/idGenerator';
23
+
24
+ export class SessionModel {
25
+ private userId: string;
26
+
27
+ constructor(userId: string) {
28
+ this.userId = userId;
29
+ }
30
+ // **************** Query *************** //
31
+
32
+ async query({ current = 0, pageSize = 9999 } = {}) {
33
+ const offset = current * pageSize;
34
+
35
+ return serverDB.query.sessions.findMany({
36
+ limit: pageSize,
37
+ offset,
38
+ orderBy: [desc(sessions.updatedAt)],
39
+ where: and(eq(sessions.userId, this.userId), not(eq(sessions.slug, INBOX_SESSION_ID))),
40
+ with: { agentsToSessions: { columns: {}, with: { agent: true } }, group: true },
41
+ });
42
+ }
43
+
44
+ async queryWithGroups(): Promise<ChatSessionList> {
45
+ // 查询所有会话
46
+ const result = await this.query();
47
+
48
+ const groups = await serverDB.query.sessionGroups.findMany({
49
+ orderBy: [asc(sessionGroups.sort), desc(sessionGroups.createdAt)],
50
+ where: eq(sessions.userId, this.userId),
51
+ });
52
+
53
+ return {
54
+ sessionGroups: groups as unknown as ChatSessionList['sessionGroups'],
55
+ sessions: result.map((item) => this.mapSessionItem(item as any)),
56
+ };
57
+ }
58
+
59
+ async queryByKeyword(keyword: string) {
60
+ if (!keyword) return [];
61
+
62
+ const keywordLowerCase = keyword.toLowerCase();
63
+
64
+ const data = await this.findSessions({ keyword: keywordLowerCase });
65
+
66
+ return data.map((item) => this.mapSessionItem(item as any));
67
+ }
68
+
69
+ async findByIdOrSlug(
70
+ idOrSlug: string,
71
+ ): Promise<(SessionItem & { agent: AgentItem }) | undefined> {
72
+ const result = await serverDB.query.sessions.findFirst({
73
+ where: and(
74
+ or(eq(sessions.id, idOrSlug), eq(sessions.slug, idOrSlug)),
75
+ eq(sessions.userId, this.userId),
76
+ ),
77
+ with: { agentsToSessions: { columns: {}, with: { agent: true } }, group: true },
78
+ });
79
+
80
+ if (!result) return;
81
+
82
+ return { ...result, agent: (result?.agentsToSessions?.[0] as any)?.agent } as any;
83
+ }
84
+
85
+ async count() {
86
+ const result = await serverDB
87
+ .select({
88
+ count: count(),
89
+ })
90
+ .from(sessions)
91
+ .where(eq(sessions.userId, this.userId))
92
+ .execute();
93
+
94
+ return result[0].count;
95
+ }
96
+
97
+ // **************** Create *************** //
98
+
99
+ async create({
100
+ id = idGenerator('sessions'),
101
+ type = 'agent',
102
+ session = {},
103
+ config = {},
104
+ slug,
105
+ }: {
106
+ config?: Partial<NewAgent>;
107
+ id?: string;
108
+ session?: Partial<NewSession>;
109
+ slug?: string;
110
+ type: 'agent' | 'group';
111
+ }): Promise<SessionItem> {
112
+ return serverDB.transaction(async (trx) => {
113
+ const newAgents = await trx
114
+ .insert(agents)
115
+ .values({
116
+ ...config,
117
+ createdAt: new Date(),
118
+ id: idGenerator('agents'),
119
+ updatedAt: new Date(),
120
+ userId: this.userId,
121
+ })
122
+ .returning();
123
+
124
+ const result = await trx
125
+ .insert(sessions)
126
+ .values({
127
+ ...session,
128
+ createdAt: new Date(),
129
+ id,
130
+ slug,
131
+ type,
132
+ updatedAt: new Date(),
133
+ userId: this.userId,
134
+ })
135
+ .returning();
136
+
137
+ await trx.insert(agentsToSessions).values({
138
+ agentId: newAgents[0].id,
139
+ sessionId: id,
140
+ });
141
+
142
+ return result[0];
143
+ });
144
+ }
145
+
146
+ async createInbox() {
147
+ const serverAgentConfig = parseAgentConfig(appEnv.DEFAULT_AGENT_CONFIG) || {};
148
+
149
+ return await this.create({
150
+ config: merge(DEFAULT_AGENT_CONFIG, serverAgentConfig),
151
+ slug: INBOX_SESSION_ID,
152
+ type: 'agent',
153
+ });
154
+ }
155
+
156
+ async batchCreate(newSessions: NewSession[]) {
157
+ const sessionsToInsert = newSessions.map((s) => {
158
+ return {
159
+ ...s,
160
+ id: this.genId(),
161
+ userId: this.userId,
162
+ };
163
+ });
164
+
165
+ return serverDB.insert(sessions).values(sessionsToInsert);
166
+ }
167
+
168
+ async duplicate(id: string, newTitle?: string) {
169
+ const result = await this.findByIdOrSlug(id);
170
+
171
+ if (!result) return;
172
+
173
+ const { agent, ...session } = result;
174
+ const sessionId = this.genId();
175
+
176
+ return this.create({
177
+ config: agent,
178
+ id: sessionId,
179
+ session: {
180
+ ...session,
181
+ title: newTitle || session.title,
182
+ },
183
+ type: 'agent',
184
+ });
185
+ }
186
+
187
+ // **************** Delete *************** //
188
+
189
+ /**
190
+ * Delete a session, also delete all messages and topics associated with it.
191
+ */
192
+ async delete(id: string) {
193
+ return serverDB
194
+ .delete(sessions)
195
+ .where(and(eq(sessions.id, id), eq(sessions.userId, this.userId)));
196
+ }
197
+
198
+ /**
199
+ * Batch delete sessions, also delete all messages and topics associated with them.
200
+ */
201
+ async batchDelete(ids: string[]) {
202
+ return serverDB
203
+ .delete(sessions)
204
+ .where(and(inArray(sessions.id, ids), eq(sessions.userId, this.userId)));
205
+ }
206
+
207
+ async deleteAll() {
208
+ return serverDB.delete(sessions).where(eq(sessions.userId, this.userId));
209
+ }
210
+ // **************** Update *************** //
211
+
212
+ async update(id: string, data: Partial<SessionItem>) {
213
+ return serverDB
214
+ .update(sessions)
215
+ .set(data)
216
+ .where(and(eq(sessions.id, id), eq(sessions.userId, this.userId)))
217
+ .returning();
218
+ }
219
+
220
+ async updateConfig(id: string, data: Partial<AgentItem>) {
221
+ return serverDB
222
+ .update(agents)
223
+ .set(data)
224
+ .where(and(eq(agents.id, id), eq(agents.userId, this.userId)));
225
+ }
226
+
227
+ // **************** Helper *************** //
228
+
229
+ private genId = () => idGenerator('sessions');
230
+
231
+ private mapSessionItem = ({
232
+ agentsToSessions,
233
+ title,
234
+ backgroundColor,
235
+ description,
236
+ avatar,
237
+ groupId,
238
+ ...res
239
+ }: SessionItem & { agentsToSessions?: { agent: AgentItem }[] }): LobeAgentSession => {
240
+ // TODO: 未来这里需要更好的实现方案,目前只取第一个
241
+ const agent = agentsToSessions?.[0]?.agent;
242
+ return {
243
+ ...res,
244
+ group: groupId,
245
+ meta: {
246
+ avatar: agent?.avatar ?? avatar ?? undefined,
247
+ backgroundColor: agent?.backgroundColor ?? backgroundColor ?? undefined,
248
+ description: agent?.description ?? description ?? undefined,
249
+ title: agent?.title ?? title ?? undefined,
250
+ },
251
+ model: agent?.model,
252
+ } as any;
253
+ };
254
+
255
+ async findSessions(params: {
256
+ current?: number;
257
+ group?: string;
258
+ keyword?: string;
259
+ pageSize?: number;
260
+ pinned?: boolean;
261
+ }) {
262
+ const { pinned, keyword, group, pageSize = 9999, current = 0 } = params;
263
+
264
+ const offset = current * pageSize;
265
+ return serverDB.query.sessions.findMany({
266
+ limit: pageSize,
267
+ offset,
268
+ orderBy: [desc(sessions.updatedAt)],
269
+ where: and(
270
+ eq(sessions.userId, this.userId),
271
+ pinned !== undefined ? eq(sessions.pinned, pinned) : eq(sessions.userId, this.userId),
272
+ keyword
273
+ ? or(
274
+ like(
275
+ sql`lower(${sessions.title})` as unknown as Column,
276
+ `%${keyword.toLowerCase()}%`,
277
+ ),
278
+ like(
279
+ sql`lower(${sessions.description})` as unknown as Column,
280
+ `%${keyword.toLowerCase()}%`,
281
+ ),
282
+ )
283
+ : eq(sessions.userId, this.userId),
284
+ group ? eq(sessions.groupId, group) : isNull(sessions.groupId),
285
+ ),
286
+
287
+ with: { agentsToSessions: { columns: {}, with: { agent: true } }, group: true },
288
+ });
289
+ }
290
+ }
@@ -0,0 +1,69 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { and, asc, desc } from 'drizzle-orm/expressions';
3
+
4
+ import { serverDB } from '@/database/server';
5
+ import { idGenerator } from '@/database/server/utils/idGenerator';
6
+
7
+ import { SessionGroupItem, sessionGroups } from '../schemas/lobechat';
8
+
9
+ export class SessionGroupModel {
10
+ private userId: string;
11
+
12
+ constructor(userId: string) {
13
+ this.userId = userId;
14
+ }
15
+
16
+ create = async (params: { name: string; sort?: number }) => {
17
+ const [result] = await serverDB
18
+ .insert(sessionGroups)
19
+ .values({ ...params, id: this.genId(), userId: this.userId })
20
+ .returning();
21
+
22
+ return result;
23
+ };
24
+
25
+ delete = async (id: string) => {
26
+ return serverDB
27
+ .delete(sessionGroups)
28
+ .where(and(eq(sessionGroups.id, id), eq(sessionGroups.userId, this.userId)));
29
+ };
30
+
31
+ deleteAll = async () => {
32
+ return serverDB.delete(sessionGroups);
33
+ };
34
+
35
+ query = async () => {
36
+ return serverDB.query.sessionGroups.findMany({
37
+ orderBy: [asc(sessionGroups.sort), desc(sessionGroups.createdAt)],
38
+ where: eq(sessionGroups.userId, this.userId),
39
+ });
40
+ };
41
+
42
+ findById = async (id: string) => {
43
+ return serverDB.query.sessionGroups.findFirst({
44
+ where: and(eq(sessionGroups.id, id), eq(sessionGroups.userId, this.userId)),
45
+ });
46
+ };
47
+
48
+ async update(id: string, value: Partial<SessionGroupItem>) {
49
+ return serverDB
50
+ .update(sessionGroups)
51
+ .set({ ...value, updatedAt: new Date() })
52
+ .where(and(eq(sessionGroups.id, id), eq(sessionGroups.userId, this.userId)));
53
+ }
54
+
55
+ async updateOrder(sortMap: { id: string; sort: number }[]) {
56
+ await serverDB.transaction(async (tx) => {
57
+ const updates = sortMap.map(({ id, sort }) => {
58
+ return tx
59
+ .update(sessionGroups)
60
+ .set({ sort, updatedAt: new Date() })
61
+ .where(and(eq(sessionGroups.id, id), eq(sessionGroups.userId, this.userId)));
62
+ });
63
+
64
+ await Promise.all(updates);
65
+ });
66
+ }
67
+
68
+ private genId = () => idGenerator('sessionGroups');
69
+ }
@@ -0,0 +1,265 @@
1
+ import { Column, count, inArray, sql } from 'drizzle-orm';
2
+ import { and, desc, eq, exists, isNull, like, or } from 'drizzle-orm/expressions';
3
+
4
+ import { serverDB } from '@/database/server/core/db';
5
+
6
+ import { NewMessage, TopicItem, messages, topics } from '../schemas/lobechat';
7
+ import { idGenerator } from '../utils/idGenerator';
8
+
9
+ export interface CreateTopicParams {
10
+ favorite?: boolean;
11
+ messages?: string[];
12
+ sessionId?: string | null;
13
+ title: string;
14
+ }
15
+
16
+ interface QueryTopicParams {
17
+ current?: number;
18
+ pageSize?: number;
19
+ sessionId?: string | null;
20
+ }
21
+
22
+ export class TopicModel {
23
+ private userId: string;
24
+
25
+ constructor(userId: string) {
26
+ this.userId = userId;
27
+ }
28
+ // **************** Query *************** //
29
+
30
+ async query({ current = 0, pageSize = 9999, sessionId }: QueryTopicParams = {}) {
31
+ const offset = current * pageSize;
32
+
33
+ return (
34
+ serverDB
35
+ .select({
36
+ createdAt: topics.createdAt,
37
+ favorite: topics.favorite,
38
+ id: topics.id,
39
+ title: topics.title,
40
+ updatedAt: topics.updatedAt,
41
+ })
42
+ .from(topics)
43
+ .where(and(eq(topics.userId, this.userId), this.matchSession(sessionId)))
44
+ // In boolean sorting, false is considered "smaller" than true.
45
+ // So here we use desc to ensure that topics with favorite as true are in front.
46
+ .orderBy(desc(topics.favorite), desc(topics.updatedAt))
47
+ .limit(pageSize)
48
+ .offset(offset)
49
+ );
50
+ }
51
+
52
+ async findById(id: string) {
53
+ return serverDB.query.topics.findFirst({
54
+ where: and(eq(topics.id, id), eq(topics.userId, this.userId)),
55
+ });
56
+ }
57
+
58
+ async queryAll(): Promise<TopicItem[]> {
59
+ return serverDB
60
+ .select()
61
+ .from(topics)
62
+ .orderBy(topics.updatedAt)
63
+ .where(eq(topics.userId, this.userId))
64
+ .execute();
65
+ }
66
+
67
+ async queryByKeyword(keyword: string, sessionId?: string | null): Promise<TopicItem[]> {
68
+ if (!keyword) return [];
69
+
70
+ const keywordLowerCase = keyword.toLowerCase();
71
+
72
+ const matchKeyword = (field: any) =>
73
+ like(sql`lower(${field})` as unknown as Column, `%${keywordLowerCase}%`);
74
+
75
+ return serverDB.query.topics.findMany({
76
+ orderBy: [desc(topics.updatedAt)],
77
+ where: and(
78
+ eq(topics.userId, this.userId),
79
+ this.matchSession(sessionId),
80
+ or(
81
+ matchKeyword(topics.title),
82
+ exists(
83
+ serverDB
84
+ .select()
85
+ .from(messages)
86
+ .where(and(eq(messages.topicId, topics.id), or(matchKeyword(messages.content)))),
87
+ ),
88
+ ),
89
+ ),
90
+ });
91
+ }
92
+
93
+ async count() {
94
+ const result = await serverDB
95
+ .select({
96
+ count: count(),
97
+ })
98
+ .from(topics)
99
+ .where(eq(topics.userId, this.userId))
100
+ .execute();
101
+
102
+ return result[0].count;
103
+ }
104
+
105
+ // **************** Create *************** //
106
+
107
+ async create(
108
+ { messages: messageIds, ...params }: CreateTopicParams,
109
+ id: string = this.genId(),
110
+ ): Promise<TopicItem> {
111
+ return serverDB.transaction(async (tx) => {
112
+ // 在 topics 表中插入新的 topic
113
+ const [topic] = await tx
114
+ .insert(topics)
115
+ .values({
116
+ ...params,
117
+ id: id,
118
+ userId: this.userId,
119
+ })
120
+ .returning();
121
+
122
+ // 如果有关联的 messages, 更新它们的 topicId
123
+ if (messageIds && messageIds.length > 0) {
124
+ await tx
125
+ .update(messages)
126
+ .set({ topicId: topic.id })
127
+ .where(and(eq(messages.userId, this.userId), inArray(messages.id, messageIds)));
128
+ }
129
+
130
+ return topic;
131
+ });
132
+ }
133
+
134
+ async batchCreate(topicParams: (CreateTopicParams & { id?: string })[]) {
135
+ // 开始一个事务
136
+ return serverDB.transaction(async (tx) => {
137
+ // 在 topics 表中批量插入新的 topics
138
+ const createdTopics = await tx
139
+ .insert(topics)
140
+ .values(
141
+ topicParams.map((params) => ({
142
+ favorite: params.favorite,
143
+ id: params.id || this.genId(),
144
+ sessionId: params.sessionId,
145
+ title: params.title,
146
+ userId: this.userId,
147
+ })),
148
+ )
149
+ .returning();
150
+
151
+ // 对每个新创建的 topic,更新关联的 messages 的 topicId
152
+ await Promise.all(
153
+ createdTopics.map(async (topic, index) => {
154
+ const messageIds = topicParams[index].messages;
155
+ if (messageIds && messageIds.length > 0) {
156
+ await tx
157
+ .update(messages)
158
+ .set({ topicId: topic.id })
159
+ .where(and(eq(messages.userId, this.userId), inArray(messages.id, messageIds)));
160
+ }
161
+ }),
162
+ );
163
+
164
+ return createdTopics;
165
+ });
166
+ }
167
+
168
+ async duplicate(topicId: string, newTitle?: string) {
169
+ return serverDB.transaction(async (tx) => {
170
+ // find original topic
171
+ const originalTopic = await tx.query.topics.findFirst({
172
+ where: and(eq(topics.id, topicId), eq(topics.userId, this.userId)),
173
+ });
174
+
175
+ if (!originalTopic) {
176
+ throw new Error(`Topic with id ${topicId} not found`);
177
+ }
178
+
179
+ // copy topic
180
+ const [duplicatedTopic] = await tx
181
+ .insert(topics)
182
+ .values({
183
+ ...originalTopic,
184
+ id: this.genId(),
185
+ title: newTitle || originalTopic?.title,
186
+ })
187
+ .returning();
188
+
189
+ // 查找与原始 topic 关联的 messages
190
+ const originalMessages = await tx
191
+ .select()
192
+ .from(messages)
193
+ .where(and(eq(messages.topicId, topicId), eq(messages.userId, this.userId)));
194
+
195
+ // copy messages
196
+ const duplicatedMessages = await Promise.all(
197
+ originalMessages.map(async (message) => {
198
+ const result = (await tx
199
+ .insert(messages)
200
+ .values({
201
+ ...message,
202
+ id: idGenerator('messages'),
203
+ topicId: duplicatedTopic.id,
204
+ })
205
+ .returning()) as NewMessage[];
206
+
207
+ return result[0];
208
+ }),
209
+ );
210
+
211
+ return {
212
+ messages: duplicatedMessages,
213
+ topic: duplicatedTopic,
214
+ };
215
+ });
216
+ }
217
+
218
+ // **************** Delete *************** //
219
+
220
+ /**
221
+ * Delete a session, also delete all messages and topics associated with it.
222
+ */
223
+ async delete(id: string) {
224
+ return serverDB.delete(topics).where(and(eq(topics.id, id), eq(topics.userId, this.userId)));
225
+ }
226
+
227
+ /**
228
+ * Deletes multiple topics based on the sessionId.
229
+ */
230
+ async batchDeleteBySessionId(sessionId?: string | null) {
231
+ return serverDB
232
+ .delete(topics)
233
+ .where(and(this.matchSession(sessionId), eq(topics.userId, this.userId)));
234
+ }
235
+
236
+ /**
237
+ * Deletes multiple topics and all messages associated with them in a transaction.
238
+ */
239
+ async batchDelete(ids: string[]) {
240
+ return serverDB
241
+ .delete(topics)
242
+ .where(and(inArray(topics.id, ids), eq(topics.userId, this.userId)));
243
+ }
244
+
245
+ async deleteAll() {
246
+ return serverDB.delete(topics).where(eq(topics.userId, this.userId));
247
+ }
248
+
249
+ // **************** Update *************** //
250
+
251
+ async update(id: string, data: Partial<TopicItem>) {
252
+ return serverDB
253
+ .update(topics)
254
+ .set({ ...data, updatedAt: new Date() })
255
+ .where(and(eq(topics.id, id), eq(topics.userId, this.userId)))
256
+ .returning();
257
+ }
258
+
259
+ // **************** Helper *************** //
260
+
261
+ private genId = () => idGenerator('topics');
262
+
263
+ private matchSession = (sessionId?: string | null) =>
264
+ sessionId ? eq(topics.sessionId, sessionId) : isNull(topics.sessionId);
265
+ }