@lobehub/lobehub 2.0.0-next.137 → 2.0.0-next.138

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.
@@ -54,6 +54,169 @@ const createTestDocument = async (model: DocumentModel, fModel: FileModel, conte
54
54
  };
55
55
 
56
56
  describe('DocumentModel', () => {
57
+ describe('create', () => {
58
+ it('should create a new document', async () => {
59
+ const { id: fileId } = await fileModel.create({
60
+ fileType: 'text/plain',
61
+ name: 'test.txt',
62
+ size: 100,
63
+ url: 'https://example.com/test.txt',
64
+ });
65
+
66
+ const file = await fileModel.findById(fileId);
67
+ if (!file) throw new Error('File not found');
68
+
69
+ const result = await documentModel.create({
70
+ content: 'Test content',
71
+ fileId: file.id,
72
+ fileType: 'text/plain',
73
+ source: file.url,
74
+ sourceType: 'file',
75
+ totalCharCount: 12,
76
+ totalLineCount: 1,
77
+ });
78
+
79
+ expect(result).toBeDefined();
80
+ expect(result.content).toBe('Test content');
81
+ expect(result.fileId).toBe(file.id);
82
+ });
83
+ });
84
+
85
+ describe('delete', () => {
86
+ it('should delete a document', async () => {
87
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
88
+
89
+ await documentModel.delete(documentId);
90
+
91
+ const deleted = await documentModel.findById(documentId);
92
+ expect(deleted).toBeUndefined();
93
+ });
94
+
95
+ it('should not delete document belonging to another user', async () => {
96
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
97
+
98
+ // Try to delete with another user's model
99
+ await documentModel2.delete(documentId);
100
+
101
+ // Document should still exist
102
+ const stillExists = await documentModel.findById(documentId);
103
+ expect(stillExists).toBeDefined();
104
+ });
105
+ });
106
+
107
+ describe('deleteAll', () => {
108
+ it('should delete all documents for the user', async () => {
109
+ await createTestDocument(documentModel, fileModel, 'First document');
110
+ await createTestDocument(documentModel, fileModel, 'Second document');
111
+ await createTestDocument(documentModel2, fileModel2, 'Other user document');
112
+
113
+ await documentModel.deleteAll();
114
+
115
+ const userDocs = await documentModel.query();
116
+ const otherUserDocs = await documentModel2.query();
117
+
118
+ expect(userDocs).toHaveLength(0);
119
+ expect(otherUserDocs).toHaveLength(1);
120
+ });
121
+ });
122
+
123
+ describe('query', () => {
124
+ it('should return all documents for the user', async () => {
125
+ await createTestDocument(documentModel, fileModel, 'First document');
126
+ await createTestDocument(documentModel, fileModel, 'Second document');
127
+
128
+ const result = await documentModel.query();
129
+
130
+ expect(result).toHaveLength(2);
131
+ });
132
+
133
+ it('should only return documents for the current user', async () => {
134
+ await createTestDocument(documentModel, fileModel, 'User 1 document');
135
+ await createTestDocument(documentModel2, fileModel2, 'User 2 document');
136
+
137
+ const result = await documentModel.query();
138
+
139
+ expect(result).toHaveLength(1);
140
+ expect(result[0].content).toBe('User 1 document');
141
+ });
142
+
143
+ it('should return documents ordered by updatedAt desc', async () => {
144
+ const { documentId: doc1Id } = await createTestDocument(
145
+ documentModel,
146
+ fileModel,
147
+ 'First document',
148
+ );
149
+ const { documentId: doc2Id } = await createTestDocument(
150
+ documentModel,
151
+ fileModel,
152
+ 'Second document',
153
+ );
154
+
155
+ // Wait a bit to ensure timestamp difference
156
+ await new Promise((resolve) => setTimeout(resolve, 50));
157
+
158
+ // Update first document to make it more recent
159
+ await documentModel.update(doc1Id, { content: 'Updated first document' });
160
+
161
+ const result = await documentModel.query();
162
+
163
+ expect(result[0].id).toBe(doc1Id);
164
+ expect(result[1].id).toBe(doc2Id);
165
+ });
166
+ });
167
+
168
+ describe('findById', () => {
169
+ it('should find document by id', async () => {
170
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
171
+
172
+ const found = await documentModel.findById(documentId);
173
+
174
+ expect(found).toBeDefined();
175
+ expect(found?.id).toBe(documentId);
176
+ expect(found?.content).toBe('Test content');
177
+ });
178
+
179
+ it('should return undefined for non-existent document', async () => {
180
+ const found = await documentModel.findById('non-existent-id');
181
+
182
+ expect(found).toBeUndefined();
183
+ });
184
+
185
+ it('should not find document belonging to another user', async () => {
186
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
187
+
188
+ const found = await documentModel2.findById(documentId);
189
+
190
+ expect(found).toBeUndefined();
191
+ });
192
+ });
193
+
194
+ describe('update', () => {
195
+ it('should update a document', async () => {
196
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Original content');
197
+
198
+ await documentModel.update(documentId, {
199
+ content: 'Updated content',
200
+ totalCharCount: 15,
201
+ });
202
+
203
+ const updated = await documentModel.findById(documentId);
204
+
205
+ expect(updated?.content).toBe('Updated content');
206
+ expect(updated?.totalCharCount).toBe(15);
207
+ });
208
+
209
+ it('should not update document belonging to another user', async () => {
210
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Original content');
211
+
212
+ await documentModel2.update(documentId, { content: 'Hacked content' });
213
+
214
+ const unchanged = await documentModel.findById(documentId);
215
+
216
+ expect(unchanged?.content).toBe('Original content');
217
+ });
218
+ });
219
+
57
220
  describe('findByFileId', () => {
58
221
  it('should find document by fileId', async () => {
59
222
  const { documentId, file } = await createTestDocument(
@@ -0,0 +1,294 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
+
4
+ import { chunks, embeddings, users } from '../../schemas';
5
+ import { LobeChatDatabase } from '../../type';
6
+ import { EmbeddingModel } from '../embedding';
7
+ import { getTestDB } from './_util';
8
+ import { designThinkingQuery } from './fixtures/embedding';
9
+
10
+ const userId = 'embedding-user-test';
11
+ const otherUserId = 'other-user-test';
12
+
13
+ const serverDB: LobeChatDatabase = await getTestDB();
14
+ const embeddingModel = new EmbeddingModel(serverDB, userId);
15
+
16
+ describe('EmbeddingModel', () => {
17
+ beforeEach(async () => {
18
+ await serverDB.delete(users);
19
+ await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
20
+ });
21
+
22
+ afterEach(async () => {
23
+ await serverDB.delete(users);
24
+ });
25
+
26
+ describe('create', () => {
27
+ it('should create a new embedding', async () => {
28
+ // Create a chunk first
29
+ const [chunk] = await serverDB
30
+ .insert(chunks)
31
+ .values({ text: 'Test chunk', userId })
32
+ .returning();
33
+
34
+ const id = await embeddingModel.create({
35
+ chunkId: chunk.id,
36
+ embeddings: designThinkingQuery,
37
+ model: 'text-embedding-ada-002',
38
+ });
39
+
40
+ expect(id).toBeDefined();
41
+
42
+ const created = await serverDB.query.embeddings.findFirst({
43
+ where: eq(embeddings.id, id),
44
+ });
45
+
46
+ expect(created).toBeDefined();
47
+ expect(created?.chunkId).toBe(chunk.id);
48
+ expect(created?.model).toBe('text-embedding-ada-002');
49
+ expect(created?.userId).toBe(userId);
50
+ });
51
+ });
52
+
53
+ describe('bulkCreate', () => {
54
+ it('should create multiple embeddings', async () => {
55
+ // Create chunks first
56
+ const [chunk1, chunk2] = await serverDB
57
+ .insert(chunks)
58
+ .values([
59
+ { text: 'Test chunk 1', userId },
60
+ { text: 'Test chunk 2', userId },
61
+ ])
62
+ .returning();
63
+
64
+ await embeddingModel.bulkCreate([
65
+ { chunkId: chunk1.id, embeddings: designThinkingQuery, model: 'text-embedding-ada-002' },
66
+ { chunkId: chunk2.id, embeddings: designThinkingQuery, model: 'text-embedding-ada-002' },
67
+ ]);
68
+
69
+ const created = await serverDB.query.embeddings.findMany({
70
+ where: eq(embeddings.userId, userId),
71
+ });
72
+
73
+ expect(created).toHaveLength(2);
74
+ });
75
+
76
+ it('should handle duplicate chunkId with onConflictDoNothing', async () => {
77
+ // Create a chunk
78
+ const [chunk] = await serverDB
79
+ .insert(chunks)
80
+ .values({ text: 'Test chunk', userId })
81
+ .returning();
82
+
83
+ // Create first embedding
84
+ await embeddingModel.create({
85
+ chunkId: chunk.id,
86
+ embeddings: designThinkingQuery,
87
+ model: 'text-embedding-ada-002',
88
+ });
89
+
90
+ // Try to create duplicate
91
+ await embeddingModel.bulkCreate([
92
+ { chunkId: chunk.id, embeddings: designThinkingQuery, model: 'text-embedding-3-small' },
93
+ ]);
94
+
95
+ const created = await serverDB.query.embeddings.findMany({
96
+ where: eq(embeddings.chunkId, chunk.id),
97
+ });
98
+
99
+ // Should still have only 1 embedding due to unique constraint
100
+ expect(created).toHaveLength(1);
101
+ expect(created[0].model).toBe('text-embedding-ada-002');
102
+ });
103
+ });
104
+
105
+ describe('delete', () => {
106
+ it('should delete an embedding', async () => {
107
+ const [chunk] = await serverDB
108
+ .insert(chunks)
109
+ .values({ text: 'Test chunk', userId })
110
+ .returning();
111
+
112
+ const id = await embeddingModel.create({
113
+ chunkId: chunk.id,
114
+ embeddings: designThinkingQuery,
115
+ model: 'text-embedding-ada-002',
116
+ });
117
+
118
+ await embeddingModel.delete(id);
119
+
120
+ const deleted = await serverDB.query.embeddings.findFirst({
121
+ where: eq(embeddings.id, id),
122
+ });
123
+
124
+ expect(deleted).toBeUndefined();
125
+ });
126
+
127
+ it('should not delete embedding belonging to another user', async () => {
128
+ // Create chunk and embedding for other user
129
+ const [chunk] = await serverDB
130
+ .insert(chunks)
131
+ .values({ text: 'Other user chunk', userId: otherUserId })
132
+ .returning();
133
+
134
+ const [otherEmbedding] = await serverDB
135
+ .insert(embeddings)
136
+ .values({
137
+ chunkId: chunk.id,
138
+ embeddings: designThinkingQuery,
139
+ model: 'text-embedding-ada-002',
140
+ userId: otherUserId,
141
+ })
142
+ .returning();
143
+
144
+ await embeddingModel.delete(otherEmbedding.id);
145
+
146
+ const stillExists = await serverDB.query.embeddings.findFirst({
147
+ where: eq(embeddings.id, otherEmbedding.id),
148
+ });
149
+
150
+ expect(stillExists).toBeDefined();
151
+ });
152
+ });
153
+
154
+ describe('query', () => {
155
+ it('should return all embeddings for the user', async () => {
156
+ const [chunk1, chunk2] = await serverDB
157
+ .insert(chunks)
158
+ .values([
159
+ { text: 'Test chunk 1', userId },
160
+ { text: 'Test chunk 2', userId },
161
+ ])
162
+ .returning();
163
+
164
+ await serverDB.insert(embeddings).values([
165
+ { chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
166
+ { chunkId: chunk2.id, embeddings: designThinkingQuery, userId },
167
+ ]);
168
+
169
+ const result = await embeddingModel.query();
170
+
171
+ expect(result).toHaveLength(2);
172
+ });
173
+
174
+ it('should only return embeddings for the current user', async () => {
175
+ const [chunk1] = await serverDB
176
+ .insert(chunks)
177
+ .values([{ text: 'Test chunk 1', userId }])
178
+ .returning();
179
+
180
+ const [chunk2] = await serverDB
181
+ .insert(chunks)
182
+ .values([{ text: 'Other user chunk', userId: otherUserId }])
183
+ .returning();
184
+
185
+ await serverDB.insert(embeddings).values([
186
+ { chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
187
+ { chunkId: chunk2.id, embeddings: designThinkingQuery, userId: otherUserId },
188
+ ]);
189
+
190
+ const result = await embeddingModel.query();
191
+
192
+ expect(result).toHaveLength(1);
193
+ expect(result[0].userId).toBe(userId);
194
+ });
195
+ });
196
+
197
+ describe('findById', () => {
198
+ it('should return an embedding by id', async () => {
199
+ const [chunk] = await serverDB
200
+ .insert(chunks)
201
+ .values({ text: 'Test chunk', userId })
202
+ .returning();
203
+
204
+ const id = await embeddingModel.create({
205
+ chunkId: chunk.id,
206
+ embeddings: designThinkingQuery,
207
+ model: 'text-embedding-ada-002',
208
+ });
209
+
210
+ const result = await embeddingModel.findById(id);
211
+
212
+ expect(result).toBeDefined();
213
+ expect(result?.id).toBe(id);
214
+ expect(result?.chunkId).toBe(chunk.id);
215
+ });
216
+
217
+ it('should return undefined for non-existent embedding', async () => {
218
+ // Use a valid UUID format that doesn't exist
219
+ const result = await embeddingModel.findById('00000000-0000-0000-0000-000000000000');
220
+
221
+ expect(result).toBeUndefined();
222
+ });
223
+
224
+ it('should not return embedding belonging to another user', async () => {
225
+ const [chunk] = await serverDB
226
+ .insert(chunks)
227
+ .values({ text: 'Other user chunk', userId: otherUserId })
228
+ .returning();
229
+
230
+ const [otherEmbedding] = await serverDB
231
+ .insert(embeddings)
232
+ .values({
233
+ chunkId: chunk.id,
234
+ embeddings: designThinkingQuery,
235
+ userId: otherUserId,
236
+ })
237
+ .returning();
238
+
239
+ const result = await embeddingModel.findById(otherEmbedding.id);
240
+
241
+ expect(result).toBeUndefined();
242
+ });
243
+ });
244
+
245
+ describe('countUsage', () => {
246
+ it('should return the count of embeddings for the user', async () => {
247
+ const [chunk1, chunk2, chunk3] = await serverDB
248
+ .insert(chunks)
249
+ .values([
250
+ { text: 'Test chunk 1', userId },
251
+ { text: 'Test chunk 2', userId },
252
+ { text: 'Test chunk 3', userId },
253
+ ])
254
+ .returning();
255
+
256
+ await serverDB.insert(embeddings).values([
257
+ { chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
258
+ { chunkId: chunk2.id, embeddings: designThinkingQuery, userId },
259
+ { chunkId: chunk3.id, embeddings: designThinkingQuery, userId },
260
+ ]);
261
+
262
+ const count = await embeddingModel.countUsage();
263
+
264
+ expect(count).toBe(3);
265
+ });
266
+
267
+ it('should return 0 when user has no embeddings', async () => {
268
+ const count = await embeddingModel.countUsage();
269
+
270
+ expect(count).toBe(0);
271
+ });
272
+
273
+ it('should only count embeddings for the current user', async () => {
274
+ const [chunk1] = await serverDB
275
+ .insert(chunks)
276
+ .values([{ text: 'Test chunk 1', userId }])
277
+ .returning();
278
+
279
+ const [chunk2] = await serverDB
280
+ .insert(chunks)
281
+ .values([{ text: 'Other user chunk', userId: otherUserId }])
282
+ .returning();
283
+
284
+ await serverDB.insert(embeddings).values([
285
+ { chunkId: chunk1.id, embeddings: designThinkingQuery, userId },
286
+ { chunkId: chunk2.id, embeddings: designThinkingQuery, userId: otherUserId },
287
+ ]);
288
+
289
+ const count = await embeddingModel.countUsage();
290
+
291
+ expect(count).toBe(1);
292
+ });
293
+ });
294
+ });