@lobehub/chat 1.36.7 → 1.36.9

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 (47) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/changelog/v1.json +21 -0
  3. package/locales/ar/models.json +78 -0
  4. package/locales/ar/providers.json +3 -0
  5. package/locales/bg-BG/models.json +78 -0
  6. package/locales/bg-BG/providers.json +3 -0
  7. package/locales/de-DE/models.json +78 -0
  8. package/locales/de-DE/providers.json +3 -0
  9. package/locales/en-US/models.json +78 -0
  10. package/locales/en-US/providers.json +3 -0
  11. package/locales/es-ES/models.json +78 -0
  12. package/locales/es-ES/providers.json +3 -0
  13. package/locales/fa-IR/models.json +78 -0
  14. package/locales/fa-IR/providers.json +3 -0
  15. package/locales/fr-FR/models.json +78 -0
  16. package/locales/fr-FR/providers.json +3 -0
  17. package/locales/it-IT/models.json +78 -0
  18. package/locales/it-IT/providers.json +3 -0
  19. package/locales/ja-JP/models.json +78 -0
  20. package/locales/ja-JP/providers.json +3 -0
  21. package/locales/ko-KR/models.json +78 -0
  22. package/locales/ko-KR/providers.json +3 -0
  23. package/locales/nl-NL/models.json +78 -0
  24. package/locales/nl-NL/providers.json +3 -0
  25. package/locales/pl-PL/modelProvider.json +9 -9
  26. package/locales/pl-PL/models.json +78 -0
  27. package/locales/pl-PL/providers.json +3 -0
  28. package/locales/pt-BR/models.json +78 -0
  29. package/locales/pt-BR/providers.json +3 -0
  30. package/locales/ru-RU/models.json +78 -0
  31. package/locales/ru-RU/providers.json +3 -0
  32. package/locales/tr-TR/models.json +78 -0
  33. package/locales/tr-TR/providers.json +3 -0
  34. package/locales/vi-VN/models.json +78 -0
  35. package/locales/vi-VN/providers.json +3 -0
  36. package/locales/zh-CN/models.json +88 -10
  37. package/locales/zh-CN/providers.json +3 -0
  38. package/locales/zh-TW/models.json +78 -0
  39. package/locales/zh-TW/providers.json +3 -0
  40. package/package.json +1 -1
  41. package/src/app/(backend)/api/webhooks/clerk/route.ts +18 -3
  42. package/src/config/modelProviders/zhipu.ts +14 -0
  43. package/src/database/server/models/__tests__/nextauth.test.ts +33 -0
  44. package/src/libs/next-auth/adapter/index.ts +8 -2
  45. package/src/server/services/user/index.test.ts +200 -0
  46. package/src/server/services/user/index.ts +24 -32
  47. package/vitest.config.ts +1 -1
@@ -55,6 +55,24 @@
55
55
  "Baichuan4-Turbo": {
56
56
  "description": "模型能力國內第一,在知識百科、長文本、生成創作等中文任務上超越國外主流模型。還具備行業領先的多模態能力,多項權威評測基準表現優異。"
57
57
  },
58
+ "Doubao-lite-128k": {
59
+ "description": "Doubao-lite 擁有極致的回應速度,更好的性價比,為客戶不同場景提供更靈活的選擇。支持 128k 上下文窗口的推理和精調。"
60
+ },
61
+ "Doubao-lite-32k": {
62
+ "description": "Doubao-lite 擁有極致的回應速度,更好的性價比,為客戶不同場景提供更靈活的選擇。支持 32k 上下文窗口的推理和精調。"
63
+ },
64
+ "Doubao-lite-4k": {
65
+ "description": "Doubao-lite 擁有極致的回應速度,更好的性價比,為客戶不同場景提供更靈活的選擇。支持 4k 上下文窗口的推理和精調。"
66
+ },
67
+ "Doubao-pro-128k": {
68
+ "description": "效果最好的主力模型,適合處理複雜任務,在參考問答、總結摘要、創作、文本分類、角色扮演等場景都有很好的效果。支持 128k 上下文窗口的推理和精調。"
69
+ },
70
+ "Doubao-pro-32k": {
71
+ "description": "效果最好的主力模型,適合處理複雜任務,在參考問答、總結摘要、創作、文本分類、角色扮演等場景都有很好的效果。支持 32k 上下文窗口的推理和精調。"
72
+ },
73
+ "Doubao-pro-4k": {
74
+ "description": "效果最好的主力模型,適合處理複雜任務,在參考問答、總結摘要、創作、文本分類、角色扮演等場景都有很好的效果。支持 4k 上下文窗口的推理和精調。"
75
+ },
58
76
  "ERNIE-3.5-128K": {
59
77
  "description": "百度自研的旗艦級大規模語言模型,覆蓋海量中英文語料,具有強大的通用能力,可滿足絕大部分對話問答、創作生成、插件應用場景要求;支持自動對接百度搜索插件,保障問答信息時效。"
60
78
  },
