@lobehub/lobehub 2.0.0-next.169 → 2.0.0-next.170

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.
@@ -427,6 +427,13 @@
427
427
  "when": 1765437218969,
428
428
  "tag": "0060_add_user_last_active_at",
429
429
  "breakpoints": true
430
+ },
431
+ {
432
+ "idx": 61,
433
+ "version": "7",
434
+ "when": 1765540257262,
435
+ "tag": "0061_add_document_and_memory_index",
436
+ "breakpoints": true
430
437
  }
431
438
  ],
432
439
  "version": "6"
@@ -946,5 +946,31 @@
946
946
  "bps": true,
947
947
  "folderMillis": 1765437218969,
948
948
  "hash": "004923916a27bc61294eeb20738f2884f1ca43978a03aa9f0216fc849c98465b"
949
+ },
950
+ {
951
+ "sql": [
952
+ "ALTER TABLE \"user_memories\" ADD COLUMN \"captured_at\" timestamp with time zone DEFAULT now() NOT NULL;",
953
+ "\nALTER TABLE \"user_memories_contexts\" ADD COLUMN \"captured_at\" timestamp with time zone DEFAULT now() NOT NULL;",
954
+ "\nALTER TABLE \"user_memories_experiences\" ADD COLUMN \"captured_at\" timestamp with time zone DEFAULT now() NOT NULL;",
955
+ "\nALTER TABLE \"user_memories_identities\" ADD COLUMN \"captured_at\" timestamp with time zone DEFAULT now() NOT NULL;",
956
+ "\nALTER TABLE \"user_memories_preferences\" ADD COLUMN \"captured_at\" timestamp with time zone DEFAULT now() NOT NULL;",
957
+ "\nCREATE INDEX \"agents_user_id_idx\" ON \"agents\" USING btree (\"user_id\");",
958
+ "\nCREATE INDEX \"documents_source_type_idx\" ON \"documents\" USING btree (\"source_type\");",
959
+ "\nCREATE INDEX \"documents_user_id_idx\" ON \"documents\" USING btree (\"user_id\");",
960
+ "\nCREATE INDEX \"files_user_id_idx\" ON \"files\" USING btree (\"user_id\");",
961
+ "\nCREATE INDEX \"users_created_at_idx\" ON \"users\" USING btree (\"created_at\");",
962
+ "\nCREATE INDEX \"users_banned_true_created_at_idx\" ON \"users\" USING btree (\"created_at\") WHERE \"users\".\"banned\" = true;",
963
+ "\nCREATE INDEX \"user_memories_user_id_index\" ON \"user_memories\" USING btree (\"user_id\");",
964
+ "\nCREATE INDEX \"user_memories_contexts_user_id_index\" ON \"user_memories_contexts\" USING btree (\"user_id\");",
965
+ "\nCREATE INDEX \"user_memories_experiences_user_id_index\" ON \"user_memories_experiences\" USING btree (\"user_id\");",
966
+ "\nCREATE INDEX \"user_memories_experiences_user_memory_id_index\" ON \"user_memories_experiences\" USING btree (\"user_memory_id\");",
967
+ "\nCREATE INDEX \"user_memories_identities_user_id_index\" ON \"user_memories_identities\" USING btree (\"user_id\");",
968
+ "\nCREATE INDEX \"user_memories_identities_user_memory_id_index\" ON \"user_memories_identities\" USING btree (\"user_memory_id\");",
969
+ "\nCREATE INDEX \"user_memories_preferences_user_id_index\" ON \"user_memories_preferences\" USING btree (\"user_id\");",
970
+ "\nCREATE INDEX \"user_memories_preferences_user_memory_id_index\" ON \"user_memories_preferences\" USING btree (\"user_memory_id\");"
971
+ ],
972
+ "bps": true,
973
+ "folderMillis": 1765540257262,
974
+ "hash": "2cc13eba2a174ebf18ac32a3ae673338a3dd46ec37a22108a7783a80f44a5286"
949
975
  }
950
976
  ]
