@studious-lms/server 1.1.24 → 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 (485) 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 +82 -15
  23. package/dist/lib/fileUpload.js.map +1 -0
  24. package/dist/lib/googleCloudStorage.d.ts +13 -0
  25. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  26. package/dist/lib/googleCloudStorage.js +45 -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 +25 -0
  35. package/dist/lib/notificationHandler.d.ts.map +1 -0
  36. package/dist/lib/notificationHandler.js +32 -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 +6403 -3741
  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 +547 -57
  163. package/dist/routers/announcement.js.map +1 -0
  164. package/dist/routers/assignment.d.ts +431 -318
  165. package/dist/routers/assignment.d.ts.map +1 -1
  166. package/dist/routers/assignment.js +104 -1559
  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 -295
  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 -877
  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 +37 -6
  221. package/dist/routers/section.d.ts.map +1 -1
  222. package/dist/routers/section.js +26 -167
  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 +311 -289
  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 +4 -0
  350. package/dist/utils/logger.d.ts.map +1 -1
  351. package/dist/utils/logger.js +35 -3
  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 +81 -16
  370. package/src/lib/googleCloudStorage.ts +42 -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 +622 -57
  403. package/src/routers/assignment.ts +157 -1688
  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 -341
  411. package/src/routers/folder.ts +107 -836
  412. package/src/routers/labChat.ts +29 -960
  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 -200
  418. package/src/routers/user.ts +49 -226
  419. package/src/routers/worksheet.ts +252 -0
  420. package/src/seedDatabase.ts +330 -290
  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 +33 -3
  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 -65
  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/tests/auth.test.ts +0 -25
@@ -2,133 +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 {
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";
5
20
 
6
- const AnnouncementSelect = {
7
- id: true,
8
- teacher: {
9
- select: {
10
- id: true,
11
- username: true,
12
- },
13
- },
14
- remarks: true,
15
- createdAt: true,
16
- };
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
+ });
32
+
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
+ });
17
45
 
18
46
  export const announcementRouter = createTRPCRouter({
19
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
20
56
  .input(z.object({
21
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(),
22
61
  }))
23
- .query(async ({ ctx, input }) => {
24
- 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({
25
110
  where: {
26
- classId: input.classId,
111
+ id: classId,
112
+ teachers: {
113
+ some: {
114
+ id: ctx.user.id,
115
+ },
116
+ },
27
117
  },
28
- select: AnnouncementSelect,
29
- orderBy: {
30
- 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
+ },
31
171
  },
32
172
  });
33
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
+
34
183
  return {
35
- announcements,
184
+ success: true,
185
+ message: uploadSuccess ? "Upload confirmed successfully" : "Upload failed",
36
186
  };
37
187
  }),
38
188
 
39
- create: protectedTeacherProcedure
189
+ // Comment endpoints
190
+ addComment: protectedClassMemberProcedure
40
191
  .input(z.object({
192
+ announcementId: z.string(),
41
193
  classId: z.string(),
42
- remarks: z.string(),
194
+ content: z.string().min(1, "Comment cannot be empty"),
195
+ parentCommentId: z.string().optional(),
43
196
  }))
44
197
  .mutation(async ({ ctx, input }) => {
45
- const announcement = await prisma.announcement.create({
198
+ if (!ctx.user) {
199
+ throw new TRPCError({
200
+ code: "UNAUTHORIZED",
201
+ message: "User must be authenticated",
202
+ });
203
+ }
204
+
205
+ const announcement = await findAnnouncementByIdAndClass(input.announcementId, input.classId);
206
+ if (!announcement) {
207
+ throw new TRPCError({
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
+ },
220
+ });
221
+
222
+ if (!parentComment) {
223
+ throw new TRPCError({
224
+ code: "NOT_FOUND",
225
+ message: "Parent comment not found",
226
+ });
227
+ }
228
+ }
229
+
230
+ const comment = await prisma.comment.create({
46
231
  data: {
47
- remarks: input.remarks,
48
- teacher: {
49
- connect: {
50
- id: ctx.user?.id,
51
- },
232
+ content: input.content,
233
+ author: {
234
+ connect: { id: ctx.user.id },
235
+ },
236
+ announcement: {
237
+ connect: { id: input.announcementId },
52
238
  },
53
- class: {
54
- connect: {
55
- id: input.classId,
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
+ },
56
257
  },
57
258
  },
58
259
  },
59
- select: AnnouncementSelect,
60
260
  });
61
261
 
62
- return {
63
- announcement,
64
- };
262
+ return { comment };
65
263
  }),
66
264
 
67
- update: protectedProcedure
265
+ updateComment: protectedProcedure
68
266
  .input(z.object({
69
267
  id: z.string(),
70
- data: z.object({
71
- content: z.string(),
72
- }),
268
+ content: z.string().min(1, "Comment cannot be empty"),
73
269
  }))
74
270
  .mutation(async ({ ctx, input }) => {
271
+ if (!ctx.user) {
272
+ throw new TRPCError({
273
+ code: "UNAUTHORIZED",
274
+ message: "User must be authenticated",
275
+ });
276
+ }
75
277
 
76
- const announcement = await prisma.announcement.findUnique({
278
+ const comment = await prisma.comment.findUnique({
77
279
  where: { id: input.id },
78
- include: {
79
- class: {
80
- include: {
81
- teachers: true,
82
- },
83
- },
84
- },
85
280
  });
86
281
 
87
- if (!announcement) {
282
+ if (!comment) {
88
283
  throw new TRPCError({
89
284
  code: "NOT_FOUND",
90
- message: "Announcement not found",
285
+ message: "Comment not found",
286
+ });
287
+ }
288
+
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",
91
294
  });
92
295
  }
93
296
 
94
- const updatedAnnouncement = await prisma.announcement.update({
297
+ const updatedComment = await prisma.comment.update({
95
298
  where: { id: input.id },
96
299
  data: {
97
- 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
+ },
98
316
  },
99
317
  });
100
318
 
101
- return { announcement: updatedAnnouncement };
319
+ return { comment: updatedComment };
102
320
  }),
103
321
 
104
- delete: protectedProcedure
322
+ deleteComment: protectedProcedure
105
323
  .input(z.object({
106
324
  id: z.string(),
107
325
  }))
108
326
  .mutation(async ({ ctx, input }) => {
327
+ if (!ctx.user) {
328
+ throw new TRPCError({
329
+ code: "UNAUTHORIZED",
330
+ message: "User must be authenticated",
331
+ });
332
+ }
109
333
 
110
- const announcement = await prisma.announcement.findUnique({
334
+ const comment = await prisma.comment.findUnique({
111
335
  where: { id: input.id },
112
336
  include: {
113
- class: {
337
+ announcement: {
114
338
  include: {
115
- teachers: true,
339
+ class: {
340
+ include: {
341
+ teachers: true,
342
+ },
343
+ },
116
344
  },
117
345
  },
118
346
  },
119
347
  });
120
348
 
121
- if (!announcement) {
349
+ if (!comment) {
122
350
  throw new TRPCError({
123
351
  code: "NOT_FOUND",
124
- message: "Announcement not found",
352
+ message: "Comment not found",
353
+ });
354
+ }
355
+
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",
125
367
  });
126
368
  }
127
369
 
128
- await prisma.announcement.delete({
370
+ await prisma.comment.delete({
129
371
  where: { id: input.id },
130
372
  });
131
373
 
132
374
  return { success: true };
133
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
+ }),
134
699
  });