@@ -242,6 +260,21 @@
242
260
  "SenseChat-Turbo": {
243
261
  "description": "適用於快速問答、模型微調場景"
244
262
  },
263
+ "Skylark2-lite-8k": {
264
+ "description": "雲雀(Skylark)第二代模型,Skylark2-lite 模型有較高的回應速度,適用於實時性要求高、成本敏感、對模型精度要求不高的場景,上下文窗口長度為 8k。"
265
+ },
266
+ "Skylark2-pro-32k": {
267
+ "description": "雲雀(Skylark)第二代模型,Skylark2-pro 版本有較高的模型精度,適用於較為複雜的文本生成場景,如專業領域文案生成、小說創作、高品質翻譯等,上下文窗口長度為 32k。"
268
+ },
269
+ "Skylark2-pro-4k": {
270
+ "description": "雲雀(Skylark)第二代模型,Skylark2-pro 模型有較高的模型精度,適用於較為複雜的文本生成場景,如專業領域文案生成、小說創作、高品質翻譯等,上下文窗口長度為 4k。"
271
+ },
272
+ "Skylark2-pro-character-4k": {
273
+ "description": "雲雀(Skylark)第二代模型,Skylark2-pro-character 模型具有優秀的角色扮演和聊天能力,擅長根據用戶 prompt 要求扮演不同角色與用戶展開聊天,角色風格突出,對話內容自然流暢,適用於構建聊天機器人、虛擬助手和在線客服等場景,有較高的回應速度。"
274
+ },
275
+ "Skylark2-pro-turbo-8k": {
276
+ "description": "雲雀(Skylark)第二代模型,Skylark2-pro-turbo-8k 推理更快,成本更低,上下文窗口長度為 8k。"
277
+ },
245
278
  "THUDM/chatglm3-6b": {
246
279
  "description": "ChatGLM3-6B 是 ChatGLM 系列的開源模型,由智譜 AI 開發。該模型保留了前代模型的優秀特性,如對話流暢和部署門檻低,同時引入了新的特性。它採用了更多樣的訓練數據、更充分的訓練步數和更合理的訓練策略,在 10B 以下的預訓練模型中表現出色。ChatGLM3-6B 支持多輪對話、工具調用、代碼執行和 Agent 任務等複雜場景。除對話模型外,還開源了基礎模型 ChatGLM-6B-Base 和長文本對話模型 ChatGLM3-6B-32K。該模型對學術研究完全開放,在登記後也允許免費商業使用"
247
280
  },
@@ -476,6 +509,9 @@
476
509
  "cohere-command-r-plus": {
477
510
  "description": "Command R+是一個最先進的RAG優化模型,旨在應對企業級工作負載。"
478
511
  },
512
+ "command-light": {
513
+ "description": ""
514
+ },
479
515
  "command-r": {
480
516
  "description": "Command R 是優化用於對話和長上下文任務的 LLM,特別適合動態交互與知識管理。"
481
517
  },
@@ -539,6 +575,9 @@
539
575
  "gemini-1.5-flash-8b-exp-0924": {
540
576
  "description": "Gemini 1.5 Flash 8B 0924 是最新的實驗性模型,在文本和多模態用例中都有顯著的性能提升。"
541
577
  },
578
+ "gemini-1.5-flash-exp-0827": {
579
+ "description": "Gemini 1.5 Flash 0827 提供了優化後的多模態處理能力,適用多種複雜任務場景。"
580
+ },
542
581
  "gemini-1.5-flash-latest": {
543
582
  "description": "Gemini 1.5 Flash 是 Google 最新的多模態 AI 模型,具備快速處理能力,支持文本、圖像和視頻輸入,適用於多種任務的高效擴展。"
544
583
  },