@@ -115,8 +115,10 @@ describe('DocumentModel', () => {
115
115
  const userDocs = await documentModel.query();
116
116
  const otherUserDocs = await documentModel2.query();
117
117
 
118
- expect(userDocs).toHaveLength(0);
119
- expect(otherUserDocs).toHaveLength(1);
118
+ expect(userDocs.items).toHaveLength(0);
119
+ expect(userDocs.total).toBe(0);
120
+ expect(otherUserDocs.items).toHaveLength(1);
121
+ expect(otherUserDocs.total).toBe(1);
120
122
  });
121
123
  });
122
124
 
@@ -127,7 +129,8 @@ describe('DocumentModel', () => {
127
129
 
128
130
  const result = await documentModel.query();
129
131
 
130
- expect(result).toHaveLength(2);
132
+ expect(result.items).toHaveLength(2);
133
+ expect(result.total).toBe(2);
131
134
  });
132
135
 
133
136
  it('should only return documents for the current user', async () => {
@@ -136,8 +139,9 @@ describe('DocumentModel', () => {
136
139
 
137
140
  const result = await documentModel.query();
138
141
 
139
- expect(result).toHaveLength(1);
140
- expect(result[0].content).toBe('User 1 document');
142
+ expect(result.items).toHaveLength(1);
143
+ expect(result.total).toBe(1);
144
+ expect(result.items[0].content).toBe(null); // content is excluded in query
141
145
  });
142
146
 
143
147
  it('should return documents ordered by updatedAt desc', async () => {
@@ -160,8 +164,8 @@ describe('DocumentModel', () => {
160
164
 
161
165
  const result = await documentModel.query();
162
166
 
163
- expect(result[0].id).toBe(doc1Id);
164
- expect(result[1].id).toBe(doc2Id);
167
+ expect(result.items[0].id).toBe(doc1Id);
168
+ expect(result.items[1].id).toBe(doc2Id);
165
169
  });
166
170
  });
167
171
 
@@ -109,6 +109,7 @@ function generateRandomCreateUserMemoryExperienceParams() {
109
109
  situationVector: generateRandomEmbedding(),
110
110
  tags: [],
111
111
  type: 'learning',
112
+ capturedAt: new Date(),
112
113
  },
113
114
  } as CreateUserMemoryExperienceParams;
114
115
  }
@@ -127,6 +128,7 @@ function generateRandomCreateUserMemoryIdentityParams() {
127
128
  role: 'role ' + nanoid(),
128
129
  tags: [],
129
130
  type: 'personal',
131
+ capturedAt: new Date(),
130
132
  },
131
133
  } as CreateUserMemoryIdentityParams;
132
134
  }
@@ -148,6 +150,7 @@ function generateRandomCreateUserMemoryContextParams() {
148
150
  titleVector: generateRandomEmbedding(),
149
151
  type: 'environment',
150
152
  userMemoryIds: [],
153
+ capturedAt: new Date(),
151
154
  },
152
155
  } as CreateUserMemoryContextParams;
153
156
  }
@@ -163,6 +166,7 @@ function generateRandomCreateUserMemoryPreferenceParams() {
163
166
  suggestions: 'suggestions ' + nanoid(),
164
167
  tags: [],
165
168
  type: 'choice',
169
+ capturedAt: new Date(),
166
170
  },
167
171
  } as CreateUserMemoryPreferenceParams;
168
172
  }
@@ -1,8 +1,15 @@
1
- import { and, desc, eq } from 'drizzle-orm';
1
+ import { and, count, desc, eq, inArray } from 'drizzle-orm';
2
2
 
3
3
  import { DocumentItem, NewDocument, documents } from '../schemas';
4
4
  import { LobeChatDatabase } from '../type';
5
5
 
