@studious-lms/server 1.1.26 → 1.2.6

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 (486) hide show
  1. package/.coderabbit.yaml +9 -0
  2. package/.env.example +53 -0
  3. package/.env.test.example +37 -0
  4. package/README.md +34 -7
  5. package/dist/exportType.d.ts.map +1 -1
  6. package/dist/exportType.js +4 -0
  7. package/dist/exportType.js.map +1 -0
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +212 -51
  11. package/dist/index.js.map +1 -0
  12. package/dist/instrument.d.ts +2 -0
  13. package/dist/instrument.d.ts.map +1 -0
  14. package/dist/instrument.js +18 -0
  15. package/dist/instrument.js.map +1 -0
  16. package/dist/lib/config/env.d.ts +190 -0
  17. package/dist/lib/config/env.d.ts.map +1 -0
  18. package/dist/lib/config/env.js +121 -0
  19. package/dist/lib/config/env.js.map +1 -0
  20. package/dist/lib/fileUpload.d.ts +2 -2
  21. package/dist/lib/fileUpload.d.ts.map +1 -1
  22. package/dist/lib/fileUpload.js +15 -5
  23. package/dist/lib/fileUpload.js.map +1 -0
  24. package/dist/lib/googleCloudStorage.d.ts +6 -0
  25. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  26. package/dist/lib/googleCloudStorage.js +26 -6
  27. package/dist/lib/googleCloudStorage.js.map +1 -0
  28. package/dist/lib/jsonConversion.d.ts.map +1 -1
  29. package/dist/lib/jsonConversion.js +16 -14
  30. package/dist/lib/jsonConversion.js.map +1 -0
  31. package/dist/lib/jsonStyles.d.ts.map +1 -1
  32. package/dist/lib/jsonStyles.js +4 -0
  33. package/dist/lib/jsonStyles.js.map +1 -0
  34. package/dist/lib/notificationHandler.d.ts +2 -2
  35. package/dist/lib/notificationHandler.d.ts.map +1 -1
  36. package/dist/lib/notificationHandler.js +4 -0
  37. package/dist/lib/notificationHandler.js.map +1 -0
  38. package/dist/lib/prisma.d.ts +2 -2
  39. package/dist/lib/prisma.d.ts.map +1 -1
  40. package/dist/lib/prisma.js +24 -1
  41. package/dist/lib/prisma.js.map +1 -0
  42. package/dist/lib/pusher.d.ts +4 -1
  43. package/dist/lib/pusher.d.ts.map +1 -1
  44. package/dist/lib/pusher.js +14 -6
  45. package/dist/lib/pusher.js.map +1 -0
  46. package/dist/lib/redis.d.ts +5 -0
  47. package/dist/lib/redis.d.ts.map +1 -0
  48. package/dist/lib/redis.js +53 -0
  49. package/dist/lib/redis.js.map +1 -0
  50. package/dist/lib/thumbnailGenerator.d.ts +0 -21
  51. package/dist/lib/thumbnailGenerator.d.ts.map +1 -1
  52. package/dist/lib/thumbnailGenerator.js +159 -158
  53. package/dist/lib/thumbnailGenerator.js.map +1 -0
  54. package/dist/middleware/auth.d.ts.map +1 -1
  55. package/dist/middleware/auth.js +41 -93
  56. package/dist/middleware/auth.js.map +1 -0
  57. package/dist/middleware/logging.d.ts.map +1 -1
  58. package/dist/middleware/logging.js +4 -0
  59. package/dist/middleware/logging.js.map +1 -0
  60. package/dist/middleware/security.d.ts +5 -0
  61. package/dist/middleware/security.d.ts.map +1 -0
  62. package/dist/middleware/security.js +77 -0
  63. package/dist/middleware/security.js.map +1 -0
  64. package/dist/models/agenda.d.ts +97 -0
  65. package/dist/models/agenda.d.ts.map +1 -0
  66. package/dist/models/agenda.js +40 -0
  67. package/dist/models/agenda.js.map +1 -0
  68. package/dist/models/announcement.d.ts +223 -0
  69. package/dist/models/announcement.d.ts.map +1 -0
  70. package/dist/models/announcement.js +120 -0
  71. package/dist/models/announcement.js.map +1 -0
  72. package/dist/models/assignment.d.ts +1292 -0
  73. package/dist/models/assignment.d.ts.map +1 -0
  74. package/dist/models/assignment.js +309 -0
  75. package/dist/models/assignment.js.map +1 -0
  76. package/dist/models/attendance.d.ts +180 -0
  77. package/dist/models/attendance.d.ts.map +1 -0
  78. package/dist/models/attendance.js +188 -0
  79. package/dist/models/attendance.js.map +1 -0
  80. package/dist/models/auth.d.ts +153 -0
  81. package/dist/models/auth.d.ts.map +1 -0
  82. package/dist/models/auth.js +217 -0
  83. package/dist/models/auth.js.map +1 -0
  84. package/dist/models/class.d.ts +439 -0
  85. package/dist/models/class.d.ts.map +1 -0
  86. package/dist/models/class.js +546 -0
  87. package/dist/models/class.js.map +1 -0
  88. package/dist/models/comment.d.ts +171 -0
  89. package/dist/models/comment.d.ts.map +1 -0
  90. package/dist/models/comment.js +138 -0
  91. package/dist/models/comment.js.map +1 -0
  92. package/dist/models/conversation.d.ts +164 -0
  93. package/dist/models/conversation.d.ts.map +1 -0
  94. package/dist/models/conversation.js +175 -0
  95. package/dist/models/conversation.js.map +1 -0
  96. package/dist/models/event.d.ts +295 -0
  97. package/dist/models/event.d.ts.map +1 -0
  98. package/dist/models/event.js +145 -0
  99. package/dist/models/event.js.map +1 -0
  100. package/dist/models/file.d.ts +536 -0
  101. package/dist/models/file.d.ts.map +1 -0
  102. package/dist/models/file.js +126 -0
  103. package/dist/models/file.js.map +1 -0
  104. package/dist/models/folder.d.ts +295 -0
  105. package/dist/models/folder.d.ts.map +1 -0
  106. package/dist/models/folder.js +202 -0
  107. package/dist/models/folder.js.map +1 -0
  108. package/dist/models/labChat.d.ts +243 -0
  109. package/dist/models/labChat.d.ts.map +1 -0
  110. package/dist/models/labChat.js +204 -0
  111. package/dist/models/labChat.js.map +1 -0
  112. package/dist/models/marketing.d.ts +72 -0
  113. package/dist/models/marketing.d.ts.map +1 -0
  114. package/dist/models/marketing.js +26 -0
  115. package/dist/models/marketing.js.map +1 -0
  116. package/dist/models/message.d.ts +100 -0
  117. package/dist/models/message.d.ts.map +1 -0
  118. package/dist/models/message.js +131 -0
  119. package/dist/models/message.js.map +1 -0
  120. package/dist/models/newtonChat.d.ts +72 -0
  121. package/dist/models/newtonChat.d.ts.map +1 -0
  122. package/dist/models/newtonChat.js +61 -0
  123. package/dist/models/newtonChat.js.map +1 -0
  124. package/dist/models/notification.d.ts +65 -0
  125. package/dist/models/notification.d.ts.map +1 -0
  126. package/dist/models/notification.js +46 -0
  127. package/dist/models/notification.js.map +1 -0
  128. package/dist/models/section.d.ts +102 -0
  129. package/dist/models/section.d.ts.map +1 -0
  130. package/dist/models/section.js +83 -0
  131. package/dist/models/section.js.map +1 -0
  132. package/dist/models/user.d.ts +39 -0
  133. package/dist/models/user.d.ts.map +1 -0
  134. package/dist/models/user.js +38 -0
  135. package/dist/models/user.js.map +1 -0
  136. package/dist/models/worksheet.d.ts +460 -0
  137. package/dist/models/worksheet.d.ts.map +1 -0
  138. package/dist/models/worksheet.js +200 -0
  139. package/dist/models/worksheet.js.map +1 -0
  140. package/dist/pipelines/aiLabChat.d.ts +21 -0
  141. package/dist/pipelines/aiLabChat.d.ts.map +1 -0
  142. package/dist/pipelines/aiLabChat.js +460 -0
  143. package/dist/pipelines/aiLabChat.js.map +1 -0
  144. package/dist/pipelines/aiNewtonChat.d.ts +30 -0
  145. package/dist/pipelines/aiNewtonChat.d.ts.map +1 -0
  146. package/dist/pipelines/aiNewtonChat.js +289 -0
  147. package/dist/pipelines/aiNewtonChat.js.map +1 -0
  148. package/dist/pipelines/gradeWorksheet.d.ts +30 -0
  149. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -0
  150. package/dist/pipelines/gradeWorksheet.js +252 -0
  151. package/dist/pipelines/gradeWorksheet.js.map +1 -0
  152. package/dist/routers/_app.d.ts +6438 -3910
  153. package/dist/routers/_app.d.ts.map +1 -1
  154. package/dist/routers/_app.js +10 -0
  155. package/dist/routers/_app.js.map +1 -0
  156. package/dist/routers/agenda.d.ts +58 -6
  157. package/dist/routers/agenda.d.ts.map +1 -1
  158. package/dist/routers/agenda.js +6 -58
  159. package/dist/routers/agenda.js.map +1 -0
  160. package/dist/routers/announcement.d.ts +325 -6
  161. package/dist/routers/announcement.d.ts.map +1 -1
  162. package/dist/routers/announcement.js +543 -77
  163. package/dist/routers/announcement.js.map +1 -0
  164. package/dist/routers/assignment.d.ts +419 -357
  165. package/dist/routers/assignment.d.ts.map +1 -1
  166. package/dist/routers/assignment.js +100 -1689
  167. package/dist/routers/assignment.js.map +1 -0
  168. package/dist/routers/attendance.d.ts +20 -9
  169. package/dist/routers/attendance.d.ts.map +1 -1
  170. package/dist/routers/attendance.js +10 -263
  171. package/dist/routers/attendance.js.map +1 -0
  172. package/dist/routers/auth.d.ts +21 -1
  173. package/dist/routers/auth.d.ts.map +1 -1
  174. package/dist/routers/auth.js +37 -241
  175. package/dist/routers/auth.js.map +1 -0
  176. package/dist/routers/class.d.ts +198 -68
  177. package/dist/routers/class.d.ts.map +1 -1
  178. package/dist/routers/class.js +88 -909
  179. package/dist/routers/class.js.map +1 -0
  180. package/dist/routers/comment.d.ts +153 -0
  181. package/dist/routers/comment.d.ts.map +1 -0
  182. package/dist/routers/comment.js +58 -0
  183. package/dist/routers/comment.js.map +1 -0
  184. package/dist/routers/conversation.d.ts +73 -3
  185. package/dist/routers/conversation.d.ts.map +1 -1
  186. package/dist/routers/conversation.js +23 -265
  187. package/dist/routers/conversation.js.map +1 -0
  188. package/dist/routers/event.d.ts +46 -37
  189. package/dist/routers/event.d.ts.map +1 -1
  190. package/dist/routers/event.js +15 -431
  191. package/dist/routers/event.js.map +1 -0
  192. package/dist/routers/file.d.ts +4 -2
  193. package/dist/routers/file.d.ts.map +1 -1
  194. package/dist/routers/file.js +11 -298
  195. package/dist/routers/file.js.map +1 -0
  196. package/dist/routers/folder.d.ts +21 -14
  197. package/dist/routers/folder.d.ts.map +1 -1
  198. package/dist/routers/folder.js +36 -743
  199. package/dist/routers/folder.js.map +1 -0
  200. package/dist/routers/labChat.d.ts +12 -9
  201. package/dist/routers/labChat.d.ts.map +1 -1
  202. package/dist/routers/labChat.js +21 -885
  203. package/dist/routers/labChat.js.map +1 -0
  204. package/dist/routers/marketing.d.ts +2 -2
  205. package/dist/routers/marketing.d.ts.map +1 -1
  206. package/dist/routers/marketing.js +9 -54
  207. package/dist/routers/marketing.js.map +1 -0
  208. package/dist/routers/message.d.ts +2 -1
  209. package/dist/routers/message.d.ts.map +1 -1
  210. package/dist/routers/message.js +29 -519
  211. package/dist/routers/message.js.map +1 -0
  212. package/dist/routers/newtonChat.d.ts +55 -0
  213. package/dist/routers/newtonChat.d.ts.map +1 -0
  214. package/dist/routers/newtonChat.js +22 -0
  215. package/dist/routers/newtonChat.js.map +1 -0
  216. package/dist/routers/notifications.d.ts +8 -8
  217. package/dist/routers/notifications.d.ts.map +1 -1
  218. package/dist/routers/notifications.js +20 -81
  219. package/dist/routers/notifications.js.map +1 -0
  220. package/dist/routers/section.d.ts +23 -8
  221. package/dist/routers/section.d.ts.map +1 -1
  222. package/dist/routers/section.js +23 -273
  223. package/dist/routers/section.js.map +1 -0
  224. package/dist/routers/user.d.ts +1 -1
  225. package/dist/routers/user.d.ts.map +1 -1
  226. package/dist/routers/user.js +34 -204
  227. package/dist/routers/user.js.map +1 -0
  228. package/dist/routers/worksheet.d.ts +362 -0
  229. package/dist/routers/worksheet.d.ts.map +1 -0
  230. package/dist/routers/worksheet.js +153 -0
  231. package/dist/routers/worksheet.js.map +1 -0
  232. package/dist/seedDatabase.d.ts +2 -3
  233. package/dist/seedDatabase.d.ts.map +1 -1
  234. package/dist/seedDatabase.js +309 -288
  235. package/dist/seedDatabase.js.map +1 -0
  236. package/dist/server/pipelines/aiLabChat.d.ts +21 -0
  237. package/dist/server/pipelines/aiLabChat.d.ts.map +1 -0
  238. package/dist/server/pipelines/aiLabChat.js +456 -0
  239. package/dist/server/pipelines/aiLabChat.js.map +1 -0
  240. package/dist/server/pipelines/aiNewtonChat.d.ts +30 -0
  241. package/dist/server/pipelines/aiNewtonChat.d.ts.map +1 -0
  242. package/dist/server/pipelines/aiNewtonChat.js +285 -0
  243. package/dist/server/pipelines/aiNewtonChat.js.map +1 -0
  244. package/dist/server/pipelines/gradeWorksheet.d.ts +30 -0
  245. package/dist/server/pipelines/gradeWorksheet.d.ts.map +1 -0
  246. package/dist/server/pipelines/gradeWorksheet.js +248 -0
  247. package/dist/server/pipelines/gradeWorksheet.js.map +1 -0
  248. package/dist/services/agenda.d.ts +100 -0
  249. package/dist/services/agenda.d.ts.map +1 -0
  250. package/dist/services/agenda.js +21 -0
  251. package/dist/services/agenda.js.map +1 -0
  252. package/dist/services/announcement.d.ts +135 -0
  253. package/dist/services/announcement.d.ts.map +1 -0
  254. package/dist/services/announcement.js +223 -0
  255. package/dist/services/announcement.js.map +1 -0
  256. package/dist/services/assignment.d.ts +1462 -0
  257. package/dist/services/assignment.d.ts.map +1 -0
  258. package/dist/services/assignment.js +898 -0
  259. package/dist/services/assignment.js.map +1 -0
  260. package/dist/services/attendance.d.ts +93 -0
  261. package/dist/services/attendance.d.ts.map +1 -0
  262. package/dist/services/attendance.js +61 -0
  263. package/dist/services/attendance.js.map +1 -0
  264. package/dist/services/auth.d.ts +68 -0
  265. package/dist/services/auth.d.ts.map +1 -0
  266. package/dist/services/auth.js +218 -0
  267. package/dist/services/auth.js.map +1 -0
  268. package/dist/services/class.d.ts +621 -0
  269. package/dist/services/class.d.ts.map +1 -0
  270. package/dist/services/class.js +474 -0
  271. package/dist/services/class.js.map +1 -0
  272. package/dist/services/comment.d.ts +100 -0
  273. package/dist/services/comment.d.ts.map +1 -0
  274. package/dist/services/comment.js +83 -0
  275. package/dist/services/comment.js.map +1 -0
  276. package/dist/services/conversation.d.ts +159 -0
  277. package/dist/services/conversation.d.ts.map +1 -0
  278. package/dist/services/conversation.js +138 -0
  279. package/dist/services/conversation.js.map +1 -0
  280. package/dist/services/event.d.ts +216 -0
  281. package/dist/services/event.d.ts.map +1 -0
  282. package/dist/services/event.js +168 -0
  283. package/dist/services/event.js.map +1 -0
  284. package/dist/services/file.d.ts +74 -0
  285. package/dist/services/file.d.ts.map +1 -0
  286. package/dist/services/file.js +133 -0
  287. package/dist/services/file.js.map +1 -0
  288. package/dist/services/folder.d.ts +239 -0
  289. package/dist/services/folder.d.ts.map +1 -0
  290. package/dist/services/folder.js +248 -0
  291. package/dist/services/folder.js.map +1 -0
  292. package/dist/services/labChat.d.ts +165 -0
  293. package/dist/services/labChat.d.ts.map +1 -0
  294. package/dist/services/labChat.js +289 -0
  295. package/dist/services/labChat.js.map +1 -0
  296. package/dist/services/marketing.d.ts +50 -0
  297. package/dist/services/marketing.d.ts.map +1 -0
  298. package/dist/services/marketing.js +32 -0
  299. package/dist/services/marketing.js.map +1 -0
  300. package/dist/services/message.d.ts +95 -0
  301. package/dist/services/message.d.ts.map +1 -0
  302. package/dist/services/message.js +350 -0
  303. package/dist/services/message.js.map +1 -0
  304. package/dist/services/newtonChat.d.ts +22 -0
  305. package/dist/services/newtonChat.d.ts.map +1 -0
  306. package/dist/services/newtonChat.js +174 -0
  307. package/dist/services/newtonChat.js.map +1 -0
  308. package/dist/services/notification.d.ts +65 -0
  309. package/dist/services/notification.d.ts.map +1 -0
  310. package/dist/services/notification.js +33 -0
  311. package/dist/services/notification.js.map +1 -0
  312. package/dist/services/section.d.ts +53 -0
  313. package/dist/services/section.d.ts.map +1 -0
  314. package/dist/services/section.js +199 -0
  315. package/dist/services/section.js.map +1 -0
  316. package/dist/services/user.d.ts +48 -0
  317. package/dist/services/user.d.ts.map +1 -0
  318. package/dist/services/user.js +141 -0
  319. package/dist/services/user.js.map +1 -0
  320. package/dist/services/worksheet.d.ts +239 -0
  321. package/dist/services/worksheet.d.ts.map +1 -0
  322. package/dist/services/worksheet.js +235 -0
  323. package/dist/services/worksheet.js.map +1 -0
  324. package/dist/socket/handlers.d.ts.map +1 -1
  325. package/dist/socket/handlers.js +4 -0
  326. package/dist/socket/handlers.js.map +1 -0
  327. package/dist/trpc.d.ts.map +1 -1
  328. package/dist/trpc.js +4 -0
  329. package/dist/trpc.js.map +1 -0
  330. package/dist/types/trpc.d.ts.map +1 -1
  331. package/dist/types/trpc.js +4 -0
  332. package/dist/types/trpc.js.map +1 -0
  333. package/dist/utils/aiUser.d.ts +1 -3
  334. package/dist/utils/aiUser.d.ts.map +1 -1
  335. package/dist/utils/aiUser.js +8 -3
  336. package/dist/utils/aiUser.js.map +1 -0
  337. package/dist/utils/email.d.ts +12 -1
  338. package/dist/utils/email.d.ts.map +1 -1
  339. package/dist/utils/email.js +26 -4
  340. package/dist/utils/email.js.map +1 -0
  341. package/dist/utils/generateInviteCode.d.ts +1 -2
  342. package/dist/utils/generateInviteCode.d.ts.map +1 -1
  343. package/dist/utils/generateInviteCode.js +5 -2
  344. package/dist/utils/generateInviteCode.js.map +1 -0
  345. package/dist/utils/inference.d.ts +8 -0
  346. package/dist/utils/inference.d.ts.map +1 -1
  347. package/dist/utils/inference.js +78 -10
  348. package/dist/utils/inference.js.map +1 -0
  349. package/dist/utils/logger.d.ts +3 -0
  350. package/dist/utils/logger.d.ts.map +1 -1
  351. package/dist/utils/logger.js +8 -1
  352. package/dist/utils/logger.js.map +1 -0
  353. package/dist/utils/prismaErrorHandler.d.ts.map +1 -1
  354. package/dist/utils/prismaErrorHandler.js +7 -0
  355. package/dist/utils/prismaErrorHandler.js.map +1 -0
  356. package/dist/utils/prismaWrapper.d.ts +1 -0
  357. package/dist/utils/prismaWrapper.d.ts.map +1 -1
  358. package/dist/utils/prismaWrapper.js +8 -0
  359. package/dist/utils/prismaWrapper.js.map +1 -0
  360. package/docker-compose.yml +19 -0
  361. package/package.json +21 -4
  362. package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +30 -0
  363. package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +35 -0
  364. package/prisma/schema.prisma +180 -12
  365. package/scripts/test-pre-push.ts +14 -0
  366. package/src/index.ts +247 -52
  367. package/src/instrument.ts +15 -0
  368. package/src/lib/config/env.ts +132 -0
  369. package/src/lib/fileUpload.ts +13 -6
  370. package/src/lib/googleCloudStorage.ts +23 -6
  371. package/src/lib/jsonConversion.ts +12 -14
  372. package/src/lib/prisma.ts +23 -2
  373. package/src/lib/pusher.ts +11 -6
  374. package/src/lib/redis.ts +56 -0
  375. package/src/lib/thumbnailGenerator.ts +170 -168
  376. package/src/middleware/auth.ts +86 -137
  377. package/src/middleware/security.ts +80 -0
  378. package/src/models/agenda.ts +46 -0
  379. package/src/models/announcement.ts +134 -0
  380. package/src/models/assignment.ts +322 -0
  381. package/src/models/attendance.ts +208 -0
  382. package/src/models/auth.ts +247 -0
  383. package/src/models/class.ts +598 -0
  384. package/src/models/comment.ts +152 -0
  385. package/src/models/conversation.ts +200 -0
  386. package/src/models/event.ts +177 -0
  387. package/src/models/file.ts +129 -0
  388. package/src/models/folder.ts +225 -0
  389. package/src/models/labChat.ts +213 -0
  390. package/src/models/marketing.ts +45 -0
  391. package/src/models/message.ts +153 -0
  392. package/src/models/newtonChat.ts +70 -0
  393. package/src/models/notification.ts +54 -0
  394. package/src/models/section.ts +98 -0
  395. package/src/models/user.ts +47 -0
  396. package/src/models/worksheet.ts +294 -0
  397. package/src/pipelines/aiLabChat.ts +511 -0
  398. package/src/pipelines/aiNewtonChat.ts +347 -0
  399. package/src/pipelines/gradeWorksheet.ts +286 -0
  400. package/src/routers/_app.ts +6 -0
  401. package/src/routers/agenda.ts +3 -61
  402. package/src/routers/announcement.ts +616 -79
  403. package/src/routers/assignment.ts +148 -1827
  404. package/src/routers/attendance.ts +16 -277
  405. package/src/routers/auth.ts +79 -313
  406. package/src/routers/class.ts +265 -1038
  407. package/src/routers/comment.ts +76 -0
  408. package/src/routers/conversation.ts +53 -284
  409. package/src/routers/event.ts +50 -481
  410. package/src/routers/file.ts +45 -344
  411. package/src/routers/folder.ts +107 -836
  412. package/src/routers/labChat.ts +29 -969
  413. package/src/routers/marketing.ts +35 -77
  414. package/src/routers/message.ts +45 -571
  415. package/src/routers/newtonChat.ts +36 -0
  416. package/src/routers/notifications.ts +32 -82
  417. package/src/routers/section.ts +58 -322
  418. package/src/routers/user.ts +49 -226
  419. package/src/routers/worksheet.ts +252 -0
  420. package/src/seedDatabase.ts +328 -289
  421. package/src/services/agenda.ts +21 -0
  422. package/src/services/announcement.ts +290 -0
  423. package/src/services/assignment.ts +1198 -0
  424. package/src/services/attendance.ts +85 -0
  425. package/src/services/auth.ts +277 -0
  426. package/src/services/class.ts +622 -0
  427. package/src/services/comment.ts +106 -0
  428. package/src/services/conversation.ts +213 -0
  429. package/src/services/event.ts +231 -0
  430. package/src/services/file.ts +167 -0
  431. package/src/services/folder.ts +316 -0
  432. package/src/services/labChat.ts +352 -0
  433. package/src/services/marketing.ts +57 -0
  434. package/src/services/message.ts +461 -0
  435. package/src/services/newtonChat.ts +222 -0
  436. package/src/services/notification.ts +50 -0
  437. package/src/services/section.ts +283 -0
  438. package/src/services/user.ts +172 -0
  439. package/src/services/worksheet.ts +358 -0
  440. package/src/trpc.ts +4 -0
  441. package/src/utils/aiUser.ts +4 -3
  442. package/src/utils/email.ts +33 -4
  443. package/src/utils/generateInviteCode.ts +1 -3
  444. package/src/utils/inference.ts +89 -10
  445. package/src/utils/logger.ts +4 -1
  446. package/src/utils/prismaErrorHandler.ts +3 -0
  447. package/src/utils/prismaWrapper.ts +4 -0
  448. package/tests/globalSetup.ts +62 -0
  449. package/tests/helpers.ts +22 -0
  450. package/tests/middleware/security.test.ts +42 -0
  451. package/tests/routers/agenda.test.ts +138 -0
  452. package/tests/routers/announcement.test.ts +490 -0
  453. package/tests/routers/assignment.test.ts +837 -0
  454. package/tests/routers/attendance.test.ts +160 -0
  455. package/tests/routers/auth.test.ts +171 -0
  456. package/tests/{class.test.ts → routers/class.test.ts} +163 -92
  457. package/tests/routers/comment.test.ts +126 -0
  458. package/tests/routers/conversation.test.ts +145 -0
  459. package/tests/routers/event.test.ts +289 -0
  460. package/tests/routers/folder.test.ts +178 -0
  461. package/tests/routers/labChat.test.ts +115 -0
  462. package/tests/routers/marketing.test.ts +59 -0
  463. package/tests/routers/message.test.ts +123 -0
  464. package/tests/routers/notification.test.ts +69 -0
  465. package/tests/routers/section.test.ts +208 -0
  466. package/tests/server/rateLimit.test.ts +73 -0
  467. package/tests/setup.ts +39 -59
  468. package/tests/user.test.ts +136 -0
  469. package/tests/utils/aiUser.test.ts +22 -0
  470. package/tests/utils/generateInviteCode.test.ts +24 -0
  471. package/tests/utils/logger.test.ts +74 -0
  472. package/tests/utils/prismaErrorHandler.test.ts +101 -0
  473. package/tests/utils/prismaWrapper.test.ts +82 -0
  474. package/tests/worksheet.test.ts +181 -0
  475. package/tsconfig.json +9 -2
  476. package/vitest.config.ts +30 -1
  477. package/vitest.unit.config.ts +21 -0
  478. package/API_SPECIFICATION.md +0 -1597
  479. package/BASE64_REMOVAL_SUMMARY.md +0 -164
  480. package/CHAT_API_SPEC.md +0 -579
  481. package/LAB_CHAT_API_SPEC.md +0 -518
  482. package/dist/routers/school.d.ts +0 -208
  483. package/dist/routers/school.d.ts.map +0 -1
  484. package/dist/routers/school.js +0 -481
  485. package/src/lib/notificationHandler.ts +0 -36
  486. package/tests/auth.test.ts +0 -25