@@ -548,6 +587,12 @@
548
587
  "gemini-1.5-pro-002": {
549
588
  "description": "Gemini 1.5 Pro 002 是最新的生產就緒模型,提供更高品質的輸出,特別在數學、長上下文和視覺任務方面有顯著提升。"
550
589
  },
590
+ "gemini-1.5-pro-exp-0801": {
591
+ "description": "Gemini 1.5 Pro 0801 提供出色的多模態處理能力,為應用開發帶來更大靈活性。"
592
+ },
593
+ "gemini-1.5-pro-exp-0827": {
594
+ "description": "Gemini 1.5 Pro 0827 結合最新優化技術,帶來更高效的多模態數據處理能力。"
595
+ },
551
596
  "gemini-1.5-pro-latest": {
552
597
  "description": "Gemini 1.5 Pro 支持高達 200 萬個 tokens,是中型多模態模型的理想選擇,適用於複雜任務的多方面支持。"
553
598
  },
@@ -557,6 +602,9 @@
557
602
  "gemini-exp-1121": {
558
603
  "description": "Gemini Exp 1121 是 Google 最新的實驗性多模態 AI 模型,具備快速處理能力,支持文本、圖像和視頻輸入,適用於多種任務的高效擴展。"
559
604
  },
605
+ "gemini-exp-1206": {
606
+ "description": "Gemini Exp 1206 是 Google 最新的實驗性多模態 AI 模型,與歷史版本相比有一定的質量提升。"
607
+ },
560
608
  "gemma-7b-it": {
561
609
  "description": "Gemma 7B 適合中小規模任務處理,兼具成本效益。"
562
610
  },
@@ -647,6 +695,12 @@
647
695
  "gpt-3.5-turbo-instruct": {
648
696
  "description": "GPT 3.5 Turbo,適用於各種文本生成和理解任務,Currently points to gpt-3.5-turbo-0125"
649
697
  },
698
+ "gpt-35-turbo": {
699
+ "description": "GPT 3.5 Turbo,OpenAI 提供的高效模型,適用於聊天和文本生成任務,支持並行函數調用。"
700
+ },
701
+ "gpt-35-turbo-16k": {
702
+ "description": "GPT 3.5 Turbo 16k,高容量文本生成模型,適合複雜任務。"
703
+ },
650
704
  "gpt-4": {
651
705
  "description": "GPT-4提供了一個更大的上下文窗口,能夠處理更長的文本輸入,適用於需要廣泛信息整合和數據分析的場景。"
652
706
  },
@@ -689,6 +743,9 @@
689
743
  "gpt-4o-2024-08-06": {
690
744
  "description": "ChatGPT-4o是一款動態模型,實時更新以保持當前最新版本。它結合了強大的語言理解與生成能力,適合於大規模應用場景,包括客戶服務、教育和技術支持。"
691
745
  },
746
+ "gpt-4o-2024-11-20": {
747
+ "description": "ChatGPT-4o 是一款動態模型,實時更新以保持當前最新版本。它結合了強大的語言理解與生成能力,適合於大規模應用場景,包括客戶服務、教育和技術支持。"
748
+ },
692
749
  "gpt-4o-mini": {
693
750
  "description": "GPT-4o mini是OpenAI在GPT-4 Omni之後推出的最新模型,支持圖文輸入並輸出文本。作為他們最先進的小型模型,它比其他近期的前沿模型便宜很多,並且比GPT-3.5 Turbo便宜超過60%。它保持了最先進的智能,同時具有顯著的性價比。GPT-4o mini在MMLU測試中獲得了82%的得分,目前在聊天偏好上排名高於GPT-4。"
694
751
  },
@@ -707,6 +764,9 @@
707
764
  "hunyuan-functioncall": {
708
765
  "description": "混元最新 MOE 架構 FunctionCall 模型,經過高質量的 FunctionCall 數據訓練,上下文窗口達 32K,在多個維度的評測指標上處於領先。"
709
766
  },