6
+ export interface QueryDocumentParams {
7
+ current?: number;
8
+ fileTypes?: string[];
9
+ pageSize?: number;
10
+ sourceTypes?: string[];
11
+ }
12
+
6
13
  export class DocumentModel {
7
14
  private userId: string;
8
15
  private db: LobeChatDatabase;
@@ -31,11 +38,72 @@ export class DocumentModel {
31
38
  return this.db.delete(documents).where(eq(documents.userId, this.userId));
32
39
  };
33
40
 
34
- query = async (): Promise<DocumentItem[]> => {
35
- return this.db.query.documents.findMany({
36
- orderBy: [desc(documents.updatedAt)],
37
- where: eq(documents.userId, this.userId),
38
- });
41
+ query = async ({
42
+ current = 0,
43
+ pageSize = 9999,
44
+ fileTypes,
45
+ sourceTypes,
46
+ }: QueryDocumentParams = {}): Promise<{
47
+ items: DocumentItem[];
48
+ total: number;
49
+ }> => {
50
+ const offset = current * pageSize;
51
+ const conditions = [eq(documents.userId, this.userId)];
52
+
53
+ if (fileTypes?.length) {
54
+ conditions.push(inArray(documents.fileType, fileTypes));
55
+ }
56
+
57
+ if (sourceTypes?.length) {
58
+ conditions.push(inArray(documents.sourceType, sourceTypes as ('file' | 'web' | 'api')[]));
59
+ }
60
+
61
+ const whereCondition = and(...conditions);
62
+
63
+ // Fetch items and total count in parallel
64
+ // Optimize: Exclude large JSONB fields (content, pages, editorData) for better performance
65
+ const [rawItems, totalResult] = await Promise.all([
66
+ this.db
67
+ .select({
68
+ accessedAt: documents.accessedAt,
69
+ clientId: documents.clientId,
70
+ createdAt: documents.createdAt,
71
+ fileId: documents.fileId,
72
+ fileType: documents.fileType,
73
+ filename: documents.filename,
74
+ id: documents.id,
75
+ metadata: documents.metadata,
76
+ parentId: documents.parentId,
77
+ slug: documents.slug,
78
+ source: documents.source,
79
+ sourceType: documents.sourceType,
80
+ title: documents.title,
81
+ totalCharCount: documents.totalCharCount,
82
+ totalLineCount: documents.totalLineCount,
83
+ updatedAt: documents.updatedAt,
84
+ userId: documents.userId,
85
+ // Exclude large fields: content, pages, editorData
86
+ })
87
+ .from(documents)
88
+ .where(whereCondition)
89
+ .orderBy(desc(documents.updatedAt))
90
+ .limit(pageSize)
91
+ .offset(offset),
92
+ this.db
93
+ .select({ count: count(documents.id) })
94
+ .from(documents)
95
+ .where(whereCondition),
96
+ ]);
97
+
98
+ // Map to DocumentItem type with excluded fields as null
99
+ const items = rawItems.map((item) => ({
100
+ ...item,
101
+ content: null,
102
+ editorData: null,
103
+ pages: null,
104
+ })) as DocumentItem[];
105
+
106
+ return { items, total: totalResult[0].count };
39
107
  };
40
108
 
41
109
  findById = async (id: string): Promise<DocumentItem | undefined> => {
@@ -753,6 +753,7 @@ export class UserMemoryModel {
753
753
  accessedAt: userMemoriesContexts.accessedAt,
754
754
  associatedObjects: userMemoriesContexts.associatedObjects,
755
755
  associatedSubjects: userMemoriesContexts.associatedSubjects,
756
+ capturedAt: userMemoriesContexts.capturedAt,
756
757
  createdAt: userMemoriesContexts.createdAt,
757
758
  currentStatus: userMemoriesContexts.currentStatus,
758
759
  description: userMemoriesContexts.description,
@@ -803,6 +804,7 @@ export class UserMemoryModel {
803
804
  .select({
804
805
  accessedAt: userMemoriesExperiences.accessedAt,
805
806
  action: userMemoriesExperiences.action,
807
+ capturedAt: userMemoriesExperiences.capturedAt,
806
808
  createdAt: userMemoriesExperiences.createdAt,
807
809
  id: userMemoriesExperiences.id,
808
810
  keyLearning: userMemoriesExperiences.keyLearning,
@@ -852,6 +854,7 @@ export class UserMemoryModel {
852
854
  let query = this.db
853
855
  .select({
854
856
  accessedAt: userMemoriesPreferences.accessedAt,
857
+ capturedAt: userMemoriesPreferences.capturedAt,
855
858
  conclusionDirectives: userMemoriesPreferences.conclusionDirectives,
856
859
  createdAt: userMemoriesPreferences.createdAt,
857
860
  id: userMemoriesPreferences.id,
@@ -64,6 +64,7 @@ export const agents = pgTable(
64
64
  (t) => [
65
65
  uniqueIndex('client_id_user_id_unique').on(t.clientId, t.userId),
66
66
  uniqueIndex('agents_slug_user_id_unique').on(t.slug, t.userId),
67
+ index('agents_user_id_idx').on(t.userId),
67
68
  index('agents_title_idx').on(t.title),
68
69
  index('agents_description_idx').on(t.description),
69
70
  ],
@@ -97,6 +97,8 @@ export const documents = pgTable(
97
97
  (table) => [
98
98
  index('documents_source_idx').on(table.source),
99
99
  index('documents_file_type_idx').on(table.fileType),
100
+ index('documents_source_type_idx').on(table.sourceType),
101
+ index('documents_user_id_idx').on(table.userId),
100
102
  index('documents_file_id_idx').on(table.fileId),
101
103
  index('documents_parent_id_idx').on(table.parentId),
102
104
  uniqueIndex('documents_client_id_user_id_unique').on(table.clientId, table.userId),
@@ -152,6 +154,7 @@ export const files = pgTable(
152
154
  (table) => {
153
155
  return {
154
156
  fileHashIdx: index('file_hash_idx').on(table.fileHash),
157
+ userIdIdx: index('files_user_id_idx').on(table.userId),
155
158
  parentIdIdx: index('files_parent_id_idx').on(table.parentId),
156
159
  clientIdUnique: uniqueIndex('files_client_id_user_id_unique').on(
157
160
  table.clientId,
@@ -2,6 +2,7 @@
2
2
  import { DEFAULT_PREFERENCE } from '@lobechat/const';
3
3
  import type { CustomPluginParams } from '@lobechat/types';
4
4
  import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk';
5
+ import { sql } from 'drizzle-orm';
5
6
  import { boolean, index, jsonb, pgTable, primaryKey, text } from 'drizzle-orm/pg-core';
6
7
 
7
8
  import { timestamps, timestamptz, varchar255 } from './_helpers';
@@ -46,10 +47,18 @@ export const users = pgTable(
46
47
 
47
48
  ...timestamps,
48
49
  },
49
- (table) => ({
50
- emailIdx: index('users_email_idx').on(table.email),
51
- usernameIdx: index('users_username_idx').on(table.username),
52
- }),
50
+ (table) => [
51
+ index('users_email_idx').on(table.email),
52
+ index('users_username_idx').on(table.username),
53
+ index('users_created_at_idx').on(table.createdAt),
54
+ /**
55
+ * Partial index to speed up admin queries on banned users.
56
+ * Only rows with banned=true are indexed.
57
+ */
58
+ index('users_banned_true_created_at_idx')
59
+ .on(table.createdAt)
60
+ .where(sql`${table.banned} = true`),
61
+ ],
53
62
  );
54
63
 
55
64
  export type NewUser = typeof users.$inferInsert;
@@ -30,6 +30,7 @@ export const userMemories = pgTable(
30
30
 
31
31
  accessedCount: bigint('accessed_count', { mode: 'number' }).default(0),
32
32
  lastAccessedAt: timestamptz('last_accessed_at').notNull(),
33
+ capturedAt: timestamptz('captured_at').notNull().defaultNow(),
33
34
 
34
35
  ...timestamps,
35
36
  },
@@ -42,6 +43,7 @@ export const userMemories = pgTable(
42
43
  'hnsw',
43
44
  table.detailsVector1024.op('vector_cosine_ops'),
44
45
  ),
46
+ index('user_memories_user_id_index').on(table.userId),
45
47
  ],
46
48
  );
47
49
 
@@ -72,6 +74,8 @@ export const userMemoriesContexts = pgTable(
72
74
  scoreImpact: numeric('score_impact', { mode: 'number' }).default(0),
73
75
  scoreUrgency: numeric('score_urgency', { mode: 'number' }).default(0),
74
76
 
77
+ capturedAt: timestamptz('captured_at').notNull().defaultNow(),
78
+
75
79
  ...timestamps,
76
80
  },
77
81
  (table) => [
@@ -84,6 +88,7 @@ export const userMemoriesContexts = pgTable(
84
88
  table.descriptionVector.op('vector_cosine_ops'),
85
89
  ),
86
90
  index('user_memories_contexts_type_index').on(table.type),
91
+ index('user_memories_contexts_user_id_index').on(table.userId),
87
92
  ],
88
93
  );
89
94
 
@@ -110,6 +115,8 @@ export const userMemoriesPreferences = pgTable(
110
115
 
111
116
  scorePriority: numeric('score_priority', { mode: 'number' }).default(0),
112
117
 
118
+ capturedAt: timestamptz('captured_at').notNull().defaultNow(),
119
+
113
120
  ...timestamps,
114
121
  },
115
122
  (table) => [
@@ -117,6 +124,8 @@ export const userMemoriesPreferences = pgTable(
117
124
  'hnsw',
118
125
  table.conclusionDirectivesVector.op('vector_cosine_ops'),
119
126
  ),
127
+ index('user_memories_preferences_user_id_index').on(table.userId),
128
+ index('user_memories_preferences_user_memory_id_index').on(table.userMemoryId),
120
129
  ],
121
130
  );
122
131
 
@@ -142,6 +151,8 @@ export const userMemoriesIdentities = pgTable(
142
151
  relationship: varchar255('relationship'),
143
152
  role: text('role'),
144
153
 
154
+ capturedAt: timestamptz('captured_at').notNull().defaultNow(),
155
+
145
156
  ...timestamps,
146
157
  },
147
158
  (table) => [
@@ -150,6 +161,8 @@ export const userMemoriesIdentities = pgTable(
150
161
  table.descriptionVector.op('vector_cosine_ops'),
151
162
  ),
152
163
  index('user_memories_identities_type_index').on(table.type),
164
+ index('user_memories_identities_user_id_index').on(table.userId),
165
+ index('user_memories_identities_user_memory_id_index').on(table.userMemoryId),
153
166
  ],
154
167
  );
155
168
 
@@ -180,6 +193,8 @@ export const userMemoriesExperiences = pgTable(
180
193
 
181
194
  scoreConfidence: real('score_confidence').default(0),
182
195
 
196
+ capturedAt: timestamptz('captured_at').notNull().defaultNow(),
197
+
183
198
  ...timestamps,
184
199
  },
185
200
  (table) => [
@@ -196,6 +211,8 @@ export const userMemoriesExperiences = pgTable(
196
211
  table.keyLearningVector.op('vector_cosine_ops'),
197
212
  ),
198
213
  index('user_memories_experiences_type_index').on(table.type),
214
+ index('user_memories_experiences_user_id_index').on(table.userId),
215
+ index('user_memories_experiences_user_memory_id_index').on(table.userMemoryId),
199
216
  ],
200
217
  );
201
218
 
@@ -24,7 +24,7 @@ export class DocumentService {
24
24
  return lambdaClient.document.createDocument.mutate(params);
25
25
  }
26
26
 
27
- async queryDocuments(): Promise<DocumentItem[]> {
27
+ async queryDocuments(): Promise<{ items: DocumentItem[]; total: number }> {
28
28
  return lambdaClient.document.queryDocuments.query();
29
29
  }
30
30
 
@@ -189,7 +189,7 @@ export const createDocumentSlice: StateCreator<
189
189
  set({ isDocumentListLoading: true }, false, n('fetchDocuments/start'));
190
190
 
191
191
  try {
192
- const documentItems = await documentService.queryDocuments();
192
+ const { items: documentItems } = await documentService.queryDocuments();
193
193
  const documents = documentItems.filter(isAllowedDocument).map((doc) => ({
194
194
  ...doc,
195
195
  filename: doc.filename ?? doc.title ?? 'Untitled',