@lobehub/lobehub 2.0.0-next.196 → 2.0.0-next.198

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 (80) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +21 -0
  3. package/package.json +2 -3
  4. package/packages/database/src/core/getTestDB.ts +50 -0
  5. package/packages/database/src/models/__tests__/_test_template.ts +1 -1
  6. package/packages/database/src/models/__tests__/agent.test.ts +1 -1
  7. package/packages/database/src/models/__tests__/aiModel.test.ts +1 -1
  8. package/packages/database/src/models/__tests__/aiProvider.test.ts +1 -1
  9. package/packages/database/src/models/__tests__/apiKey.test.ts +1 -1
  10. package/packages/database/src/models/__tests__/asyncTask.test.ts +1 -1
  11. package/packages/database/src/models/__tests__/chatGroup.test.ts +1 -1
  12. package/packages/database/src/models/__tests__/chunk.test.ts +1 -1
  13. package/packages/database/src/models/__tests__/document.test.ts +1 -1
  14. package/packages/database/src/models/__tests__/drizzleMigration.test.ts +1 -1
  15. package/packages/database/src/models/__tests__/embedding.test.ts +1 -1
  16. package/packages/database/src/models/__tests__/file.test.ts +1 -1
  17. package/packages/database/src/models/__tests__/generation.test.ts +1 -1
  18. package/packages/database/src/models/__tests__/generationBatch.test.ts +1 -1
  19. package/packages/database/src/models/__tests__/generationTopic.test.ts +1 -1
  20. package/packages/database/src/models/__tests__/knowledgeBase.test.ts +1 -1
  21. package/packages/database/src/models/__tests__/messages/message.create.test.ts +1 -1
  22. package/packages/database/src/models/__tests__/messages/message.delete.test.ts +1 -1
  23. package/packages/database/src/models/__tests__/messages/message.query.test.ts +1 -1
  24. package/packages/database/src/models/__tests__/messages/message.stats.test.ts +1 -1
  25. package/packages/database/src/models/__tests__/messages/message.thread-query.test.ts +1 -1
  26. package/packages/database/src/models/__tests__/messages/message.update.test.ts +1 -1
  27. package/packages/database/src/models/__tests__/messages/messageWithTask.test.ts +1 -1
  28. package/packages/database/src/models/__tests__/messages/queryWithMessageGroup.perf.test.ts +1 -1
  29. package/packages/database/src/models/__tests__/messages/queryWithMessageGroup.test.ts +1 -1
  30. package/packages/database/src/models/__tests__/oauthHandoff.test.ts +1 -1
  31. package/packages/database/src/models/__tests__/plugin.test.ts +1 -1
  32. package/packages/database/src/models/__tests__/session.test.ts +1 -1
  33. package/packages/database/src/models/__tests__/sessionGroup.test.ts +1 -1
  34. package/packages/database/src/models/__tests__/thread.test.ts +1 -1
  35. package/packages/database/src/models/__tests__/topicDocument.test.ts +1 -1
  36. package/packages/database/src/models/__tests__/topics/topic.create.test.ts +1 -1
  37. package/packages/database/src/models/__tests__/topics/topic.delete.test.ts +1 -1
  38. package/packages/database/src/models/__tests__/topics/topic.query.test.ts +1 -1
  39. package/packages/database/src/models/__tests__/topics/topic.stats.test.ts +1 -1
  40. package/packages/database/src/models/__tests__/topics/topic.update.test.ts +1 -1
  41. package/packages/database/src/models/__tests__/user.test.ts +1 -1
  42. package/packages/database/src/models/__tests__/userMemories.test.ts +1 -1
  43. package/packages/database/src/models/__tests__/userMemoryIdentity.test.ts +1 -1
  44. package/packages/database/src/models/userMemory/__tests__/context.test.ts +1 -1
  45. package/packages/database/src/models/userMemory/__tests__/experience.test.ts +1 -1
  46. package/packages/database/src/models/userMemory/__tests__/identity.test.ts +1 -1
  47. package/packages/database/src/models/userMemory/__tests__/preference.test.ts +1 -1
  48. package/packages/database/src/repositories/agentGroup/index.test.ts +1 -1
  49. package/packages/database/src/repositories/agentMigration/__tests__/agentMigrationRepo.test.ts +1 -1
  50. package/packages/database/src/repositories/aiInfra/index.test.ts +1 -1
  51. package/packages/database/src/repositories/compression/index.test.ts +1 -1
  52. package/packages/database/src/repositories/dataExporter/index.test.ts +1 -1
  53. package/packages/database/src/repositories/dataImporter/__tests__/index.test.ts +1 -1
  54. package/packages/database/src/repositories/dataImporter/deprecated/__tests__/index.test.ts +2 -2
  55. package/packages/database/src/repositories/home/__tests__/index.test.ts +1 -1
  56. package/packages/database/src/repositories/home/index.test.ts +1 -1
  57. package/packages/database/src/repositories/knowledge/index.test.ts +1 -1
  58. package/packages/database/src/repositories/search/index.test.ts +1 -1
  59. package/packages/database/src/repositories/topicImporter/__tests__/importTopic.test.ts +1 -1
  60. package/packages/database/src/repositories/userMemory/__tests__/UserMemoryTopicRepository.test.ts +1 -1
  61. package/packages/database/src/server/models/__tests__/adapter.test.ts +2 -2
  62. package/packages/database/src/server/models/__tests__/user.test.ts +2 -2
  63. package/packages/database/tests/test-utils.ts +1 -1
  64. package/packages/model-runtime/src/core/streams/openai/openai.test.ts +155 -0
  65. package/packages/model-runtime/src/core/streams/openai/openai.ts +26 -1
  66. package/packages/types/src/export.ts +1 -1
  67. package/packages/types/src/index.ts +0 -1
  68. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +1 -0
  69. package/src/features/LibraryModal/AssignKnowledgeBase/List.tsx +12 -13
  70. package/src/features/ResourceManager/components/Explorer/MasonryView/index.tsx +1 -0
  71. package/src/server/routers/lambda/__tests__/file.test.ts +76 -3
  72. package/src/server/routers/lambda/file.ts +13 -1
  73. package/src/services/config.ts +2 -16
  74. package/packages/database/src/core/dbForTest.ts +0 -43
  75. package/packages/database/src/core/migrations.json +0 -1080
  76. package/packages/database/src/models/__tests__/_util.ts +0 -30
  77. package/packages/database/src/repositories/tableViewer/index.test.ts +0 -255
  78. package/packages/database/src/repositories/tableViewer/index.ts +0 -251
  79. package/packages/types/src/tableViewer.ts +0 -30
  80. package/scripts/migrateClientDB/compile-migrations.ts +0 -14