767
+ "hunyuan-large": {
768
+ "description": ""
769
+ },
710
770
  "hunyuan-lite": {
711
771
  "description": "升級為 MOE 結構,上下文窗口為 256k,在 NLP、代碼、數學、行業等多項評測集上領先眾多開源模型。"
712
772
  },
@@ -787,6 +847,9 @@
787
847
  "llama-3.2-90b-vision-preview": {
788
848
  "description": "Llama 3.2 旨在處理結合視覺和文本數據的任務。它在圖像描述和視覺問答等任務中表現出色,跨越了語言生成和視覺推理之間的鴻溝。"
789
849
  },
850
+ "llama-3.3-70b-versatile": {
851
+ "description": "Meta Llama 3.3 多語言大語言模型 (LLM) 是 70B(文本輸入/文本輸出)中的預訓練和指令調整生成模型。Llama 3.3 指令調整的純文本模型針對多語言對話用例進行了優化,並且在常見行業基準上優於許多可用的開源和封閉式聊天模型。"
852
+ },
790
853
  "llama3-70b-8192": {
791
854
  "description": "Meta Llama 3 70B 提供無與倫比的複雜性處理能力,為高要求項目量身定制。"
792
855
  },
@@ -1094,12 +1157,21 @@
1094
1157
  "qwen-math-turbo-latest": {
1095
1158
  "description": "通義千問數學模型是專門用於數學解題的語言模型。"
1096
1159
  },
1160
+ "qwen-max": {
1161
+ "description": "通義千問千億級別超大規模語言模型,支持中文、英文等不同語言輸入,當前通義千問 2.5 產品版本背後的 API 模型。"
1162
+ },
1097
1163
  "qwen-max-latest": {
1098
1164
  "description": "通義千問千億級別超大規模語言模型,支持中文、英文等不同語言輸入,當前通義千問2.5產品版本背後的API模型。"
1099
1165
  },
1166
+ "qwen-plus": {
1167
+ "description": "通義千問超大規模語言模型增強版,支持中文、英文等不同語言輸入。"
1168
+ },
1100
1169
  "qwen-plus-latest": {
1101
1170
  "description": "通義千問超大規模語言模型增強版,支持中文、英文等不同語言輸入。"
1102
1171
  },
1172
+ "qwen-turbo": {
1173
+ "description": "通義千問超大規模語言模型,支持中文、英文等不同語言輸入。"
1174
+ },
1103
1175
  "qwen-turbo-latest": {
1104
1176
  "description": "通義千問超大規模語言模型,支持中文、英文等不同語言輸入。"
1105
1177
  },
@@ -1136,12 +1208,18 @@
1136
1208
  "qwen2.5-7b-instruct": {
1137
1209
  "description": "通義千問2.5對外開源的7B規模的模型。"
1138
1210
  },
1211
+ "qwen2.5-coder-1.5b-instruct": {
1212
+ "description": "通義千問代碼模型開源版。"
1213
+ },
1139
1214
  "qwen2.5-coder-32b-instruct": {
1140
1215
  "description": "通義千問代碼模型開源版。"
1141
1216
  },
1142
1217
  "qwen2.5-coder-7b-instruct": {
1143
1218
  "description": "通義千問代碼模型開源版。"
1144
1219
  },
1220
+ "qwen2.5-math-1.5b-instruct": {
1221
+ "description": "Qwen-Math 模型具有強大的數學解題能力。"
1222
+ },
1145
1223
  "qwen2.5-math-72b-instruct": {
1146
1224
  "description": "Qwen-Math模型具有強大的數學解題能力。"
1147
1225
  },
@@ -34,6 +34,9 @@
34
34
  "groq": {
35
35
  "description": "Groq 的 LPU 推理引擎在最新的獨立大語言模型(LLM)基準測試中表現卓越,以其驚人的速度和效率重新定義了 AI 解決方案的標準。Groq 是一種即時推理速度的代表,在基於雲的部署中展現了良好的性能。"
36
36
  },