@@ -2,161 +2,698 @@ import { z } from "zod";
2
2
  import { createTRPCRouter, protectedClassMemberProcedure, protectedTeacherProcedure, protectedProcedure } from "../trpc.js";
3
3
  import { prisma } from "../lib/prisma.js";
4
4
  import { TRPCError } from "@trpc/server";
5
- import { sendNotifications } from "../lib/notificationHandler.js";
5
+ import {
6
+ getAllAnnouncements,
7
+ getAnnouncement,
8
+ createAnnouncementRecord,
9
+ updateAnnouncementRecord,
10
+ deleteAnnouncementRecord,
11
+ } from "../services/announcement.js";
12
+ import { findAnnouncementByIdAndClass } from "../models/announcement.js";
13
+ import {
14
+ findCommentWithAnnouncement,
15
+ findReactionByUserAndComment,
16
+ upsertReaction,
17
+ deleteReactionById,
18
+ } from "../models/comment.js";
19
+ import { getReactions as getCommentReactions } from "../services/comment.js";
20
+
6
21
  import { logger } from "../utils/logger.js";
22
+ import { createDirectUploadFiles, type UploadedFile, type DirectUploadFile, confirmDirectUpload } from "../lib/fileUpload.js";
23
+ import { deleteFile } from "../lib/googleCloudStorage.js";
24
+ import { sendToMultiple } from "../services/notification.js";
25
+
26
+ // Schema for direct file uploads (no base64 data)
27
+ const directFileSchema = z.object({
28
+ name: z.string(),
29
+ type: z.string(),
30
+ size: z.number(),
31
+ });
7
32
 