@@ -8,7 +8,7 @@ import { AiProviderModelListItem, EnabledAiModel } from 'model-bank';
8
8
  import { DEFAULT_MODEL_PROVIDER_LIST } from 'model-bank/modelProviders';
9
9
  import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
10
10
 
11
- import { getTestDB } from '../../models/__tests__/_util';
11
+ import { getTestDB } from '../../core/getTestDB';
12
12
  import { LobeChatDatabase } from '../../type';
13
13
  import { AiInfraRepos } from './index';
14
14
 
@@ -2,7 +2,7 @@
2
2
  import { MessageGroupType } from '@lobechat/types';
3
3
  import { beforeEach, describe, expect, it } from 'vitest';
4
4
 
5
- import { getTestDB } from '../../models/__tests__/_util';
5
+ import { getTestDB } from '../../core/getTestDB';
6
6
  import { messageGroups, messages } from '../../schemas/message';
7
7
  import { topics } from '../../schemas/topic';
8
8
  import { users } from '../../schemas/user';
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { getTestDB } from '../../models/__tests__/_util';
3
+ import { getTestDB } from '../../core/getTestDB';
4
4
  import {
5
5
  agents,
6
6
  agentsKnowledgeBases,
@@ -2,7 +2,7 @@ import type { ImportPgDataStructure } from '@lobechat/types';
2
2
  import { eq, inArray } from 'drizzle-orm';
3
3
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
- import { getTestDB } from '../../../models/__tests__/_util';
5
+ import { getTestDB } from '../../../core/getTestDB';
6
6
  import * as Schema from '../../../schemas';
7
7
  import { DataImporterRepos } from '../index';
8
8
  import agentsData from './fixtures/agents.json';
@@ -3,7 +3,7 @@ import type { ImporterEntryData } from '@lobechat/types';
3
3
  import { eq, inArray } from 'drizzle-orm';
4
4
  import { beforeEach, describe, expect, it, vi } from 'vitest';
5
5
 
6
- import { getTestDBInstance } from '@/database/core/dbForTest';
6
+ import { getTestDB } from '../../../../core/getTestDB';
7
7
  import {
8
8
  agents,
9
9
  agentsToSessions,
@@ -19,7 +19,7 @@ import mockImportData from './fixtures/messages.json';
19
19
 
20
20
  const CURRENT_CONFIG_VERSION = 7;
21
21
 
22
- const serverDB = await getTestDBInstance();
22
+ const serverDB = await getTestDB();
23
23
 
24
24
  const userId = 'test-user-id';
25
25
  let importer: DataImporterRepos;
@@ -1,7 +1,7 @@
1
1
  import { eq } from 'drizzle-orm';
2
2
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
3
 
4
- import { getTestDB } from '../../../models/__tests__/_util';
4
+ import { getTestDB } from '../../../core/getTestDB';
5
5
  import * as Schema from '../../../schemas';
6
6
  import { HomeRepository } from '../index';
7
7
 
@@ -1,7 +1,7 @@
1
1
  // @vitest-environment node
2
2
  import { beforeEach, describe, expect, it } from 'vitest';
3
3
 
4
- import { getTestDB } from '../../models/__tests__/_util';
4
+ import { getTestDB } from '../../core/getTestDB';
5
5
  import { NewAgent, agents } from '../../schemas/agent';
6
6
  import { NewChatGroup, chatGroups } from '../../schemas/chatGroup';
7
7
  import { agentsToSessions } from '../../schemas/relations';
@@ -2,7 +2,7 @@
2
2
  import { FilesTabs } from '@lobechat/types';
3
3
  import { beforeEach, describe, expect, it } from 'vitest';
4
4
 
5
- import { getTestDB } from '../../models/__tests__/_util';
5
+ import { getTestDB } from '../../core/getTestDB';
6
6
  import { NewDocument, documents } from '../../schemas/file';
7
7
  import { NewFile, files } from '../../schemas/file';
8
8
  import { users } from '../../schemas/user';
@@ -1,7 +1,7 @@
1
1
  // @vitest-environment node
2
2
  import { beforeEach, describe, expect, it } from 'vitest';
3
3
 
4
- import { getTestDB } from '../../models/__tests__/_util';
4
+ import { getTestDB } from '../../core/getTestDB';
5
5
  import { NewAgent, agents } from '../../schemas/agent';
6
6
  import { NewFile, files } from '../../schemas/file';
7
7
  import { messages } from '../../schemas/message';
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
6
 
7
- import { getTestDB } from '../../../models/__tests__/_util';
7
+ import { getTestDB } from '../../../core/getTestDB';
8
8
  import { agents, messagePlugins, messages, topics, users } from '../../../schemas';
9
9
  import { LobeChatDatabase } from '../../../type';
10
10
  import { TopicImporterRepo } from '../index';
@@ -1,7 +1,7 @@
1
1
  // @vitest-environment node
2
2
  import { beforeEach, describe, expect, it } from 'vitest';
3
3
 
4
- import { getTestDB } from '../../../models/__tests__/_util';
4
+ import { getTestDB } from '../../../core/getTestDB';
5
5
  import { messages } from '../../../schemas/message';
6
6
  import { topics } from '../../../schemas/topic';
7
7
  import { users } from '../../../schemas/user';
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
3
 
4
4
  import { DrizzleAdapter } from '@/libs/oidc-provider/adapter';
5
5
 
6
- import { getTestDBInstance } from '../../../core/dbForTest';
6
+ import { getTestDB } from '../../../core/getTestDB';
7
7
  import { users } from '../../../schemas';
8
8
  import {
9
9
  oidcAccessTokens,
@@ -14,7 +14,7 @@ import {
14
14
  oidcSessions,
15
15
  } from '../../../schemas/oidc';
16
16
 
17
- let serverDB = await getTestDBInstance();
17
+ let serverDB = await getTestDB();
18
18
 
19
19
  // Test data
20
20
  const testModelName = 'Session';
@@ -7,12 +7,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
7
 
8
8
  import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
9
9
 
10
- import { getTestDBInstance } from '../../../core/dbForTest';
10
+ import { getTestDB } from '../../../core/getTestDB';
11
11
  import { SessionModel } from '../../../models/session';
12
12
  import { UserModel, UserNotFoundError } from '../../../models/user';
13
13
  import { UserSettingsItem, nextauthAccounts, userSettings, users } from '../../../schemas';
14
14
 
15
- let serverDB = await getTestDBInstance();
15
+ let serverDB = await getTestDB();
16
16
 
17
17
  const userId = 'user-db';
18
18
  const userEmail = 'user@example.com';
@@ -1 +1 @@
1
- export * from '../src/models/__tests__/_util';
1
+ export * from '../src/core/getTestDB';
@@ -1048,6 +1048,161 @@ describe('OpenAIStream', () => {
1048
1048
  ].map((i) => `${i}\n`),
1049
1049
  );
1050
1050
  });
1051
+
1052
+ it('should handle OpenRouter tool calls with thoughtSignature (for Gemini models)', async () => {
1053
+ // OpenRouter returns thoughtSignature in tool_calls for Gemini models
1054
+ // This is required for preserving reasoning blocks across turns
1055
+ // Ref: https://openrouter.ai/docs/guides/best-practices/reasoning-tokens
1056
+ const mockOpenAIStream = new ReadableStream({
1057
+ start(controller) {
1058
+ controller.enqueue({
1059
+ choices: [
1060
+ {
1061
+ delta: {
1062
+ tool_calls: [
1063
+ {
1064
+ function: { name: 'github__get_me', arguments: '{}' },
1065
+ id: 'call_123',
1066
+ index: 0,
1067
+ type: 'function',
1068
+ // OpenRouter adds thoughtSignature for Gemini 3 models
1069
+ thoughtSignature: 'ErEDCq4DAdHtim...',
1070
+ },
1071
+ ],
1072
+ },
1073
+ index: 0,
1074
+ },
1075
+ ],
1076
+ id: 'or-123',
1077
+ });
1078
+
1079
+ controller.close();
1080
+ },
1081
+ });
1082
+
1083
+ const onToolCallMock = vi.fn();
1084
+
1085
+ const protocolStream = OpenAIStream(mockOpenAIStream, {
1086
+ callbacks: {
1087
+ onToolsCalling: onToolCallMock,
1088
+ },
1089
+ });
1090
+
1091
+ const decoder = new TextDecoder();
1092
+ const chunks = [];
1093
+
1094
+ // @ts-ignore
1095
+ for await (const chunk of protocolStream) {
1096
+ chunks.push(decoder.decode(chunk, { stream: true }));
1097
+ }
1098
+
1099
+ expect(chunks).toEqual([
1100
+ 'id: or-123\n',
1101
+ 'event: tool_calls\n',
1102
+ // thoughtSignature should be preserved in the output
1103
+ `data: [{"function":{"arguments":"{}","name":"github__get_me"},"id":"call_123","index":0,"type":"function","thoughtSignature":"ErEDCq4DAdHtim..."}]\n\n`,
1104
+ ]);
1105
+
1106
+ // Verify the callback receives thoughtSignature
1107
+ expect(onToolCallMock).toHaveBeenCalledWith({
1108
+ chunk: [
1109
+ {
1110
+ function: { arguments: '{}', name: 'github__get_me' },
1111
+ id: 'call_123',
1112
+ index: 0,
1113
+ thoughtSignature: 'ErEDCq4DAdHtim...',
1114
+ type: 'function',
1115
+ },
1116
+ ],
1117
+ toolsCalling: [
1118
+ {
1119
+ function: { arguments: '{}', name: 'github__get_me' },
1120
+ id: 'call_123',
1121
+ thoughtSignature: 'ErEDCq4DAdHtim...',
1122
+ type: 'function',
1123
+ },
1124
+ ],
1125
+ });
1126
+ });
1127
+
1128
+ it('should NOT include thoughtSignature in output when not present in tool call', async () => {
1129
+ // Standard tool calls without thoughtSignature should not include the field
1130
+ const mockOpenAIStream = new ReadableStream({
1131
+ start(controller) {
1132
+ controller.enqueue({
1133
+ choices: [
1134
+ {
1135
+ delta: {
1136
+ tool_calls: [
1137
+ {
1138
+ function: { name: 'search', arguments: '{"query":"test"}' },
1139
+ id: 'call_456',
1140
+ index: 0,
1141
+ type: 'function',
1142
+ // No thoughtSignature field
1143
+ },
1144
+ ],
1145
+ },
1146
+ index: 0,
1147
+ },
1148
+ ],
1149
+ id: 'standard-123',
1150
+ });
1151
+
1152
+ controller.close();
1153
+ },
1154
+ });
1155
+
1156
+ const onToolCallMock = vi.fn();
1157
+
1158
+ const protocolStream = OpenAIStream(mockOpenAIStream, {
1159
+ callbacks: {
1160
+ onToolsCalling: onToolCallMock,
1161
+ },
1162
+ });
1163
+
1164
+ const decoder = new TextDecoder();
1165
+ const chunks = [];
1166
+
1167
+ // @ts-ignore
1168
+ for await (const chunk of protocolStream) {
1169
+ chunks.push(decoder.decode(chunk, { stream: true }));
1170
+ }
1171
+
1172
+ expect(chunks).toEqual([
1173
+ 'id: standard-123\n',
1174
+ 'event: tool_calls\n',
1175
+ // thoughtSignature should NOT be in the output
1176
+ `data: [{"function":{"arguments":"{\\"query\\":\\"test\\"}","name":"search"},"id":"call_456","index":0,"type":"function"}]\n\n`,
1177
+ ]);
1178
+
1179
+ // Verify the callback does NOT receive thoughtSignature
1180
+ expect(onToolCallMock).toHaveBeenCalledWith({
1181
+ chunk: [
1182
+ {
1183
+ function: { arguments: '{"query":"test"}', name: 'search' },
1184
+ id: 'call_456',
1185
+ index: 0,
1186
+ // thoughtSignature should not be present
1187
+ type: 'function',
1188
+ },
1189
+ ],
1190
+ toolsCalling: [
1191
+ {
1192
+ function: { arguments: '{"query":"test"}', name: 'search' },
1193
+ id: 'call_456',
1194
+ // thoughtSignature should not be present
1195
+ type: 'function',
1196
+ },
1197
+ ],
1198
+ });
1199
+
1200
+ // Verify thoughtSignature is not in the chunk
1201
+ expect(onToolCallMock.mock.calls[0][0].chunk[0]).not.toHaveProperty('thoughtSignature');
1202
+ expect(onToolCallMock.mock.calls[0][0].toolsCalling[0]).not.toHaveProperty(
1203
+ 'thoughtSignature',
1204
+ );
1205
+ });
1051
1206
  });