37
+ "higress": {
38
+ "description": ""
39
+ },
37
40
  "huggingface": {
38
41
  "description": "HuggingFace Inference API 提供了一種快速且免費的方式,讓您可以探索成千上萬種模型,適用於各種任務。無論您是在為新應用程式進行原型設計,還是在嘗試機器學習的功能,這個 API 都能讓您即時訪問多個領域的高性能模型。"
39
42
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "1.36.7",
3
+ "version": "1.36.9",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -29,13 +29,28 @@ export const POST = async (req: Request): Promise<NextResponse> => {
29
29
  switch (type) {
30
30
  case 'user.created': {
31
31
  pino.info('creating user due to clerk webhook');
32
- return userService.createUser(data.id, data);
32
+ const result = await userService.createUser(data.id, data);
33
+
34
+ return NextResponse.json(result, { status: 200 });
33
35
  }
36
+
34
37
  case 'user.deleted': {
35
- return userService.deleteUser(data.id);
38
+ if (!data.id) {
39
+ pino.warn('clerk sent a delete user request, but no user ID was included in the payload');
40
+ return NextResponse.json({ message: 'ok' }, { status: 200 });
41
+ }
42
+
43
+ pino.info('delete user due to clerk webhook');
44
+
45
+ await userService.deleteUser(data.id);
46
+
47
+ return NextResponse.json({ message: 'user deleted' }, { status: 200 });
36
48
  }
49
+
37
50
  case 'user.updated': {
38
- return userService.updateUser(data.id, data);
51
+ const result = await userService.updateUser(data.id, data);
52
+
53
+ return NextResponse.json(result, { status: 200 });
39
54
  }
40
55
 
41
56
  default: {
@@ -120,6 +120,20 @@ const ZhiPu: ModelProviderCard = {
120
120
  },
121
121
  tokens: 128_000,
122
122
  },
123
+ {
124
+ description: 'GLM-4V-Flash 专注于高效的单一图像理解,适用于快速图像解析的场景,例如实时图像分析或批量图像处理。',
125
+ displayName: 'GLM-4V-Flash',
126
+ enabled: true,
127
+ id: 'glm-4v-flash',
128
+ pricing: {
129
+ currency: 'CNY',
130
+ input: 0,
131
+ output: 0,
132
+ },
133
+ releasedAt: '2024-12-09',
134
+ tokens: 8192,
135
+ vision: true,
136
+ },
123
137
  {
124
138
  description: 'GLM-4V-Plus 具备对视频内容及多图片的理解能力,适合多模态任务。',
125
139
  displayName: 'GLM-4V-Plus',
@@ -146,6 +146,39 @@ describe('LobeNextAuthDbAdapter', () => {
146
146
  await serverDB.query.users.findMany({ where: eq(users.id, anotherUserId) }),
147
147
  ).toHaveLength(1);
148
148
  });
149
+
150
+ it('should create a user if id not exist even email is invalid type', async () => {
151
+ // In previous version, it will link the account to the existing user if the email is null
152
+ // issue: https://github.com/lobehub/lobe-chat/issues/4918
153
+ expect(nextAuthAdapter).toBeDefined();
154
+ expect(nextAuthAdapter.createUser).toBeDefined();
155
+
156
+ const existUserId = 'user-db-1';
157
+ const existUserName = 'John Doe 1';
158
+ // @ts-expect-error: createUser is defined
159
+ await nextAuthAdapter.createUser({
160
+ ...user,
161
+ id: existUserId,
162
+ name: existUserName,
163
+ email: Object({}), // assign a non-string value
164
+ });
165
+
166
+ const anotherUserId = 'user-db-2';
167
+ const anotherUserName = 'John Doe 2';
168
+ // @ts-expect-error: createUser is defined
169
+ await nextAuthAdapter.createUser({
170
+ ...user,
171
+ id: anotherUserId,
172
+ name: anotherUserName,
173
+ // @ts-expect-error: try to assign undefined value
174
+ email: undefined,
175
+ });
176
+
177
+ // Should create a new user if id not exists and email is null
178
+ expect(
179
+ await serverDB.query.users.findMany({ where: eq(users.id, anotherUserId) }),
180
+ ).toHaveLength(1);
181
+ });
149
182
  });
150
183
 
151
184
  describe('deleteUser', () => {
@@ -53,7 +53,10 @@ export function LobeNextAuthDbAdapter(serverDB: NeonDatabase<typeof schema>): Ad
53
53
  async createUser(user): Promise<AdapterUser> {
54
54
  const { id, name, email, emailVerified, image, providerAccountId } = user;
55
55
  // return the user if it already exists
56
- let existingUser = email.trim() ? await UserModel.findByEmail(serverDB, email) : undefined;
56
+ let existingUser =
57
+ email && typeof email === 'string' && email.trim()
58
+ ? await UserModel.findByEmail(serverDB, email)
59
+ : undefined;
57
60
  // If the user is not found by email, try to find by providerAccountId
58
61
  if (!existingUser && providerAccountId) {
59
62
  existingUser = await UserModel.findById(serverDB, providerAccountId);
@@ -169,7 +172,10 @@ export function LobeNextAuthDbAdapter(serverDB: NeonDatabase<typeof schema>): Ad
169
172
  },
170
173
 
171
174
  async getUserByEmail(email): Promise<AdapterUser | null> {
172
- const lobeUser = email.trim() ? await UserModel.findByEmail(serverDB, email) : undefined;
175
+ const lobeUser =
176
+ email && typeof email === 'string' && email.trim()
177
+ ? await UserModel.findByEmail(serverDB, email)
178
+ : undefined;
173
179
  return lobeUser ? mapLobeUserToAdapterUser(lobeUser) : null;
174
180
  },
175
181
 
@@ -0,0 +1,200 @@
1
+ import { UserJSON } from '@clerk/backend';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { UserItem } from '@/database/schemas';
5
+ import { UserModel } from '@/database/server/models/user';
6
+ import { pino } from '@/libs/logger';
7
+
8
+ import { UserService } from './index';
9
+
10
+ // Mock dependencies
11
+ vi.mock('@/database/server/models/user', () => {
12
+ const MockUserModel = vi.fn();
13
+ // @ts-ignore
14
+ MockUserModel.findById = vi.fn();
15
+ // @ts-ignore
16
+ MockUserModel.createUser = vi.fn();
17
+ // @ts-ignore
18
+ MockUserModel.deleteUser = vi.fn();
19
+
20
+ // Mock instance methods
21
+ MockUserModel.prototype.updateUser = vi.fn();
22
+
23
+ return { UserModel: MockUserModel };
24
+ });
25
+
26
+ vi.mock('@/libs/logger', () => ({
27
+ pino: {
28
+ info: vi.fn(),
29
+ },
30
+ }));
31
+
32
+ let service: UserService;
33
+ const mockUserId = 'test-user-id';
34
+
35
+ // Mock user data
36
+ const mockUserJSON: UserJSON = {
37
+ id: mockUserId,
38
+ email_addresses: [{ id: 'email-1', email_address: 'test@example.com' }],
39
+ phone_numbers: [{ id: 'phone-1', phone_number: '+1234567890' }],
40
+ primary_email_address_id: 'email-1',
41
+ primary_phone_number_id: 'phone-1',
42
+ image_url: 'https://example.com/avatar.jpg',
43
+ first_name: 'Test',
44
+ last_name: 'User',
45
+ username: 'testuser',
46
+ created_at: '2023-01-01T00:00:00Z',
47
+ } as unknown as UserJSON;
48
+
49
+ beforeEach(() => {
50
+ service = new UserService();
51
+ vi.clearAllMocks();
52
+ });
53
+
54
+ describe('UserService', () => {
55
+ describe('createUser', () => {
56
+ it('should create a new user when user does not exist', async () => {
57
+ // Mock user not found
58
+ vi.mocked(UserModel.findById).mockResolvedValue(null as any);
59
+
60
+ await service.createUser(mockUserId, mockUserJSON);
61
+
62
+ expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
63
+ expect(UserModel.createUser).toHaveBeenCalledWith(
64
+ expect.anything(),
65
+ expect.objectContaining({
66
+ id: mockUserId,
67
+ email: 'test@example.com',
68
+ phone: '+1234567890',
69
+ firstName: 'Test',
70
+ lastName: 'User',
71
+ username: 'testuser',
72
+ avatar: 'https://example.com/avatar.jpg',
73
+ clerkCreatedAt: new Date('2023-01-01T00:00:00Z'),
74
+ }),
75
+ );
76
+ });
77
+
78
+ it('should not create user if already exists', async () => {
79
+ // Mock user found
80
+ vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
81
+
82
+ const result = await service.createUser(mockUserId, mockUserJSON);
83
+
84
+ expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
85
+ expect(UserModel.createUser).not.toHaveBeenCalled();
86
+ expect(result).toEqual({
87
+ message: 'user not created due to user already existing in the database',
88
+ success: false,
89
+ });
90
+ });
91
+
92
+ it('should handle user without primary phone number', async () => {
93
+ vi.mocked(UserModel.findById).mockResolvedValue(null as any);
94
+
95
+ const userWithoutPrimaryPhone = {
96
+ ...mockUserJSON,
97
+ primary_phone_number_id: null,
98
+ phone_numbers: [{ id: 'phone-1', phone_number: '+1234567890' }],
99
+ } as UserJSON;
100
+
101
+ await service.createUser(mockUserId, userWithoutPrimaryPhone);
102
+
103
+ expect(UserModel.createUser).toHaveBeenCalledWith(
104
+ expect.anything(),
105
+ expect.objectContaining({
106
+ phone: '+1234567890', // Should use first phone number
107
+ }),
108
+ );
109
+ });
110
+ });
111
+
112
+ describe('deleteUser', () => {
113
+ it('should delete user', async () => {
114
+ await service.deleteUser(mockUserId);
115
+
116
+ expect(UserModel.deleteUser).toHaveBeenCalledWith(expect.anything(), mockUserId);
117
+ });
118
+
119
+ it('should throw error if deletion fails', async () => {
120
+ const error = new Error('Deletion failed');
121
+ vi.mocked(UserModel.deleteUser).mockRejectedValue(error);
122
+
123
+ await expect(service.deleteUser(mockUserId)).rejects.toThrow('Deletion failed');
124
+ });
125
+ });
126
+
127
+ describe('updateUser', () => {
128
+ it('should update user when user exists', async () => {
129
+ // Mock user found
130
+ vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
131
+ const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
132
+
133
+ const result = await service.updateUser(mockUserId, mockUserJSON);
134
+
135
+ expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
136
+ expect(pino.info).toHaveBeenCalledWith('updating user due to clerk webhook');
137
+ expect(mockUpdateUser).toHaveBeenCalledWith(
138
+ expect.objectContaining({
139
+ id: mockUserId,
140
+ email: 'test@example.com',
141
+ phone: '+1234567890',
142
+ firstName: 'Test',
143
+ lastName: 'User',
144
+ username: 'testuser',
145
+ avatar: 'https://example.com/avatar.jpg',
146
+ }),
147
+ );
148
+ expect(result).toEqual({
149
+ message: 'user updated',
150
+ success: true,
151
+ });
152
+ });
153
+
154
+ it('should not update user when user does not exist', async () => {
155
+ // Mock user not found
156
+ vi.mocked(UserModel.findById).mockResolvedValue(null as any);
157
+ const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
158
+
159
+ const result = await service.updateUser(mockUserId, mockUserJSON);
160
+
161
+ expect(UserModel.findById).toHaveBeenCalledWith(expect.anything(), mockUserId);
162
+ expect(mockUpdateUser).not.toHaveBeenCalled();
163
+ expect(result).toEqual({
164
+ message: "user not updated due to the user don't existing in the database",
165
+ success: false,
166
+ });
167
+ });
168
+
169
+ it('should handle user without primary email and phone', async () => {
170
+ vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
171
+ const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
172
+
173
+ const userWithoutPrimaryContacts = {
174
+ ...mockUserJSON,
175
+ primary_email_address_id: null,
176
+ primary_phone_number_id: null,
177
+ email_addresses: [{ id: 'email-1', email_address: 'test@example.com' }],
178
+ phone_numbers: [{ id: 'phone-1', phone_number: '+1234567890' }],
179
+ } as UserJSON;
180
+
181
+ await service.updateUser(mockUserId, userWithoutPrimaryContacts);
182
+
183
+ // Verify that the first email and phone are used when primary is not specified
184
+ expect(mockUpdateUser).toHaveBeenCalledWith(
185
+ expect.objectContaining({
186
+ phone: '+1234567890',
187
+ }),
188
+ );
189
+ });
190
+
191
+ it('should handle update failure', async () => {
192
+ vi.mocked(UserModel.findById).mockResolvedValue({ id: mockUserId } as UserItem);
193
+ const mockUpdateUser = vi.mocked(UserModel.prototype.updateUser);
194
+ const error = new Error('Update failed');
195
+ mockUpdateUser.mockRejectedValue(error);
196
+
197
+ await expect(service.updateUser(mockUserId, mockUserJSON)).rejects.toThrow('Update failed');
198
+ });
199
+ });
200
+ });
@@ -1,5 +1,4 @@
1
1
  import { UserJSON } from '@clerk/backend';
2
- import { NextResponse } from 'next/server';
3
2
 
4
3
  import { serverDB } from '@/database/server';
5
4
  import { UserModel } from '@/database/server/models/user';
@@ -12,16 +11,18 @@ export class UserService {
12
11
 
13
12
  // If user already exists, skip creating a new user
14
13
  if (res)
15
- return NextResponse.json(
16
- {
17
- message: 'user not created due to user already existing in the database',
18
- success: false,
19
- },
20
- { status: 200 },
21
- );
14
+ return {
15
+ message: 'user not created due to user already existing in the database',
16
+ success: false,
17
+ };
22
18
 
23
19
  const email = params.email_addresses.find((e) => e.id === params.primary_email_address_id);
24
- const phone = params.phone_numbers.find((e) => e.id === params.primary_phone_number_id);
20
+
21
+ const phone = params.phone_numbers.find((e, index) => {
22
+ if (!!params.primary_phone_number_id) return e.id === params.primary_phone_number_id;
23
+
24
+ return index === 0;
25
+ });
25
26
 
26
27
  /* ↓ cloud slot ↓ */
27
28
 
@@ -43,25 +44,14 @@ export class UserService {
43
44
 
44
45
  /* ↑ cloud slot ↑ */
45
46
 
46
- return NextResponse.json({ message: 'user created', success: true }, { status: 200 });
47
+ return { message: 'user created', success: true };
47
48
  };
48
49
 
49
- deleteUser = async (id?: string) => {
50
- if (id) {
51
- pino.info('delete user due to clerk webhook');
52
-
53
- await UserModel.deleteUser(serverDB, id);
54
-
55
- return NextResponse.json({ message: 'user deleted' }, { status: 200 });
56
- } else {
57
- pino.warn('clerk sent a delete user request, but no user ID was included in the payload');
58
- return NextResponse.json({ message: 'ok' }, { status: 200 });
59
- }
50
+ deleteUser = async (id: string) => {
51
+ await UserModel.deleteUser(serverDB, id);
60
52
  };
61
53
 
62
54
  updateUser = async (id: string, params: UserJSON) => {
63
- pino.info('updating user due to clerk webhook');
64
-
65
55
  const userModel = new UserModel(serverDB, id);
66
56
 
67
57
  // Check if user already exists
@@ -69,16 +59,18 @@ export class UserService {
69
59
 
70
60
  // If user not exists, skip update the user
71
61
  if (!res)
72
- return NextResponse.json(
73
- {
74
- message: "user not updated due to the user don't existing in the database",
75
- success: false,
76
- },
77
- { status: 200 },
78
- );
62
+ return {
63
+ message: "user not updated due to the user don't existing in the database",
64
+ success: false,
65
+ };
66
+
67
+ pino.info('updating user due to clerk webhook');
79
68
 
80
69
  const email = params.email_addresses.find((e) => e.id === params.primary_email_address_id);
81
- const phone = params.phone_numbers.find((e) => e.id === params.primary_phone_number_id);
70
+ const phone = params.phone_numbers.find((e, index) => {
71
+ if (params.primary_phone_number_id) return e.id === params.primary_phone_number_id;
72
+ return index === 0;
73
+ });
82
74
 
83
75
  await userModel.updateUser({
84
76
  avatar: params.image_url,
@@ -90,6 +82,6 @@ export class UserService {
90
82
  username: params.username,
91
83
  });
92
84
 
93
- return NextResponse.json({ message: 'user updated', success: true }, { status: 200 });
85
+ return { message: 'user updated', success: true };
94
86
  };
95
87
  }
package/vitest.config.ts CHANGED
@@ -31,7 +31,7 @@ export default defineConfig({
31
31
  '**/dist/**',
32
32
  '**/build/**',
33
33
  'src/database/server/**/**',
34
- 'src/server/services/!(discover)/**/**',
34
+ 'src/server/services/dataImporter/**/**',
35
35
  ],
36
36
  globals: true,
37
37
  server: {