8
- const AnnouncementSelect = {
9
- id: true,
10
- teacher: {
11
- select: {
12
- id: true,
13
- username: true,
14
- },
15
- },
16
- remarks: true,
17
- createdAt: true,
18
- };
33
+ // Schemas for file upload endpoints
34
+ const getAnnouncementUploadUrlsSchema = z.object({
35
+ announcementId: z.string(),
36
+ classId: z.string(),
37
+ files: z.array(directFileSchema),
38
+ });
39
+
40
+ const confirmAnnouncementUploadSchema = z.object({
41
+ fileId: z.string(),
42
+ uploadSuccess: z.boolean(),
43
+ errorMessage: z.string().optional(),
44
+ });
19
45
 
20
46
  export const announcementRouter = createTRPCRouter({
21
47
  getAll: protectedClassMemberProcedure
48
+ .input(z.object({ classId: z.string() }))
49
+ .query(({ input }) => getAllAnnouncements(input.classId)),
50
+
51
+ get: protectedClassMemberProcedure
52
+ .input(z.object({ id: z.string(), classId: z.string() }))
53
+ .query(({ input }) => getAnnouncement(input.id, input.classId)),
54
+
55
+ create: protectedTeacherProcedure
22
56
  .input(z.object({
23
57
  classId: z.string(),
58
+ remarks: z.string().min(1, "Remarks cannot be empty"),
59
+ files: z.array(directFileSchema).optional(),
60
+ existingFileIds: z.array(z.string()).optional(),
24
61
  }))
25
- .query(async ({ ctx, input }) => {
26
- const announcements = await prisma.announcement.findMany({
62
+ .mutation(({ ctx, input }) =>
63
+ createAnnouncementRecord(ctx.user!.id, {
64
+ classId: input.classId,
65
+ remarks: input.remarks,
66
+ files: input.files,
67
+ existingFileIds: input.existingFileIds,
68
+ })
69
+ ),
70
+
71
+ update: protectedTeacherProcedure
72
+ .input(z.object({
73
+ id: z.string(),
74
+ classId: z.string(),
75
+ data: z.object({
76
+ remarks: z.string().min(1, "Remarks cannot be empty").optional(),
77
+ files: z.array(directFileSchema).optional(),
78
+ existingFileIds: z.array(z.string()).optional(),
79
+ removedAttachments: z.array(z.string()).optional(),
80
+ }),
81
+ }))
82
+ .mutation(({ ctx, input }) =>
83
+ updateAnnouncementRecord(ctx.user!.id, {
84
+ id: input.id,
85
+ classId: input.classId,
86
+ data: input.data,
87
+ })
88
+ ),
89
+
90
+ delete: protectedTeacherProcedure
91
+ .input(z.object({ id: z.string(), classId: z.string() }))
92
+ .mutation(({ ctx, input }) =>
93
+ deleteAnnouncementRecord(ctx.user!.id, input.id, input.classId)
94
+ ),
95
+
96
+ getAnnouncementUploadUrls: protectedTeacherProcedure
97
+ .input(getAnnouncementUploadUrlsSchema)
98
+ .mutation(async ({ ctx, input }) => {
99
+ const { announcementId, classId, files } = input;
100
+
101
+ if (!ctx.user) {
102
+ throw new TRPCError({
103
+ code: "UNAUTHORIZED",
104
+ message: "You must be logged in to upload files",
105
+ });
106
+ }
107
+
108
+ // Verify user is a teacher of the class
109
+ const classData = await prisma.class.findFirst({
27
110
  where: {
28
- classId: input.classId,
111
+ id: classId,
112
+ teachers: {
113
+ some: {
114
+ id: ctx.user.id,
115
+ },
116
+ },
29
117
  },
30
- select: AnnouncementSelect,
31
- orderBy: {
32
- createdAt: 'desc',
118
+ });
119
+
120
+ if (!classData) {
121
+ throw new TRPCError({
122
+ code: "NOT_FOUND",
123
+ message: "Class not found or you are not a teacher",
124
+ });
125
+ }
126
+
127
+ const announcement = await findAnnouncementByIdAndClass(announcementId, classId);
128
+ if (!announcement) {
129
+ throw new TRPCError({
130
+ code: "NOT_FOUND",
131
+ message: "Announcement not found",
132
+ });
133
+ }
134
+
135
+ // Create direct upload files
136
+ const directUploadFiles = await createDirectUploadFiles(
137
+ files,
138
+ ctx.user.id,
139
+ undefined, // No specific directory
140
+ undefined, // No assignment ID
141
+ undefined, // No submission ID
142
+ announcementId
143
+ );
144
+
145
+ return {
146
+ success: true,
147
+ uploadFiles: directUploadFiles,
148
+ };
149
+ }),
150
+
151
+ confirmAnnouncementUpload: protectedTeacherProcedure
152
+ .input(confirmAnnouncementUploadSchema)
153
+ .mutation(async ({ ctx, input }) => {
154
+ const { fileId, uploadSuccess, errorMessage } = input;
155
+
156
+ if (!ctx.user) {
157
+ throw new TRPCError({
158
+ code: "UNAUTHORIZED",
159
+ message: "You must be logged in",
160
+ });
161
+ }
162
+
163
+ // Verify file belongs to user and is an announcement file
164
+ const file = await prisma.file.findFirst({
165
+ where: {
166
+ id: fileId,
167
+ userId: ctx.user.id,
168
+ announcement: {
169
+ isNot: null,
170
+ },
33
171
  },
34
172
  });
35
173
 
174
+ if (!file) {
175
+ throw new TRPCError({
176
+ code: "NOT_FOUND",
177
+ message: "File not found or you don't have permission",
178
+ });
179
+ }
180
+
181
+ await confirmDirectUpload(fileId, uploadSuccess, errorMessage);
182
+
36
183
  return {
37
- announcements,
184
+ success: true,
185
+ message: uploadSuccess ? "Upload confirmed successfully" : "Upload failed",
38
186
  };
39
187
  }),
40
188
 
41
- create: protectedTeacherProcedure
189
+ // Comment endpoints
190
+ addComment: protectedClassMemberProcedure
42
191
  .input(z.object({
192
+ announcementId: z.string(),
43
193
  classId: z.string(),
44
- remarks: z.string(),
194
+ content: z.string().min(1, "Comment cannot be empty"),
195
+ parentCommentId: z.string().optional(),
45
196
  }))
46
197
  .mutation(async ({ ctx, input }) => {
47
- const classId = input.classId
48
- const remarks = input.remarks
49
-
50
- const classData = await prisma.class.findUnique({
51
- where: { id: classId },
52
- include: {
53
- students: {
54
- select: { id: true }
55
- }
56
- }
57
- });
198
+ if (!ctx.user) {
199
+ throw new TRPCError({
200
+ code: "UNAUTHORIZED",
201
+ message: "User must be authenticated",
202
+ });
203
+ }
58
204
 
59
- if (!classData) {
205
+ const announcement = await findAnnouncementByIdAndClass(input.announcementId, input.classId);
206
+ if (!announcement) {
60
207
  throw new TRPCError({
61
- code: "NOT_FOUND",
62
- message: "Class not found",
208
+ code: "NOT_FOUND",
209
+ message: "Announcement not found",
210
+ });
211
+ }
212
+
213
+ // If replying to a comment, verify parent comment exists and belongs to the same announcement
214
+ if (input.parentCommentId) {
215
+ const parentComment = await prisma.comment.findFirst({
216
+ where: {
217
+ id: input.parentCommentId,
218
+ announcementId: input.announcementId,
219
+ },
63
220
  });
221
+
222
+ if (!parentComment) {
223
+ throw new TRPCError({
224
+ code: "NOT_FOUND",
225
+ message: "Parent comment not found",
226
+ });
227
+ }
64
228
  }
65
229
 
66
- const announcement = await prisma.announcement.create({
230
+ const comment = await prisma.comment.create({
67
231
  data: {
68
- remarks: remarks,
69
- teacher: {
70
- connect: {
71
- id: ctx.user?.id,
72
- },
232
+ content: input.content,
233
+ author: {
234
+ connect: { id: ctx.user.id },
73
235
  },
74
- class: {
75
- connect: {
76
- id: classId,
236
+ announcement: {
237
+ connect: { id: input.announcementId },
238
+ },
239
+ ...(input.parentCommentId && {
240
+ parentComment: {
241
+ connect: { id: input.parentCommentId },
242
+ },
243
+ }),
244
+ },
245
+ include: {
246
+ author: {
247
+ select: {
248
+ id: true,
249
+ username: true,
250
+ profile: {
251
+ select: {
252
+ displayName: true,
253
+ profilePicture: true,
254
+ profilePictureThumbnail: true,
255
+ },
256
+ },
77
257
  },
78
258
  },
79
259
  },
80
- select: AnnouncementSelect,
81
260
  });
82
261
 
83
- sendNotifications(classData.students.map(student => student.id), {
84
- title: `🔔 Announcement for ${classData.name}`,
85
- content: remarks
86
- }).catch(error => {
87
- logger.error('Failed to send announcement notifications:');
88
- });
89
-
90
- return {
91
- announcement,
92
- };
262
+ return { comment };
93
263
  }),
94
264
 
95
- update: protectedProcedure
265
+ updateComment: protectedProcedure
96
266
  .input(z.object({
97
267
  id: z.string(),
98
- data: z.object({
99
- content: z.string(),
100
- }),
268
+ content: z.string().min(1, "Comment cannot be empty"),
101
269
  }))
102
270
  .mutation(async ({ ctx, input }) => {
271
+ if (!ctx.user) {
272
+ throw new TRPCError({
273
+ code: "UNAUTHORIZED",
274
+ message: "User must be authenticated",
275
+ });
276
+ }
103
277
 
104
- const announcement = await prisma.announcement.findUnique({
278
+ const comment = await prisma.comment.findUnique({
105
279
  where: { id: input.id },
106
- include: {
107
- class: {
108
- include: {
109
- teachers: true,
110
- },
111
- },
112
- },
113
280
  });
114
281
 
115
- if (!announcement) {
282
+ if (!comment) {
116
283
  throw new TRPCError({
117
284
  code: "NOT_FOUND",
118
- message: "Announcement not found",
285
+ message: "Comment not found",
119
286
  });
120
287
  }
121
288
 
122
- const updatedAnnouncement = await prisma.announcement.update({
289
+ // Only the author can update their comment
290
+ if (comment.authorId !== ctx.user.id) {
291
+ throw new TRPCError({
292
+ code: "FORBIDDEN",
293
+ message: "Only the comment author can update this comment",
294
+ });
295
+ }
296
+
297
+ const updatedComment = await prisma.comment.update({
123
298
  where: { id: input.id },
124
299
  data: {
125
- remarks: input.data.content,
300
+ content: input.content,
301
+ },
302
+ include: {
303
+ author: {
304
+ select: {
305
+ id: true,
306
+ username: true,
307
+ profile: {
308
+ select: {
309
+ displayName: true,
310
+ profilePicture: true,
311
+ profilePictureThumbnail: true,
312
+ },
313
+ },
314
+ },
315
+ },
126
316
  },
127
317
  });
128
318
 
129
- return { announcement: updatedAnnouncement };
319
+ return { comment: updatedComment };
130
320
  }),
131
321
 
132
- delete: protectedProcedure
322
+ deleteComment: protectedProcedure
133
323
  .input(z.object({
134
324
  id: z.string(),
135
325
  }))
136
326
  .mutation(async ({ ctx, input }) => {
327
+ if (!ctx.user) {
328
+ throw new TRPCError({
329
+ code: "UNAUTHORIZED",
330
+ message: "User must be authenticated",
331
+ });
332
+ }
137
333
 
138
- const announcement = await prisma.announcement.findUnique({
334
+ const comment = await prisma.comment.findUnique({
139
335
  where: { id: input.id },
140
336
  include: {
141
- class: {
337
+ announcement: {
142
338
  include: {
143
- teachers: true,
339
+ class: {
340
+ include: {
341
+ teachers: true,
342
+ },
343
+ },
144
344
  },
145
345
  },
146
346
  },
147
347
  });
148
348
 
149
- if (!announcement) {
349
+ if (!comment) {
150
350
  throw new TRPCError({
151
351
  code: "NOT_FOUND",
152
- message: "Announcement not found",
352
+ message: "Comment not found",
153
353
  });
154
354
  }
155
355
 
156
- await prisma.announcement.delete({
356
+ // Only the author or a class teacher can delete comments
357
+ const userId = ctx.user.id;
358
+ const isAuthor = comment.authorId === userId;
359
+ const isClassTeacher = comment.announcement!.class.teachers.some(
360
+ (teacher) => teacher.id === userId
361
+ );
362
+
363
+ if (!isAuthor && !isClassTeacher) {
364
+ throw new TRPCError({
365
+ code: "FORBIDDEN",
366
+ message: "Only the comment author or class teachers can delete comments",
367
+ });
368
+ }
369
+
370
+ await prisma.comment.delete({
157
371
  where: { id: input.id },
158
372
  });
159
373
 
160
374
  return { success: true };
161
375
  }),
376
+
377
+ getComments: protectedClassMemberProcedure
378
+ .input(z.object({
379
+ announcementId: z.string(),
380
+ classId: z.string(),
381
+ }))
382
+ .query(async ({ ctx, input }) => {
383
+ const announcement = await findAnnouncementByIdAndClass(input.announcementId, input.classId);
384
+ if (!announcement) {
385
+ throw new TRPCError({
386
+ code: "NOT_FOUND",
387
+ message: "Announcement not found",
388
+ });
389
+ }
390
+
391
+ // Get all top-level comments (no parent)
392
+ const comments = await prisma.comment.findMany({
393
+ where: {
394
+ announcementId: input.announcementId,
395
+ parentCommentId: null,
396
+ },
397
+ include: {
398
+ author: {
399
+ select: {
400
+ id: true,
401
+ username: true,
402
+ profile: {
403
+ select: {
404
+ displayName: true,
405
+ profilePicture: true,
406
+ profilePictureThumbnail: true,
407
+ },
408
+ },
409
+ },
410
+ },
411
+ replies: {
412
+ include: {
413
+ author: {
414
+ select: {
415
+ id: true,
416
+ username: true,
417
+ profile: {
418
+ select: {
419
+ displayName: true,
420
+ profilePicture: true,
421
+ profilePictureThumbnail: true,
422
+ },
423
+ },
424
+ },
425
+ },
426
+ },
427
+ orderBy: {
428
+ createdAt: 'asc',
429
+ },
430
+ },
431
+ },
432
+ orderBy: {
433
+ createdAt: 'asc',
434
+ },
435
+ });
436
+
437
+ return { comments };
438
+ }),
439
+
440
+ // Reaction endpoints
441
+ addReaction: protectedClassMemberProcedure
442
+ .input(z.object({
443
+ announcementId: z.string().optional(),
444
+ commentId: z.string().optional(),
445
+ classId: z.string(),
446
+ type: z.enum(['THUMBSUP', 'CELEBRATE', 'CARE', 'HEART', 'IDEA', 'HAPPY']),
447
+ }))
448
+ .mutation(async ({ ctx, input }) => {
449
+ if (!ctx.user) {
450
+ throw new TRPCError({
451
+ code: "UNAUTHORIZED",
452
+ message: "User must be authenticated",
453
+ });
454
+ }
455
+
456
+ // Exactly one of announcementId or commentId must be provided
457
+ if (!input.announcementId && !input.commentId) {
458
+ throw new TRPCError({
459
+ code: "BAD_REQUEST",
460
+ message: "Either announcementId or commentId must be provided",
461
+ });
462
+ }
463
+
464
+ if (input.announcementId && input.commentId) {
465
+ throw new TRPCError({
466
+ code: "BAD_REQUEST",
467
+ message: "Cannot react to both announcement and comment at the same time",
468
+ });
469
+ }
470
+
471
+ const userId = ctx.user.id;
472
+
473
+ if (input.announcementId) {
474
+ const announcement = await findAnnouncementByIdAndClass(input.announcementId, input.classId);
475
+ if (!announcement) {
476
+ throw new TRPCError({
477
+ code: "NOT_FOUND",
478
+ message: "Announcement not found",
479
+ });
480
+ }
481
+
482
+ // Upsert reaction: update if exists, create if not
483
+ const reaction = await prisma.reaction.upsert({
484
+ where: {
485
+ userId_announcementId: {
486
+ userId,
487
+ announcementId: input.announcementId,
488
+ },
489
+ },
490
+ update: {
491
+ type: input.type,
492
+ },
493
+ create: {
494
+ type: input.type,
495
+ userId,
496
+ announcementId: input.announcementId,
497
+ },
498
+ include: {
499
+ user: {
500
+ select: {
501
+ id: true,
502
+ username: true,
503
+ profile: {
504
+ select: {
505
+ displayName: true,
506
+ profilePicture: true,
507
+ profilePictureThumbnail: true,
508
+ },
509
+ },
510
+ },
511
+ },
512
+ },
513
+ });
514
+
515
+ return { reaction };
516
+ } else if (input.commentId) {
517
+ const comment = await findCommentWithAnnouncement(input.commentId);
518
+ if (!comment) {
519
+ throw new TRPCError({
520
+ code: "NOT_FOUND",
521
+ message: "Comment not found",
522
+ });
523
+ }
524
+ if (comment.announcement!.classId !== input.classId) {
525
+ throw new TRPCError({
526
+ code: "FORBIDDEN",
527
+ message: "Comment does not belong to this class",
528
+ });
529
+ }
530
+
531
+ const reaction = await upsertReaction({
532
+ userId,
533
+ commentId: input.commentId,
534
+ type: input.type,
535
+ });
536
+ return { reaction };
537
+ }
538
+
539
+ throw new TRPCError({
540
+ code: "INTERNAL_SERVER_ERROR",
541
+ message: "Unexpected error",
542
+ });
543
+ }),
544
+
545
+ removeReaction: protectedProcedure
546
+ .input(z.object({
547
+ announcementId: z.string().optional(),
548
+ commentId: z.string().optional(),
549
+ }))
550
+ .mutation(async ({ ctx, input }) => {
551
+ if (!ctx.user) {
552
+ throw new TRPCError({
553
+ code: "UNAUTHORIZED",
554
+ message: "User must be authenticated",
555
+ });
556
+ }
557
+
558
+ // Exactly one of announcementId or commentId must be provided
559
+ if (!input.announcementId && !input.commentId) {
560
+ throw new TRPCError({
561
+ code: "BAD_REQUEST",
562
+ message: "Either announcementId or commentId must be provided",
563
+ });
564
+ }
565
+
566
+ const userId = ctx.user.id;
567
+
568
+ if (input.announcementId) {
569
+ const reaction = await prisma.reaction.findUnique({
570
+ where: {
571
+ userId_announcementId: {
572
+ userId,
573
+ announcementId: input.announcementId,
574
+ },
575
+ },
576
+ });
577
+
578
+ if (!reaction) {
579
+ throw new TRPCError({
580
+ code: "NOT_FOUND",
581
+ message: "Reaction not found",
582
+ });
583
+ }
584
+
585
+ await prisma.reaction.delete({
586
+ where: { id: reaction.id },
587
+ });
588
+
589
+ return { success: true };
590
+ } else if (input.commentId) {
591
+ const reaction = await findReactionByUserAndComment(userId, input.commentId);
592
+ if (!reaction) {
593
+ throw new TRPCError({
594
+ code: "NOT_FOUND",
595
+ message: "Reaction not found",
596
+ });
597
+ }
598
+ await deleteReactionById(reaction.id);
599
+ return { success: true };
600
+ }
601
+
602
+ throw new TRPCError({
603
+ code: "INTERNAL_SERVER_ERROR",
604
+ message: "Unexpected error",
605
+ });
606
+ }),
607
+
608
+ getReactions: protectedClassMemberProcedure
609
+ .input(z.object({
610
+ announcementId: z.string().optional(),
611
+ commentId: z.string().optional(),
612
+ classId: z.string(),
613
+ }))
614
+ .query(async ({ ctx, input }) => {
615
+ if (!ctx.user) {
616
+ throw new TRPCError({
617
+ code: "UNAUTHORIZED",
618
+ message: "User must be authenticated",
619
+ });
620
+ }
621
+
622
+ // Exactly one of announcementId or commentId must be provided
623
+ if (!input.announcementId && !input.commentId) {
624
+ throw new TRPCError({
625
+ code: "BAD_REQUEST",
626
+ message: "Either announcementId or commentId must be provided",
627
+ });
628
+ }
629
+
630
+ const userId = ctx.user.id;
631
+
632
+ if (input.announcementId) {
633
+ const announcement = await findAnnouncementByIdAndClass(input.announcementId, input.classId);
634
+ if (!announcement) {
635
+ throw new TRPCError({
636
+ code: "NOT_FOUND",
637
+ message: "Announcement not found",
638
+ });
639
+ }
640
+
641
+ // Get reaction counts by type
642
+ const reactionCounts = await prisma.reaction.groupBy({
643
+ by: ['type'],
644
+ where: { announcementId: input.announcementId },
645
+ _count: { type: true },
646
+ });
647
+
648
+ // Get current user's reaction
649
+ const userReaction = await prisma.reaction.findUnique({
650
+ where: {
651
+ userId_announcementId: {
652
+ userId,
653
+ announcementId: input.announcementId,
654
+ },
655
+ },
656
+ });
657
+
658
+ // Format counts
659
+ const counts = {
660
+ THUMBSUP: 0,
661
+ CELEBRATE: 0,
662
+ CARE: 0,
663
+ HEART: 0,
664
+ IDEA: 0,
665
+ HAPPY: 0,
666
+ };
667
+
668
+ reactionCounts.forEach((item) => {
669
+ counts[item.type as keyof typeof counts] = item._count.type;
670
+ });
671
+
672
+ return {
673
+ counts,
674
+ userReaction: userReaction?.type || null,
675
+ total: reactionCounts.reduce((sum, item) => sum + item._count.type, 0),
676
+ };
677
+ } else if (input.commentId) {
678
+ const comment = await findCommentWithAnnouncement(input.commentId);
679
+ if (!comment) {
680
+ throw new TRPCError({
681
+ code: "NOT_FOUND",
682
+ message: "Comment not found",
683
+ });
684
+ }
685
+ if (comment.announcement!.classId !== input.classId) {
686
+ throw new TRPCError({
687
+ code: "FORBIDDEN",
688
+ message: "Comment does not belong to this class",
689
+ });
690
+ }
691
+ return getCommentReactions(userId, input.commentId);
692
+ }
693
+
694
+ throw new TRPCError({
695
+ code: "INTERNAL_SERVER_ERROR",
696
+ message: "Unexpected error",
697
+ });
698
+ }),
162
699
  });