1052
1207
 
1053
1208
  describe('Reasoning', () => {
@@ -20,6 +20,23 @@ import {
20
20
  generateToolCallId,
21
21
  } from '../protocol';
22
22
 
23
+ /**
24
+ * Extended type for OpenAI tool calls that includes provider-specific extensions
25
+ * like OpenRouter's thoughtSignature for Gemini models
26
+ */
27
+ type OpenAIExtendedToolCall = OpenAI.ChatCompletionChunk.Choice.Delta.ToolCall & {
28
+ thoughtSignature?: string;
29
+ };
30
+
31
+ /**
32
+ * Type guard to check if a tool call has thoughtSignature
33
+ */
34
+ const hasThoughtSignature = (
35
+ toolCall: OpenAI.ChatCompletionChunk.Choice.Delta.ToolCall,
36
+ ): toolCall is OpenAIExtendedToolCall => {
37
+ return 'thoughtSignature' in toolCall && typeof toolCall.thoughtSignature === 'string';
38
+ };
39
+
23
40
  // Process markdown base64 images: extract URLs and clean text in one pass
24
41
  const processMarkdownBase64Images = (text: string): { cleanedText: string; urls: string[] } => {
25
42
  if (!text) return { cleanedText: text, urls: [] };
@@ -150,7 +167,7 @@ const transformOpenAIStream = (
150
167
  };
151
168
  }
152
169
 
153
- return {
170
+ const baseData: StreamToolCallChunkData = {
154
171
  function: {
155
172
  arguments: value.function?.arguments ?? '',
156
173
  name: value.function?.name ?? null,
@@ -170,6 +187,14 @@ const transformOpenAIStream = (
170
187
  index: typeof value.index !== 'undefined' ? value.index : index,
171
188
  type: value.type || 'function',
172
189
  };
190
+
191
+ // OpenRouter returns thoughtSignature in tool_calls for Gemini models (e.g. gemini-3-flash-preview)
192
+ // [{"id":"call_123","type":"function","function":{"name":"get_weather","arguments":"{}"},"thoughtSignature":"abc123"}]
193
+ if (hasThoughtSignature(value)) {
194
+ baseData.thoughtSignature = value.thoughtSignature;
195
+ }
196
+
197
+ return baseData;
173
198
  }),
174
199
  id: chunk.id,
175
200
  type: 'tool_calls',
@@ -1,6 +1,6 @@
1
1
  export interface ExportDatabaseData {
2
2
  data: Record<string, object[]>;
3
- schemaHash?: string;
3
+ schemaHash: string;
4
4
  url?: string;
5
5
  }
6
6
 
@@ -29,7 +29,6 @@ export * from './serverConfig';
29
29
  export * from './service';
30
30
  export * from './session';
31
31
  export * from './stepContext';
32
- export * from './tableViewer';
33
32
  export * from './tool';
34
33
  export * from './topic';
35
34
  export * from './user';
@@ -52,6 +52,7 @@ const Token = memo<TokenTagProps>(({ total: messageString }) => {
52
52
  chatConfigByIdSelectors.getEnableHistoryCountById(agentId)(s),
53
53
  // need to re-render by search mode
54
54
  chatConfigByIdSelectors.isEnableSearchById(agentId)(s),
55
+ chatConfigByIdSelectors.getUseModelBuiltinSearchById(agentId)(s),
55
56
  ]);
56
57
 
57
58
  const maxTokens = useModelContextWindowTokens(model, provider);
@@ -114,19 +114,18 @@ export const List = memo(() => {
114
114
  totalCount={data!.length}
115
115
  />
116
116
  ) : (
117
- <div style={{ flex: 1, overflow: 'hidden' }}>
118
- <div style={{ height: '100%', overflowY: 'auto' }}>
119
- <div style={{ paddingInline: 16 }}>
120
- <VirtuosoMasonry
121
- ItemContent={MasonryItemWrapper}
122
- columnCount={columnCount}
123
- context={masonryContext}
124
- data={data || []}
125
- style={{
126
- gap: '16px',
127
- }}
128
- />
129
- </div>
117
+ <div style={{ height: '100%', position: 'relative' }}>
118
+ <div style={{ inset: 0, position: 'absolute' }}>
119
+ <VirtuosoMasonry
120
+ ItemContent={MasonryItemWrapper}
121
+ columnCount={columnCount}
122
+ context={masonryContext}
123
+ data={data || []}
124
+ style={{
125
+ gap: '16px',
126
+ height: '100%',
127
+ }}
128
+ />
130
129
  </div>
131
130
  </div>
132
131
  )}
@@ -87,6 +87,7 @@ const MasonryView = memo<MasonryViewProps>(
87
87
  data={data || []}
88
88
  style={{
89
89
  gap: '16px',
90
+ overflow: 'hidden',
90
91
  }}
91
92
  />
92
93
  {isLoadingMore && (
@@ -245,7 +245,35 @@ describe('fileRouter', () => {
245
245
  );
246
246
  });
247
247
 
248
- it('should handle getFileMetadata errors', async () => {
248
+ it('should fallback to input size when getFileMetadata fails', async () => {
249
+ mockFileModelCheckHash.mockResolvedValue({ isExist: false });
250
+ mockFileModelCreate.mockResolvedValue({ id: 'new-file-id' });
251
+ mockFileServiceGetFileMetadata.mockRejectedValue(new Error('File not found in S3'));
252
+
253
+ const result = await caller.createFile({
254
+ hash: 'test-hash',
255
+ fileType: 'text',
256
+ name: 'test.txt',
257
+ size: 100,
258
+ url: 'files/non-existent.txt',
259
+ metadata: {},
260
+ });
261
+
262
+ expect(result).toEqual({
263
+ id: 'new-file-id',
264
+ url: 'https://lobehub.com/f/new-file-id',
265
+ });
266
+
267
+ // Verify create was called with input size as fallback
268
+ expect(mockFileModelCreate).toHaveBeenCalledWith(
269
+ expect.objectContaining({
270
+ size: 100,
271
+ }),
272
+ true,
273
+ );
274
+ });
275
+
276
+ it('should throw error when getFileMetadata fails and input size is less than 1', async () => {
249
277
  mockFileModelCheckHash.mockResolvedValue({ isExist: false });
250
278
  mockFileServiceGetFileMetadata.mockRejectedValue(new Error('File not found in S3'));
251
279
 
@@ -254,11 +282,56 @@ describe('fileRouter', () => {
254
282
  hash: 'test-hash',
255
283
  fileType: 'text',
256
284
  name: 'test.txt',
257
- size: 100,
285
+ size: 0,
258
286
  url: 'files/non-existent.txt',
259
287
  metadata: {},
260
288
  }),
261
- ).rejects.toThrow('File not found in S3');
289
+ ).rejects.toThrow('File size must be at least 1 byte');
290
+ });
291
+
292
+ it('should use input size when getFileMetadata returns contentLength less than 1', async () => {
293
+ mockFileModelCheckHash.mockResolvedValue({ isExist: false });
294
+ mockFileModelCreate.mockResolvedValue({ id: 'new-file-id' });
295
+ mockFileServiceGetFileMetadata.mockResolvedValue({
296
+ contentLength: 0,
297
+ contentType: 'text/plain',
298
+ });
299
+
300
+ await caller.createFile({
301
+ hash: 'test-hash',
302
+ fileType: 'text',
303
+ name: 'test.txt',
304
+ size: 100,
305
+ url: 'files/test.txt',
306
+ metadata: {},
307
+ });
308
+
309
+ // Verify create was called with input size since contentLength < 1
310
+ expect(mockFileModelCreate).toHaveBeenCalledWith(
311
+ expect.objectContaining({
312
+ size: 100,
313
+ }),
314
+ true,
315
+ );
316
+ });
317
+
318
+ it('should throw error when both getFileMetadata contentLength and input size are less than 1', async () => {
319
+ mockFileModelCheckHash.mockResolvedValue({ isExist: false });
320
+ mockFileServiceGetFileMetadata.mockResolvedValue({
321
+ contentLength: 0,
322
+ contentType: 'text/plain',
323
+ });
324
+
325
+ await expect(
326
+ caller.createFile({
327
+ hash: 'test-hash',
328
+ fileType: 'text',
329
+ name: 'test.txt',
330
+ size: 0,
331
+ url: 'files/test.txt',
332
+ metadata: {},
333
+ }),
334
+ ).rejects.toThrow('File size must be at least 1 byte');
262
335
  });
263
336
  });
264
337
 
@@ -64,7 +64,19 @@ export const fileRouter = router({
64
64
  }
65
65
  }
66
66
 
67
- const { contentLength: actualSize } = await ctx.fileService.getFileMetadata(input.url);
67
+ let actualSize = input.size;
68
+ try {
69
+ const { contentLength } = await ctx.fileService.getFileMetadata(input.url);
70
+ if (contentLength >= 1) {
71
+ actualSize = contentLength;
72
+ }
73
+ } catch {
74
+ // If metadata fetch fails, use original size from input
75
+ }
76
+
77
+ if (actualSize < 1) {
78
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'File size must be at least 1 byte' });
79
+ }
68
80
 
69
81
  const { id } = await ctx.fileModel.create(
70
82
  {
@@ -8,7 +8,7 @@ import { exportService } from './export';
8
8
 
9
9
  class ConfigService {
10
10
  exportAll = async () => {
11
- const { data, url } = await exportService.exportData();
11
+ const { data, url, schemaHash } = await exportService.exportData();
12
12
  const filename = `${dayjs().format('YYYY-MM-DD-hh-mm')}_${BRANDING_NAME}-data.json`;
13
13
 
14
14
  // if url exists, means export data from server and upload the data to S3
@@ -18,24 +18,10 @@ class ConfigService {
18
18
  return;
19
19
  }
20
20
 
21
- // or export to file with the data
22
- const result = await this.createDataStructure(data, 'postgres');
21
+ const result: ImportPgDataStructure = { data, mode: 'postgres', schemaHash };
23
22
 
24
23
  exportJSONFile(result, filename);
25
24
  };
26
-
27
- private createDataStructure = async (
28
- data: any,
29
- mode: 'pglite' | 'postgres',
30
- ): Promise<ImportPgDataStructure> => {
31
- const { default: json } = await import('@/database/core/migrations.json');
32
- const latestHash = json.at(-1)?.hash;
33
- if (!latestHash) {
34
- throw new Error('Not find database sql hash');
35
- }
36
-
37
- return { data, mode, schemaHash: latestHash };
38
- };
39
25
  }
40
26
 
41
27
  export const configService = new ConfigService();
@@ -1,43 +0,0 @@
1
- import { Pool as NeonPool, neonConfig } from '@neondatabase/serverless';
2
- import { drizzle as neonDrizzle } from 'drizzle-orm/neon-serverless';
3
- import * as migrator from 'drizzle-orm/neon-serverless/migrator';
4
- import { drizzle as nodeDrizzle } from 'drizzle-orm/node-postgres';
5
- import * as nodeMigrator from 'drizzle-orm/node-postgres/migrator';
6
- import { join } from 'node:path';
7
- import { Pool as NodePool } from 'pg';
8
- import ws from 'ws';
9
-
10
- import { serverDBEnv } from '@/config/db';
11
-
12
- import * as schema from '../schemas';
13
-
14
- const migrationsFolder = join(__dirname, '../../migrations');
15
-
16
- export const getTestDBInstance = async () => {
17
- let connectionString = serverDBEnv.DATABASE_TEST_URL;
18
-
19
- if (!connectionString) {
20
- throw new Error(`You are try to use database, but "DATABASE_TEST_URL" is not set correctly`);
21
- }
22
-
23
- if (serverDBEnv.DATABASE_DRIVER === 'node') {
24
- const client = new NodePool({ connectionString });
25
-
26
- const db = nodeDrizzle(client, { schema });
27
-
28
- await nodeMigrator.migrate(db, { migrationsFolder });
29
-
30
- return db;
31
- }
32
-
33
- // https://github.com/neondatabase/serverless/blob/main/CONFIG.md#websocketconstructor-typeof-websocket--undefined
34
- neonConfig.webSocketConstructor = ws;
35
-
36
- const client = new NeonPool({ connectionString });
37
-
38
- const db = neonDrizzle(client, { schema });
39
-
40
- await migrator.migrate(db, { migrationsFolder });
41
-
42
- return db;
43
- };