@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.
- package/.github/workflows/release.yml +21 -2
- package/.github/workflows/sync.yml +1 -1
- package/.github/workflows/test.yml +35 -4
- package/CHANGELOG.md +25 -0
- package/LICENSE +38 -21
- package/codecov.yml +11 -0
- package/drizzle.config.ts +29 -0
- package/next.config.mjs +3 -0
- package/package.json +24 -4
- package/scripts/migrateServerDB/index.ts +30 -0
- package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +2 -1
- package/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +95 -88
- package/src/app/(main)/chat/settings/features/HeaderContent.tsx +37 -31
- package/src/app/api/webhooks/clerk/__tests__/fixtures/createUser.json +73 -0
- package/src/app/api/webhooks/clerk/route.ts +159 -0
- package/src/app/api/webhooks/clerk/validateRequest.ts +22 -0
- package/src/app/trpc/edge/[trpc]/route.ts +1 -1
- package/src/app/trpc/lambda/[trpc]/route.ts +26 -0
- package/src/config/auth.ts +2 -0
- package/src/config/db.ts +13 -1
- package/src/database/server/core/db.ts +44 -0
- package/src/database/server/core/dbForTest.ts +45 -0
- package/src/database/server/index.ts +1 -0
- package/src/database/server/migrations/0000_init.sql +439 -0
- package/src/database/server/migrations/0001_add_client_id.sql +9 -0
- package/src/database/server/migrations/0002_amusing_puma.sql +9 -0
- package/src/database/server/migrations/meta/0000_snapshot.json +1583 -0
- package/src/database/server/migrations/meta/0001_snapshot.json +1636 -0
- package/src/database/server/migrations/meta/0002_snapshot.json +1630 -0
- package/src/database/server/migrations/meta/_journal.json +27 -0
- package/src/database/server/models/__tests__/file.test.ts +140 -0
- package/src/database/server/models/__tests__/message.test.ts +847 -0
- package/src/database/server/models/__tests__/plugin.test.ts +172 -0
- package/src/database/server/models/__tests__/session.test.ts +595 -0
- package/src/database/server/models/__tests__/topic.test.ts +623 -0
- package/src/database/server/models/__tests__/user.test.ts +173 -0
- package/src/database/server/models/_template.ts +44 -0
- package/src/database/server/models/file.ts +51 -0
- package/src/database/server/models/message.ts +378 -0
- package/src/database/server/models/plugin.ts +63 -0
- package/src/database/server/models/session.ts +290 -0
- package/src/database/server/models/sessionGroup.ts +69 -0
- package/src/database/server/models/topic.ts +265 -0
- package/src/database/server/models/user.ts +138 -0
- package/src/database/server/modules/DataImporter/__tests__/fixtures/messages.json +1101 -0
- package/src/database/server/modules/DataImporter/__tests__/index.test.ts +954 -0
- package/src/database/server/modules/DataImporter/index.ts +333 -0
- package/src/database/server/schemas/_id.ts +15 -0
- package/src/database/server/schemas/lobechat.ts +601 -0
- package/src/database/server/utils/idGenerator.test.ts +39 -0
- package/src/database/server/utils/idGenerator.ts +26 -0
- package/src/features/User/UserPanel/useMenu.tsx +43 -37
- package/src/libs/trpc/client.ts +52 -3
- package/src/server/files/s3.ts +21 -1
- package/src/server/keyVaultsEncrypt/index.test.ts +62 -0
- package/src/server/keyVaultsEncrypt/index.ts +93 -0
- package/src/server/mock.ts +1 -1
- package/src/server/routers/{index.ts → edge/index.ts} +3 -3
- package/src/server/routers/lambda/file.ts +49 -0
- package/src/server/routers/lambda/importer.ts +54 -0
- package/src/server/routers/lambda/index.ts +28 -0
- package/src/server/routers/lambda/message.ts +165 -0
- package/src/server/routers/lambda/plugin.ts +100 -0
- package/src/server/routers/lambda/session.ts +194 -0
- package/src/server/routers/lambda/sessionGroup.ts +77 -0
- package/src/server/routers/lambda/topic.ts +134 -0
- package/src/server/routers/lambda/user.ts +57 -0
- package/src/services/file/index.ts +4 -7
- package/src/services/file/server.ts +45 -0
- package/src/services/import/index.ts +4 -1
- package/src/services/import/server.ts +115 -0
- package/src/services/message/index.ts +4 -8
- package/src/services/message/server.ts +93 -0
- package/src/services/plugin/index.ts +4 -9
- package/src/services/plugin/server.ts +46 -0
- package/src/services/session/index.ts +4 -8
- package/src/services/session/server.ts +148 -0
- package/src/services/topic/index.ts +4 -9
- package/src/services/topic/server.ts +68 -0
- package/src/services/user/index.ts +4 -9
- package/src/services/user/server.ts +28 -0
- package/tests/setup-db.ts +7 -0
- package/vitest.config.ts +2 -1
- 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
|
+
}
|