@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
@@ -4,8 +4,31 @@ import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
5
  import { createDirectUploadFiles, type DirectUploadFile, confirmDirectUpload, updateUploadProgress, type UploadedFile } from "../lib/fileUpload.js";
6
6
  import { deleteFile } from "../lib/googleCloudStorage.js";
7
- import { sendNotifications } from "../lib/notificationHandler.js";
8
7
  import { logger } from "../utils/logger.js";
8
+ import { gradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
9
+ import {
10
+ assignmentExists,
11
+ getDueToday,
12
+ getAssignment,
13
+ getSubmission,
14
+ getSubmissionById,
15
+ getSubmissions,
16
+ createAssignmentRecord,
17
+ updateAssignmentRecord,
18
+ deleteAssignmentRecord,
19
+ updateSubmissionRecord,
20
+ updateSubmissionAsTeacherRecord,
21
+ attachAssignmentToEventRecord,
22
+ detachAssignmentFromEventRecord,
23
+ getAvailableEventsForAssignment,
24
+ attachMarkSchemeRecord,
25
+ detachMarkSchemeRecord,
26
+ attachGradingBoundaryRecord,
27
+ detachGradingBoundaryRecord,
28
+ reorderAssignmentRecord,
29
+ moveAssignmentRecord,
30
+ } from "../services/assignment.js";
31
+ import { sendToMultiple } from "../services/notification.js";
9
32
 
10
33
  // DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
11
34
  // Use directFileSchema instead
@@ -20,11 +43,19 @@ const directFileSchema = z.object({
20
43
 
21
44
  const createAssignmentSchema = z.object({
22
45
  classId: z.string(),
46
+ id: z.string().optional(),
23
47
  title: z.string(),
24
48
  instructions: z.string(),
25
49
  dueDate: z.string(),
26
50
  files: z.array(directFileSchema).optional(), // Use direct file schema
27
51
  existingFileIds: z.array(z.string()).optional(),
52
+ aiPolicyLevel: z.number().default(0),
53
+ acceptFiles: z.boolean().optional(),
54
+ acceptExtendedResponse: z.boolean().optional(),
55
+ acceptWorksheet: z.boolean().optional(),
56
+ worksheetIds: z.array(z.string()).optional(),
57
+ gradeWithAI: z.boolean().optional(),
58
+ studentIds: z.array(z.string()).optional(),
28
59
  maxGrade: z.number().optional(),
29
60
  graded: z.boolean().optional(),
30
61
  weight: z.number().optional(),
@@ -42,6 +73,13 @@ const updateAssignmentSchema = z.object({
42
73
  instructions: z.string().optional(),
43
74
  dueDate: z.string().optional(),
44
75
  files: z.array(directFileSchema).optional(), // Use direct file schema
76
+ aiPolicyLevel: z.number().default(0),
77
+ acceptFiles: z.boolean().optional(),
78
+ acceptExtendedResponse: z.boolean().optional(),
79
+ acceptWorksheet: z.boolean().optional(),
80
+ worksheetIds: z.array(z.string()).optional(),
81
+ gradeWithAI: z.boolean().optional(),
82
+ studentIds: z.array(z.string()).optional(),
45
83
  existingFileIds: z.array(z.string()).optional(),
46
84
  removedAttachments: z.array(z.string()).optional(),
47
85
  maxGrade: z.number().optional(),
@@ -68,6 +106,7 @@ const submissionSchema = z.object({
68
106
  submissionId: z.string(),
69
107
  submit: z.boolean().optional(),
70
108
  newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
109
+ extendedResponse: z.string().optional(),
71
110
  existingFileIds: z.array(z.string()).optional(),
72
111
  removedAttachments: z.array(z.string()).optional(),
73
112
  });
@@ -132,1869 +171,151 @@ const updateUploadProgressSchema = z.object({
132
171
  progress: z.number().min(0).max(100),
133
172
  });
134
173
 
135
- // Helper function to get unified list of sections and assignments for a class
136
- async function getUnifiedList(tx: any, classId: string) {
137
- const [sections, assignments] = await Promise.all([
138
- tx.section.findMany({
139
- where: { classId },
140
- select: { id: true, order: true },
141
- }),
142
- tx.assignment.findMany({
143
- where: { classId },
144
- select: { id: true, order: true },
145
- }),
146
- ]);
147
-
148
- // Combine and sort by order
149
- const unified = [
150
- ...sections.map((s: any) => ({ id: s.id, order: s.order, type: 'section' as const })),
151
- ...assignments.map((a: any) => ({ id: a.id, order: a.order, type: 'assignment' as const })),
152
- ].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
153
-
154
- return unified;
155
- }
156
-
157
- // Helper function to normalize unified list to 1..n
158
- async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Array<{ id: string; type: 'section' | 'assignment' }>) {
159
- await Promise.all(
160
- orderedItems.map((item, index) => {
161
- if (item.type === 'section') {
162
- return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
163
- } else {
164
- return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
165
- }
166
- })
167
- );
168
- }
169
-
170
174
  export const assignmentRouter = createTRPCRouter({
171
- // Reorder an assignment within the unified list (sections + assignments)
172
175
  reorder: protectedTeacherProcedure
173
176
  .input(z.object({
174
177
  classId: z.string(),
175
178
  movedId: z.string(),
176
- // One of: place at start/end of unified list, or relative to targetId (can be section or assignment)
177
179
  position: z.enum(['start', 'end', 'before', 'after']),
178
- targetId: z.string().optional(), // Can be a section ID or assignment ID
179
- }))
180
- .mutation(async ({ ctx, input }) => {
181
- const { classId, movedId, position, targetId } = input;
182
-
183
- const moved = await prisma.assignment.findFirst({
184
- where: { id: movedId, classId },
185
- select: { id: true, classId: true },
186
- });
187
-
188
- if (!moved) {
189
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
190
- }
191
-
192
- if ((position === 'before' || position === 'after') && !targetId) {
193
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId required for before/after' });
194
- }
195
-
196
- const result = await prisma.$transaction(async (tx) => {
197
- const unified = await getUnifiedList(tx, classId);
198
-
199
- // Find moved item and target in unified list
200
- const movedIdx = unified.findIndex(item => item.id === movedId && item.type === 'assignment');
201
- if (movedIdx === -1) {
202
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found in unified list' });
203
- }
204
-
205
- // Build list without moved item
206
- const withoutMoved = unified.filter(item => !(item.id === movedId && item.type === 'assignment'));
207
-
208
- let next: Array<{ id: string; type: 'section' | 'assignment' }> = [];
209
-
210
- if (position === 'start') {
211
- next = [{ id: movedId, type: 'assignment' }, ...withoutMoved.map(item => ({ id: item.id, type: item.type }))];
212
- } else if (position === 'end') {
213
- next = [...withoutMoved.map(item => ({ id: item.id, type: item.type })), { id: movedId, type: 'assignment' }];
214
- } else {
215
- const targetIdx = withoutMoved.findIndex(item => item.id === targetId);
216
- if (targetIdx === -1) {
217
- throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId not found in unified list' });
218
- }
219
- if (position === 'before') {
220
- next = [
221
- ...withoutMoved.slice(0, targetIdx).map(item => ({ id: item.id, type: item.type })),
222
- { id: movedId, type: 'assignment' },
223
- ...withoutMoved.slice(targetIdx).map(item => ({ id: item.id, type: item.type })),
224
- ];
225
- } else {
226
- next = [
227
- ...withoutMoved.slice(0, targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
228
- { id: movedId, type: 'assignment' },
229
- ...withoutMoved.slice(targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
230
- ];
231
- }
232
- }
233
-
234
- // Normalize to 1..n
235
- await normalizeUnifiedList(tx, classId, next);
236
-
237
- return tx.assignment.findUnique({ where: { id: movedId } });
238
- });
239
-
240
- return result;
241
- }),
242
- order: protectedTeacherProcedure
243
- .input(z.object({
244
- id: z.string(),
245
- classId: z.string(),
246
- order: z.number(),
180
+ targetId: z.string().optional(),
247
181
  }))
248
- .mutation(async ({ ctx, input }) => {
249
- // Deprecated: prefer `reorder`. For backward-compatibility, set the order then normalize unified list.
250
- const { id, order } = input;
251
-
252
- const current = await prisma.assignment.findUnique({
253
- where: { id },
254
- select: { id: true, classId: true },
255
- });
256
-
257
- if (!current) {
258
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
259
- }
260
-
261
- const updated = await prisma.$transaction(async (tx) => {
262
- await tx.assignment.update({ where: { id }, data: { order } });
263
-
264
- // Normalize entire unified list
265
- const unified = await getUnifiedList(tx, current.classId);
266
- await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
267
-
268
- return tx.assignment.findUnique({ where: { id } });
269
- });
270
-
271
- return updated;
272
- }),
273
-
274
- move: protectedTeacherProcedure
182
+ .mutation(({ ctx, input }) =>
183
+ reorderAssignmentRecord(ctx.user!.id, {
184
+ classId: input.classId,
185
+ movedId: input.movedId,
186
+ position: input.position,
187
+ targetId: input.targetId,
188
+ })
189
+ ),
190
+
191
+ exists: protectedClassMemberProcedure
192
+ .input(z.object({ id: z.string() }))
193
+ .query(({ input }) => assignmentExists(input.id)),
194
+ move: protectedTeacherProcedure
275
195
  .input(z.object({
276
196
  id: z.string(),
277
197
  classId: z.string(),
278
198
  targetSectionId: z.string().nullable().optional(),
279
199
  }))
280
- .mutation(async ({ ctx, input }) => {
281
- const { id } = input;
282
- const targetSectionId = (input.targetSectionId ?? null) || null; // normalize empty string to null
283
-
284
- const moved = await prisma.assignment.findUnique({
285
- where: { id },
286
- select: { id: true, classId: true, sectionId: true },
287
- });
288
-
289
- if (!moved) {
290
- throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
291
- }
292
-
293
- const updated = await prisma.$transaction(async (tx) => {
294
- // Update sectionId first
295
- await tx.assignment.update({ where: { id }, data: { sectionId: targetSectionId } });
296
-
297
- // The unified list ordering remains the same, just the assignment's sectionId changed
298
- // No need to reorder since we're keeping the same position in the unified list
299
- // If frontend wants to change position, they should call reorder after move
300
-
301
- return tx.assignment.findUnique({ where: { id } });
302
- });
303
-
304
- return updated;
305
- }),
306
-
307
- create: protectedProcedure
200
+ .mutation(({ ctx, input }) =>
201
+ moveAssignmentRecord(ctx.user!.id, {
202
+ id: input.id,
203
+ classId: input.classId,
204
+ targetSectionId: (input.targetSectionId ?? null) || null,
205
+ })
206
+ ),
207
+
208
+ create: protectedTeacherProcedure
308
209
  .input(createAssignmentSchema)
309
- .mutation(async ({ ctx, input }) => {
310
- const { classId, title, instructions, dueDate, files, existingFileIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
311
-
312
- if (!ctx.user) {
313
- throw new TRPCError({
314
- code: "UNAUTHORIZED",
315
- message: "User must be authenticated",
316
- });
317
- }
318
-
319
- // Get all students in the class
320
- const classData = await prisma.class.findUnique({
321
- where: { id: classId },
322
- include: {
323
- students: {
324
- select: { id: true }
325
- }
326
- }
327
- });
328
-
329
- if (!classData) {
330
- throw new TRPCError({
331
- code: "NOT_FOUND",
332
- message: "Class not found",
333
- });
334
- }
335
-
336
- let computedMaxGrade = maxGrade;
337
- if (markSchemeId) {
338
- const rubric = await prisma.markScheme.findUnique({
339
- where: { id: markSchemeId },
340
- select: {
341
- structured: true,
342
- }
343
- });
344
-
345
- const parsedRubric = JSON.parse(rubric?.structured || "{}");
346
-
347
- // Calculate max grade from rubric criteria levels
348
- computedMaxGrade = parsedRubric.criteria.reduce((acc: number, criterion: any) => {
349
- const maxPoints = Math.max(...criterion.levels.map((level: any) => level.points));
350
- return acc + maxPoints;
351
- }, 0);
352
- }
353
- console.log(markSchemeId, gradingBoundaryId);
354
-
355
- // Create assignment and place at top of its scope within a single transaction
356
- const teacherId = ctx.user!.id;
357
- const assignment = await prisma.$transaction(async (tx) => {
358
- const created = await tx.assignment.create({
359
- data: {
360
- title,
361
- instructions,
362
- dueDate: new Date(dueDate),
363
- maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
364
- graded,
365
- weight,
366
- type,
367
- order: 1,
368
- inProgress: inProgress || false,
369
- class: {
370
- connect: { id: classId }
371
- },
372
- ...(sectionId && {
373
- section: {
374
- connect: { id: sectionId }
375
- }
376
- }),
377
- ...(markSchemeId && {
378
- markScheme: {
379
- connect: { id: markSchemeId }
380
- }
381
- }),
382
- ...(gradingBoundaryId && {
383
- gradingBoundary: {
384
- connect: { id: gradingBoundaryId }
385
- }
386
- }),
387
- submissions: {
388
- create: classData.students.map((student) => ({
389
- student: {
390
- connect: { id: student.id }
391
- }
392
- }))
393
- },
394
- teacher: {
395
- connect: { id: teacherId }
396
- }
397
- },
398
- select: {
399
- id: true,
400
- title: true,
401
- instructions: true,
402
- dueDate: true,
403
- maxGrade: true,
404
- graded: true,
405
- weight: true,
406
- type: true,
407
- attachments: {
408
- select: {
409
- id: true,
410
- name: true,
411
- type: true,
412
- }
413
- },
414
- section: {
415
- select: {
416
- id: true,
417
- name: true
418
- }
419
- },
420
- teacher: {
421
- select: {
422
- id: true,
423
- username: true
424
- }
425
- },
426
- class: {
427
- select: {
428
- id: true,
429
- name: true
430
- }
431
- }
432
- }
433
- });
434
-
435
- // Insert new assignment at top of unified list and normalize
436
- const unified = await getUnifiedList(tx, classId);
437
- const withoutNew = unified.filter(item => !(item.id === created.id && item.type === 'assignment'));
438
- const reindexed = [{ id: created.id, type: 'assignment' as const }, ...withoutNew.map(item => ({ id: item.id, type: item.type }))];
439
- await normalizeUnifiedList(tx, classId, reindexed);
440
-
441
- return created;
442
- });
443
-
444
- // NOTE: Files are now handled via direct upload endpoints
445
- // The files field in the schema is for metadata only
446
- // Actual file uploads should use getAssignmentUploadUrls endpoint
447
- let uploadedFiles: UploadedFile[] = [];
448
- if (files && files.length > 0) {
449
- // Create direct upload files instead of processing base64
450
- uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id);
451
- }
452
-
453
- // Update assignment with new file attachments
454
- if (uploadedFiles.length > 0) {
455
- await prisma.assignment.update({
456
- where: { id: assignment.id },
457
- data: {
458
- attachments: {
459
- create: uploadedFiles.map(file => ({
460
- name: file.name,
461
- type: file.type,
462
- size: file.size,
463
- path: file.path,
464
- ...(file.thumbnailId && {
465
- thumbnail: {
466
- connect: { id: file.thumbnailId }
467
- }
468
- })
469
- }))
470
- }
471
- }
472
- });
473
- }
474
-
475
- // Connect existing files if provided
476
- if (existingFileIds && existingFileIds.length > 0) {
477
- await prisma.assignment.update({
478
- where: { id: assignment.id },
479
- data: {
480
- attachments: {
481
- connect: existingFileIds.map(fileId => ({ id: fileId }))
482
- }
483
- }
484
- });
485
- }
486
-
487
- sendNotifications(classData.students.map(student => student.id), {
488
- title: `🔔 New assignment for ${classData.name}`,
489
- content:
490
- `The assignment "${title}" has been created in ${classData.name}.\n
491
- Due date: ${new Date(dueDate).toLocaleDateString()}.
492
- [Link to assignment](/class/${classId}/assignments/${assignment.id})`
493
- }).catch(error => {
494
- logger.error('Failed to send assignment notifications:');
495
- });
496
-
497
- return assignment;
498
- }),
499
- update: protectedProcedure
210
+ .mutation(({ ctx, input }) => createAssignmentRecord(ctx.user!.id, input)),
211
+ update: protectedTeacherProcedure
500
212
  .input(updateAssignmentSchema)
501
- .mutation(async ({ ctx, input }) => {
502
- const { id, title, instructions, dueDate, files, existingFileIds, maxGrade, graded, weight, sectionId, type, inProgress } = input;
503
-
504
- if (!ctx.user) {
505
- throw new TRPCError({
506
- code: "UNAUTHORIZED",
507
- message: "User must be authenticated",
508
- });
509
- }
510
-
511
- // Get the assignment with current attachments
512
- const assignment = await prisma.assignment.findFirst({
513
- where: {
514
- id,
515
- teacherId: ctx.user.id,
516
- },
517
- include: {
518
- attachments: {
519
- select: {
520
- id: true,
521
- name: true,
522
- type: true,
523
- path: true,
524
- size: true,
525
- uploadStatus: true,
526
- thumbnail: {
527
- select: {
528
- path: true
529
- }
530
- }
531
- },
532
- },
533
- class: {
534
- select: {
535
- id: true,
536
- name: true
537
- }
538
- },
539
- },
540
- });
541
-
542
- if (!assignment) {
543
- throw new TRPCError({
544
- code: "NOT_FOUND",
545
- message: "Assignment not found",
546
- });
547
- }
548
-
549
- // NOTE: Files are now handled via direct upload endpoints
550
- let uploadedFiles: UploadedFile[] = [];
551
- if (files && files.length > 0) {
552
- // Create direct upload files instead of processing base64
553
- uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
554
- }
555
-
556
- // Delete removed attachments from storage before updating database
557
- if (input.removedAttachments && input.removedAttachments.length > 0) {
558
- const filesToDelete = assignment.attachments.filter((file) =>
559
- input.removedAttachments!.includes(file.id)
560
- );
561
-
562
- // Delete files from storage (only if they were actually uploaded)
563
- await Promise.all(filesToDelete.map(async (file) => {
564
- try {
565
- // Only delete from GCS if the file was successfully uploaded
566
- if (file.uploadStatus === 'COMPLETED') {
567
- // Delete the main file
568
- await deleteFile(file.path);
569
-
570
- // Delete thumbnail if it exists
571
- if (file.thumbnail?.path) {
572
- await deleteFile(file.thumbnail.path);
573
- }
574
- }
575
- } catch (error) {
576
- console.warn(`Failed to delete file ${file.path}:`, error);
577
- }
578
- }));
579
- }
580
-
581
- // Update assignment
582
- const updatedAssignment = await prisma.assignment.update({
583
- where: { id },
584
- data: {
585
- ...(title && { title }),
586
- ...(instructions && { instructions }),
587
- ...(dueDate && { dueDate: new Date(dueDate) }),
588
- ...(maxGrade && { maxGrade }),
589
- ...(graded !== undefined && { graded }),
590
- ...(weight && { weight }),
591
- ...(type && { type }),
592
- ...(inProgress !== undefined && { inProgress }),
593
- ...(sectionId !== undefined && {
594
- section: sectionId ? {
595
- connect: { id: sectionId }
596
- } : {
597
- disconnect: true
598
- }
599
- }),
600
- ...(uploadedFiles.length > 0 && {
601
- attachments: {
602
- create: uploadedFiles.map(file => ({
603
- name: file.name,
604
- type: file.type,
605
- size: file.size,
606
- path: file.path,
607
- ...(file.thumbnailId && {
608
- thumbnail: {
609
- connect: { id: file.thumbnailId }
610
- }
611
- })
612
- }))
613
- }
614
- }),
615
- ...(existingFileIds && existingFileIds.length > 0 && {
616
- attachments: {
617
- connect: existingFileIds.map(fileId => ({ id: fileId }))
618
- }
619
- }),
620
- ...(input.removedAttachments && input.removedAttachments.length > 0 && {
621
- attachments: {
622
- deleteMany: {
623
- id: { in: input.removedAttachments }
624
- }
625
- }
626
- }),
627
- },
628
- select: {
629
- id: true,
630
- title: true,
631
- instructions: true,
632
- dueDate: true,
633
- maxGrade: true,
634
- graded: true,
635
- weight: true,
636
- type: true,
637
- createdAt: true,
638
- submissions: {
639
- select: {
640
- student: {
641
- select: {
642
- id: true,
643
- username: true
644
- }
645
- }
646
- }
647
- },
648
- attachments: {
649
- select: {
650
- id: true,
651
- name: true,
652
- type: true,
653
- thumbnail: true,
654
- size: true,
655
- path: true,
656
- uploadedAt: true,
657
- thumbnailId: true,
658
- }
659
- },
660
- section: true,
661
- teacher: true,
662
- class: true
663
- }
664
- });
665
-
666
-
667
- if (assignment.markSchemeId) {
668
- const rubric = await prisma.markScheme.findUnique({
669
- where: { id: assignment.markSchemeId },
670
- select: {
671
- structured: true,
672
- }
673
- });
674
- const parsedRubric = JSON.parse(rubric?.structured || "{}");
675
- const computedMaxGrade = parsedRubric.criteria.reduce((acc: number, criterion: any) => {
676
- const maxPoints = Math.max(...criterion.levels.map((level: any) => level.points));
677
- return acc + maxPoints;
678
- }, 0);
679
-
680
- await prisma.assignment.update({
681
- where: { id },
682
- data: {
683
- maxGrade: computedMaxGrade,
684
- }
685
- });
686
- }
687
-
688
-
689
- return updatedAssignment;
690
- }),
213
+ .mutation(({ ctx, input }) => updateAssignmentRecord(ctx.user!.id, input)),
691
214
 
692
215
  delete: protectedProcedure
693
216
  .input(deleteAssignmentSchema)
694
- .mutation(async ({ ctx, input }) => {
695
- const { id, classId } = input;
696
-
697
- if (!ctx.user) {
698
- throw new TRPCError({
699
- code: "UNAUTHORIZED",
700
- message: "User must be authenticated",
701
- });
702
- }
703
-
704
- // Get the assignment with all related files
705
- const assignment = await prisma.assignment.findFirst({
706
- where: {
707
- id,
708
- teacherId: ctx.user.id,
709
- },
710
- include: {
711
- attachments: {
712
- include: {
713
- thumbnail: true
714
- }
715
- },
716
- submissions: {
717
- include: {
718
- attachments: {
719
- include: {
720
- thumbnail: true
721
- }
722
- },
723
- annotations: {
724
- include: {
725
- thumbnail: true
726
- }
727
- }
728
- }
729
- }
730
- }
731
- });
217
+ .mutation(({ ctx, input }) =>
218
+ deleteAssignmentRecord(ctx.user!.id, input.id, input.classId)
219
+ ),
732
220
 
733
- if (!assignment) {
734
- throw new TRPCError({
735
- code: "NOT_FOUND",
736
- message: "Assignment not found",
737
- });
738
- }
221
+ get: protectedClassMemberProcedure
222
+ .input(getAssignmentSchema)
223
+ .query(({ input }) => getAssignment(input.id, input.classId)),
739
224
 
740
- // Delete all files from storage
741
- const filesToDelete = [
742
- ...assignment.attachments,
743
- ...assignment.submissions.flatMap(sub => [...sub.attachments, ...sub.annotations])
744
- ];
745
-
746
- // Delete files from storage (only if they were actually uploaded)
747
- await Promise.all(filesToDelete.map(async (file) => {
748
- try {
749
- // Only delete from GCS if the file was successfully uploaded
750
- if (file.uploadStatus === 'COMPLETED') {
751
- // Delete the main file
752
- await deleteFile(file.path);
753
-
754
- // Delete thumbnail if it exists
755
- if (file.thumbnail) {
756
- await deleteFile(file.thumbnail.path);
757
- }
758
- }
759
- } catch (error) {
760
- console.warn(`Failed to delete file ${file.path}:`, error);
761
- }
762
- }));
763
-
764
- // Delete the assignment (this will cascade delete all related records)
765
- await prisma.assignment.delete({
766
- where: { id },
767
- });
225
+ getSubmission: protectedClassMemberProcedure
226
+ .input(z.object({ assignmentId: z.string(), classId: z.string() }))
227
+ .query(({ ctx, input }) =>
228
+ getSubmission(input.assignmentId, ctx.user!.id)
229
+ ),
768
230
 
769
- return {
770
- id,
771
- };
772
- }),
231
+ getSubmissionById: protectedClassMemberProcedure
232
+ .input(z.object({ classId: z.string(), submissionId: z.string() }))
233
+ .query(({ ctx, input }) =>
234
+ getSubmissionById(input.submissionId, input.classId, ctx.user!.id)
235
+ ),
773
236
 
774
- get: protectedProcedure
775
- .input(getAssignmentSchema)
776
- .query(async ({ ctx, input }) => {
777
- const { id, classId } = input;
237
+ updateSubmission: protectedClassMemberProcedure
238
+ .input(submissionSchema)
239
+ .mutation(({ ctx, input }) =>
240
+ updateSubmissionRecord(ctx.user!.id, {
241
+ submissionId: input.submissionId,
242
+ assignmentId: input.assignmentId,
243
+ classId: input.classId,
244
+ submit: input.submit,
245
+ newAttachments: input.newAttachments,
246
+ extendedResponse: input.extendedResponse,
247
+ existingFileIds: input.existingFileIds,
248
+ removedAttachments: input.removedAttachments,
249
+ })
250
+ ),
778
251
 
779
- if (!ctx.user) {
780
- throw new TRPCError({
781
- code: "UNAUTHORIZED",
782
- message: "User must be authenticated",
783
- });
784
- }
252
+ getSubmissions: protectedTeacherProcedure
253
+ .input(z.object({ assignmentId: z.string(), classId: z.string() }))
254
+ .query(({ ctx, input }) => getSubmissions(input.assignmentId, ctx.user!.id)),
785
255
 
786
- const assignment = await prisma.assignment.findUnique({
787
- where: {
788
- id,
789
- // classId,
790
- },
791
- include: {
792
- submissions: {
793
- select: {
794
- student: {
795
- select: {
796
- id: true,
797
- username: true
798
- }
799
- }
800
- }
801
- },
802
- attachments: {
803
- select: {
804
- id: true,
805
- name: true,
806
- type: true,
807
- size: true,
808
- path: true,
809
- uploadedAt: true,
810
- thumbnailId: true,
811
- }
812
- },
813
- section: {
814
- select: {
815
- id: true,
816
- name: true,
817
- }
818
- },
819
- teacher: {
820
- select: {
821
- id: true,
822
- username: true
823
- }
824
- },
825
- class: {
826
- select: {
827
- id: true,
828
- name: true
829
- }
830
- },
831
- eventAttached: {
832
- select: {
833
- id: true,
834
- name: true,
835
- startTime: true,
836
- endTime: true,
837
- location: true,
838
- remarks: true,
839
- }
840
- },
841
- markScheme: {
842
- select: {
843
- id: true,
844
- structured: true,
845
- }
846
- },
847
- gradingBoundary: {
848
- select: {
849
- id: true,
850
- structured: true,
851
- }
852
- }
853
- }
854
- });
256
+ updateSubmissionAsTeacher: protectedTeacherProcedure
257
+ .input(updateSubmissionSchema)
258
+ .mutation(({ ctx, input }) =>
259
+ updateSubmissionAsTeacherRecord(ctx.user!.id, {
260
+ submissionId: input.submissionId,
261
+ assignmentId: input.assignmentId,
262
+ classId: input.classId,
263
+ return: input.return,
264
+ gradeReceived: input.gradeReceived,
265
+ newAttachments: input.newAttachments,
266
+ existingFileIds: input.existingFileIds,
267
+ removedAttachments: input.removedAttachments,
268
+ rubricGrades: input.rubricGrades,
269
+ feedback: input.feedback,
270
+ })
271
+ ),
855
272
 
856
- if (!assignment) {
857
- throw new TRPCError({
858
- code: "NOT_FOUND",
859
- message: "Assignment not found",
860
- });
861
- }
273
+ attachToEvent: protectedTeacherProcedure
274
+ .input(z.object({ assignmentId: z.string(), eventId: z.string() }))
275
+ .mutation(({ ctx, input }) =>
276
+ attachAssignmentToEventRecord(ctx.user!.id, input.assignmentId, input.eventId)
277
+ ),
862
278
 
863
- const sections = await prisma.section.findMany({
864
- where: {
865
- classId: assignment.classId,
866
- },
867
- select: {
868
- id: true,
869
- name: true,
870
- },
871
- });
279
+ detachEvent: protectedTeacherProcedure
280
+ .input(z.object({ assignmentId: z.string() }))
281
+ .mutation(({ ctx, input }) =>
282
+ detachAssignmentFromEventRecord(ctx.user!.id, input.assignmentId)
283
+ ),
872
284
 
873
- return { ...assignment, sections };
874
- }),
285
+ getAvailableEvents: protectedTeacherProcedure
286
+ .input(z.object({ assignmentId: z.string() }))
287
+ .query(({ ctx, input }) =>
288
+ getAvailableEventsForAssignment(ctx.user!.id, input.assignmentId)
289
+ ),
875
290
 
876
- getSubmission: protectedClassMemberProcedure
291
+ dueToday: protectedProcedure
292
+ .query(() => getDueToday()),
293
+ attachMarkScheme: protectedTeacherProcedure
877
294
  .input(z.object({
878
295
  assignmentId: z.string(),
879
- classId: z.string(),
296
+ markSchemeId: z.string().nullable(),
880
297
  }))
881
- .query(async ({ ctx, input }) => {
882
- if (!ctx.user) {
883
- throw new TRPCError({
884
- code: "UNAUTHORIZED",
885
- message: "User must be authenticated",
886
- });
887
- }
888
-
889
- const { assignmentId } = input;
890
-
891
- const submission = await prisma.submission.findFirst({
892
- where: {
893
- assignmentId,
894
- studentId: ctx.user.id,
895
- },
896
- include: {
897
- attachments: true,
898
- student: {
899
- select: {
900
- id: true,
901
- username: true,
902
- profile: true,
903
- },
904
- },
905
- assignment: {
906
- include: {
907
- class: true,
908
- markScheme: {
909
- select: {
910
- id: true,
911
- structured: true,
912
- }
913
- },
914
- gradingBoundary: {
915
- select: {
916
- id: true,
917
- structured: true,
918
- }
919
- }
920
- },
921
- },
922
- annotations: true,
923
- },
924
- });
925
-
926
- if (!submission) {
927
- // Create a new submission if it doesn't exist
928
- return await prisma.submission.create({
929
- data: {
930
- assignment: {
931
- connect: { id: assignmentId },
932
- },
933
- student: {
934
- connect: { id: ctx.user.id },
935
- },
936
- },
937
- include: {
938
- attachments: true,
939
- annotations: true,
940
- student: {
941
- select: {
942
- id: true,
943
- username: true,
944
- },
945
- },
946
- assignment: {
947
- include: {
948
- class: true,
949
- markScheme: {
950
- select: {
951
- id: true,
952
- structured: true,
953
- }
954
- },
955
- gradingBoundary: {
956
- select: {
957
- id: true,
958
- structured: true,
959
- }
960
- }
961
- },
962
- },
963
- },
964
- });
965
- }
966
-
967
- return {
968
- ...submission,
969
- late: submission.assignment.dueDate < new Date(),
970
- };
971
- }),
972
-
973
- getSubmissionById: protectedTeacherProcedure
974
- .input(z.object({
975
- submissionId: z.string(),
976
- classId: z.string(),
977
- }))
978
- .query(async ({ ctx, input }) => {
979
- if (!ctx.user) {
980
- throw new TRPCError({
981
- code: "UNAUTHORIZED",
982
- message: "User must be authenticated",
983
- });
984
- }
985
-
986
- const { submissionId, classId } = input;
987
-
988
- const submission = await prisma.submission.findFirst({
989
- where: {
990
- id: submissionId,
991
- assignment: {
992
- classId,
993
- class: {
994
- teachers: {
995
- some: {
996
- id: ctx.user.id
997
- }
998
- }
999
- }
1000
- },
1001
- },
1002
- include: {
1003
- attachments: true,
1004
- annotations: true,
1005
- student: {
1006
- select: {
1007
- id: true,
1008
- username: true,
1009
- profile: true,
1010
- },
1011
- },
1012
- assignment: {
1013
- include: {
1014
- class: true,
1015
- markScheme: {
1016
- select: {
1017
- id: true,
1018
- structured: true,
1019
- }
1020
- },
1021
- gradingBoundary: {
1022
- select: {
1023
- id: true,
1024
- structured: true,
1025
- }
1026
- }
1027
- },
1028
- },
1029
- },
1030
- });
1031
-
1032
- if (!submission) {
1033
- throw new TRPCError({
1034
- code: "NOT_FOUND",
1035
- message: "Submission not found",
1036
- });
1037
- }
1038
-
1039
- return {
1040
- ...submission,
1041
- late: submission.assignment.dueDate < new Date(),
1042
- };
1043
- }),
1044
-
1045
- updateSubmission: protectedClassMemberProcedure
1046
- .input(submissionSchema)
1047
- .mutation(async ({ ctx, input }) => {
1048
- if (!ctx.user) {
1049
- throw new TRPCError({
1050
- code: "UNAUTHORIZED",
1051
- message: "User must be authenticated",
1052
- });
1053
- }
1054
-
1055
- const { submissionId, submit, newAttachments, existingFileIds, removedAttachments } = input;
1056
-
1057
- const submission = await prisma.submission.findFirst({
1058
- where: {
1059
- id: submissionId,
1060
- OR: [
1061
- {
1062
- student: {
1063
- id: ctx.user.id,
1064
- },
1065
- },
1066
- {
1067
- assignment: {
1068
- class: {
1069
- teachers: {
1070
- some: {
1071
- id: ctx.user.id,
1072
- },
1073
- },
1074
- },
1075
- },
1076
- },
1077
- ],
1078
- },
1079
- include: {
1080
- attachments: {
1081
- include: {
1082
- thumbnail: true
1083
- }
1084
- },
1085
- assignment: {
1086
- include: {
1087
- class: true,
1088
- markScheme: {
1089
- select: {
1090
- id: true,
1091
- structured: true,
1092
- }
1093
- },
1094
- gradingBoundary: {
1095
- select: {
1096
- id: true,
1097
- structured: true,
1098
- }
1099
- }
1100
- },
1101
- },
1102
- },
1103
- });
1104
-
1105
- if (!submission) {
1106
- throw new TRPCError({
1107
- code: "NOT_FOUND",
1108
- message: "Submission not found",
1109
- });
1110
- }
1111
-
1112
- if (submit !== undefined) {
1113
- // Toggle submission status
1114
- return await prisma.submission.update({
1115
- where: { id: submission.id },
1116
- data: {
1117
- submitted: !submission.submitted,
1118
- submittedAt: new Date(),
1119
- },
1120
- include: {
1121
- attachments: true,
1122
- student: {
1123
- select: {
1124
- id: true,
1125
- username: true,
1126
- },
1127
- },
1128
- assignment: {
1129
- include: {
1130
- class: true,
1131
- markScheme: {
1132
- select: {
1133
- id: true,
1134
- structured: true,
1135
- }
1136
- },
1137
- gradingBoundary: {
1138
- select: {
1139
- id: true,
1140
- structured: true,
1141
- }
1142
- }
1143
- },
1144
- },
1145
- },
1146
- });
1147
- }
1148
-
1149
- let uploadedFiles: UploadedFile[] = [];
1150
- if (newAttachments && newAttachments.length > 0) {
1151
- // Store files in a class and assignment specific directory
1152
- uploadedFiles = await createDirectUploadFiles(newAttachments, ctx.user.id, undefined, undefined, submission.id);
1153
- }
1154
-
1155
- // Update submission with new file attachments
1156
- if (uploadedFiles.length > 0) {
1157
- await prisma.submission.update({
1158
- where: { id: submission.id },
1159
- data: {
1160
- attachments: {
1161
- create: uploadedFiles.map(file => ({
1162
- name: file.name,
1163
- type: file.type,
1164
- size: file.size,
1165
- path: file.path,
1166
- ...(file.thumbnailId && {
1167
- thumbnail: {
1168
- connect: { id: file.thumbnailId }
1169
- }
1170
- })
1171
- }))
1172
- }
1173
- }
1174
- });
1175
- }
1176
-
1177
- // Connect existing files if provided
1178
- if (existingFileIds && existingFileIds.length > 0) {
1179
- await prisma.submission.update({
1180
- where: { id: submission.id },
1181
- data: {
1182
- attachments: {
1183
- connect: existingFileIds.map(fileId => ({ id: fileId }))
1184
- }
1185
- }
1186
- });
1187
- }
1188
-
1189
- // Delete removed attachments if any
1190
- if (removedAttachments && removedAttachments.length > 0) {
1191
- const filesToDelete = submission.attachments.filter((file) =>
1192
- removedAttachments.includes(file.id)
1193
- );
1194
-
1195
- // Delete files from storage (only if they were actually uploaded)
1196
- await Promise.all(filesToDelete.map(async (file) => {
1197
- try {
1198
- // Only delete from GCS if the file was successfully uploaded
1199
- if (file.uploadStatus === 'COMPLETED') {
1200
- // Delete the main file
1201
- await deleteFile(file.path);
1202
-
1203
- // Delete thumbnail if it exists
1204
- if (file.thumbnail?.path) {
1205
- await deleteFile(file.thumbnail.path);
1206
- }
1207
- }
1208
- } catch (error) {
1209
- console.warn(`Failed to delete file ${file.path}:`, error);
1210
- }
1211
- }));
1212
- }
1213
-
1214
- // Update submission with attachments
1215
- return await prisma.submission.update({
1216
- where: { id: submission.id },
1217
- data: {
1218
- ...(removedAttachments && removedAttachments.length > 0 && {
1219
- attachments: {
1220
- deleteMany: {
1221
- id: { in: removedAttachments },
1222
- },
1223
- },
1224
- }),
1225
- },
1226
- include: {
1227
- attachments: {
1228
- include: {
1229
- thumbnail: true
1230
- }
1231
- },
1232
- student: {
1233
- select: {
1234
- id: true,
1235
- username: true,
1236
- },
1237
- },
1238
- assignment: {
1239
- include: {
1240
- class: true,
1241
- markScheme: {
1242
- select: {
1243
- id: true,
1244
- structured: true,
1245
- }
1246
- },
1247
- gradingBoundary: {
1248
- select: {
1249
- id: true,
1250
- structured: true,
1251
- }
1252
- }
1253
- },
1254
- },
1255
- },
1256
- });
1257
- }),
1258
-
1259
- getSubmissions: protectedTeacherProcedure
1260
- .input(z.object({
1261
- assignmentId: z.string(),
1262
- classId: z.string(),
1263
- }))
1264
- .query(async ({ ctx, input }) => {
1265
- if (!ctx.user) {
1266
- throw new TRPCError({
1267
- code: "UNAUTHORIZED",
1268
- message: "User must be authenticated",
1269
- });
1270
- }
1271
-
1272
- const { assignmentId } = input;
1273
-
1274
- const submissions = await prisma.submission.findMany({
1275
- where: {
1276
- assignment: {
1277
- id: assignmentId,
1278
- class: {
1279
- teachers: {
1280
- some: { id: ctx.user.id },
1281
- },
1282
- },
1283
- },
1284
- },
1285
- include: {
1286
- attachments: {
1287
- include: {
1288
- thumbnail: true
1289
- }
1290
- },
1291
- student: {
1292
- select: {
1293
- id: true,
1294
- username: true,
1295
- profile: {
1296
- select: {
1297
- displayName: true,
1298
- profilePicture: true,
1299
- profilePictureThumbnail: true,
1300
- },
1301
- },
1302
- },
1303
- },
1304
- assignment: {
1305
- include: {
1306
- class: true,
1307
- markScheme: {
1308
- select: {
1309
- id: true,
1310
- structured: true,
1311
- }
1312
- },
1313
- gradingBoundary: {
1314
- select: {
1315
- id: true,
1316
- structured: true,
1317
- }
1318
- }
1319
- },
1320
- },
1321
- },
1322
- });
1323
-
1324
- return submissions.map(submission => ({
1325
- ...submission,
1326
- late: submission.assignment.dueDate < new Date(),
1327
- }));
1328
- }),
1329
-
1330
- updateSubmissionAsTeacher: protectedTeacherProcedure
1331
- .input(updateSubmissionSchema)
1332
- .mutation(async ({ ctx, input }) => {
1333
- if (!ctx.user) {
1334
- throw new TRPCError({
1335
- code: "UNAUTHORIZED",
1336
- message: "User must be authenticated",
1337
- });
1338
- }
1339
-
1340
- const { submissionId, return: returnSubmission, gradeReceived, newAttachments, existingFileIds, removedAttachments, rubricGrades, feedback } = input;
1341
-
1342
- const submission = await prisma.submission.findFirst({
1343
- where: {
1344
- id: submissionId,
1345
- assignment: {
1346
- class: {
1347
- teachers: {
1348
- some: { id: ctx.user.id },
1349
- },
1350
- },
1351
- },
1352
- },
1353
- include: {
1354
- attachments: {
1355
- include: {
1356
- thumbnail: true
1357
- }
1358
- },
1359
- annotations: {
1360
- include: {
1361
- thumbnail: true
1362
- }
1363
- },
1364
- assignment: {
1365
- include: {
1366
- class: true,
1367
- markScheme: {
1368
- select: {
1369
- id: true,
1370
- structured: true,
1371
- }
1372
- },
1373
- gradingBoundary: {
1374
- select: {
1375
- id: true,
1376
- structured: true,
1377
- }
1378
- }
1379
- },
1380
- },
1381
- },
1382
- });
1383
-
1384
- if (!submission) {
1385
- throw new TRPCError({
1386
- code: "NOT_FOUND",
1387
- message: "Submission not found",
1388
- });
1389
- }
1390
-
1391
- if (returnSubmission !== undefined) {
1392
- // Toggle return status
1393
- return await prisma.submission.update({
1394
- where: { id: submissionId },
1395
- data: {
1396
- returned: !submission.returned,
1397
- },
1398
- include: {
1399
- attachments: true,
1400
- student: {
1401
- select: {
1402
- id: true,
1403
- username: true,
1404
- profile: {
1405
- select: {
1406
- displayName: true,
1407
- profilePicture: true,
1408
- profilePictureThumbnail: true,
1409
- },
1410
- },
1411
- },
1412
- },
1413
- assignment: {
1414
- include: {
1415
- class: true,
1416
- markScheme: {
1417
- select: {
1418
- id: true,
1419
- structured: true,
1420
- }
1421
- },
1422
- gradingBoundary: {
1423
- select: {
1424
- id: true,
1425
- structured: true,
1426
- }
1427
- }
1428
- },
1429
- },
1430
- },
1431
- });
1432
- }
1433
-
1434
- // NOTE: Teacher annotation files are now handled via direct upload endpoints
1435
- // Use getAnnotationUploadUrls and confirmAnnotationUpload endpoints instead
1436
- // The newAttachments field is deprecated for annotations
1437
- if (newAttachments && newAttachments.length > 0) {
1438
- throw new TRPCError({
1439
- code: "BAD_REQUEST",
1440
- message: "Direct file upload is deprecated. Use getAnnotationUploadUrls endpoint instead.",
1441
- });
1442
- }
1443
-
1444
- // Connect existing files if provided
1445
- if (existingFileIds && existingFileIds.length > 0) {
1446
- await prisma.submission.update({
1447
- where: { id: submission.id },
1448
- data: {
1449
- annotations: {
1450
- connect: existingFileIds.map(fileId => ({ id: fileId }))
1451
- }
1452
- }
1453
- });
1454
- }
1455
-
1456
- // Delete removed attachments if any
1457
- if (removedAttachments && removedAttachments.length > 0) {
1458
- const filesToDelete = submission.annotations.filter((file) =>
1459
- removedAttachments.includes(file.id)
1460
- );
1461
-
1462
- // Delete files from storage (only if they were actually uploaded)
1463
- await Promise.all(filesToDelete.map(async (file) => {
1464
- try {
1465
- // Only delete from GCS if the file was successfully uploaded
1466
- if (file.uploadStatus === 'COMPLETED') {
1467
- // Delete the main file
1468
- await deleteFile(file.path);
1469
-
1470
- // Delete thumbnail if it exists
1471
- if (file.thumbnail?.path) {
1472
- await deleteFile(file.thumbnail.path);
1473
- }
1474
- }
1475
- } catch (error) {
1476
- console.warn(`Failed to delete file ${file.path}:`, error);
1477
- }
1478
- }));
1479
- }
1480
-
1481
- // Update submission with grade and attachments
1482
- return await prisma.submission.update({
1483
- where: { id: submissionId },
1484
- data: {
1485
- ...(gradeReceived !== undefined && { gradeReceived }),
1486
- ...(rubricGrades && { rubricState: JSON.stringify(rubricGrades) }),
1487
- ...(feedback && { teacherComments: feedback }),
1488
- ...(removedAttachments && removedAttachments.length > 0 && {
1489
- annotations: {
1490
- deleteMany: {
1491
- id: { in: removedAttachments },
1492
- },
1493
- },
1494
- }),
1495
- ...(returnSubmission as unknown as boolean && { returned: returnSubmission }),
1496
- },
1497
- include: {
1498
- attachments: {
1499
- include: {
1500
- thumbnail: true
1501
- }
1502
- },
1503
- annotations: {
1504
- include: {
1505
- thumbnail: true
1506
- }
1507
- },
1508
- student: {
1509
- select: {
1510
- id: true,
1511
- username: true,
1512
- profile: {
1513
- select: {
1514
- displayName: true,
1515
- profilePicture: true,
1516
- profilePictureThumbnail: true,
1517
- },
1518
- },
1519
- },
1520
- },
1521
- assignment: {
1522
- include: {
1523
- class: true,
1524
- markScheme: {
1525
- select: {
1526
- id: true,
1527
- structured: true,
1528
- }
1529
- },
1530
- gradingBoundary: {
1531
- select: {
1532
- id: true,
1533
- structured: true,
1534
- }
1535
- }
1536
- },
1537
- },
1538
- },
1539
- });
1540
- }),
1541
-
1542
- attachToEvent: protectedTeacherProcedure
1543
- .input(z.object({
1544
- assignmentId: z.string(),
1545
- eventId: z.string(),
1546
- }))
1547
- .mutation(async ({ ctx, input }) => {
1548
- if (!ctx.user) {
1549
- throw new TRPCError({
1550
- code: "UNAUTHORIZED",
1551
- message: "User must be authenticated",
1552
- });
1553
- }
1554
-
1555
- const { assignmentId, eventId } = input;
1556
-
1557
- // Check if assignment exists and user is a teacher of the class
1558
- const assignment = await prisma.assignment.findFirst({
1559
- where: {
1560
- id: assignmentId,
1561
- class: {
1562
- teachers: {
1563
- some: { id: ctx.user.id },
1564
- },
1565
- },
1566
- },
1567
- include: {
1568
- class: true,
1569
- },
1570
- });
1571
-
1572
- if (!assignment) {
1573
- throw new TRPCError({
1574
- code: "NOT_FOUND",
1575
- message: "Assignment not found or you are not authorized",
1576
- });
1577
- }
1578
-
1579
- // Check if event exists and belongs to the same class
1580
- const event = await prisma.event.findFirst({
1581
- where: {
1582
- id: eventId,
1583
- classId: assignment.classId,
1584
- },
1585
- });
1586
-
1587
- if (!event) {
1588
- throw new TRPCError({
1589
- code: "NOT_FOUND",
1590
- message: "Event not found or does not belong to the same class",
1591
- });
1592
- }
1593
-
1594
- // Attach assignment to event
1595
- const updatedAssignment = await prisma.assignment.update({
1596
- where: { id: assignmentId },
1597
- data: {
1598
- eventAttached: {
1599
- connect: { id: eventId }
1600
- }
1601
- },
1602
- include: {
1603
- attachments: {
1604
- select: {
1605
- id: true,
1606
- name: true,
1607
- type: true,
1608
- }
1609
- },
1610
- section: {
1611
- select: {
1612
- id: true,
1613
- name: true
1614
- }
1615
- },
1616
- teacher: {
1617
- select: {
1618
- id: true,
1619
- username: true
1620
- }
1621
- },
1622
- eventAttached: {
1623
- select: {
1624
- id: true,
1625
- name: true,
1626
- startTime: true,
1627
- endTime: true,
1628
- }
1629
- }
1630
- }
1631
- });
1632
-
1633
- return { assignment: updatedAssignment };
1634
- }),
1635
-
1636
- detachEvent: protectedTeacherProcedure
1637
- .input(z.object({
1638
- assignmentId: z.string(),
1639
- }))
1640
- .mutation(async ({ ctx, input }) => {
1641
- if (!ctx.user) {
1642
- throw new TRPCError({
1643
- code: "UNAUTHORIZED",
1644
- message: "User must be authenticated",
1645
- });
1646
- }
1647
-
1648
- const { assignmentId } = input;
1649
-
1650
- // Check if assignment exists and user is a teacher of the class
1651
- const assignment = await prisma.assignment.findFirst({
1652
- where: {
1653
- id: assignmentId,
1654
- class: {
1655
- teachers: {
1656
- some: { id: ctx.user.id },
1657
- },
1658
- },
1659
- },
1660
- });
1661
-
1662
- if (!assignment) {
1663
- throw new TRPCError({
1664
- code: "NOT_FOUND",
1665
- message: "Assignment not found or you are not authorized",
1666
- });
1667
- }
1668
-
1669
- // Detach assignment from event
1670
- const updatedAssignment = await prisma.assignment.update({
1671
- where: { id: assignmentId },
1672
- data: {
1673
- eventAttached: {
1674
- disconnect: true
1675
- }
1676
- },
1677
- include: {
1678
- attachments: {
1679
- select: {
1680
- id: true,
1681
- name: true,
1682
- type: true,
1683
- }
1684
- },
1685
- section: {
1686
- select: {
1687
- id: true,
1688
- name: true
1689
- }
1690
- },
1691
- teacher: {
1692
- select: {
1693
- id: true,
1694
- username: true
1695
- }
1696
- },
1697
- eventAttached: {
1698
- select: {
1699
- id: true,
1700
- name: true,
1701
- startTime: true,
1702
- endTime: true,
1703
- }
1704
- }
1705
- }
1706
- });
1707
-
1708
- return { assignment: updatedAssignment };
1709
- }),
1710
-
1711
- getAvailableEvents: protectedTeacherProcedure
1712
- .input(z.object({
1713
- assignmentId: z.string(),
1714
- }))
1715
- .query(async ({ ctx, input }) => {
1716
- if (!ctx.user) {
1717
- throw new TRPCError({
1718
- code: "UNAUTHORIZED",
1719
- message: "User must be authenticated",
1720
- });
1721
- }
1722
-
1723
- const { assignmentId } = input;
1724
-
1725
- // Get the assignment to find the class
1726
- const assignment = await prisma.assignment.findFirst({
1727
- where: {
1728
- id: assignmentId,
1729
- class: {
1730
- teachers: {
1731
- some: { id: ctx.user.id },
1732
- },
1733
- },
1734
- },
1735
- select: { classId: true }
1736
- });
1737
-
1738
- if (!assignment) {
1739
- throw new TRPCError({
1740
- code: "NOT_FOUND",
1741
- message: "Assignment not found or you are not authorized",
1742
- });
1743
- }
1744
-
1745
- // Get all events for the class that don't already have this assignment attached
1746
- const events = await prisma.event.findMany({
1747
- where: {
1748
- classId: assignment.classId,
1749
- assignmentsAttached: {
1750
- none: {
1751
- id: assignmentId
1752
- }
1753
- }
1754
- },
1755
- select: {
1756
- id: true,
1757
- name: true,
1758
- startTime: true,
1759
- endTime: true,
1760
- location: true,
1761
- remarks: true,
1762
- },
1763
- orderBy: {
1764
- startTime: 'asc'
1765
- }
1766
- });
1767
-
1768
- return { events };
1769
- }),
1770
-
1771
- dueToday: protectedProcedure
1772
- .query(async ({ ctx }) => {
1773
- if (!ctx.user) {
1774
- throw new TRPCError({
1775
- code: "UNAUTHORIZED",
1776
- message: "User must be authenticated",
1777
- });
1778
- }
1779
-
1780
- const assignments = await prisma.assignment.findMany({
1781
- where: {
1782
- dueDate: {
1783
- equals: new Date(),
1784
- },
1785
- },
1786
- select: {
1787
- id: true,
1788
- title: true,
1789
- dueDate: true,
1790
- type: true,
1791
- graded: true,
1792
- maxGrade: true,
1793
- class: {
1794
- select: {
1795
- id: true,
1796
- name: true,
1797
- }
1798
- }
1799
- }
1800
- });
1801
-
1802
- return assignments.map(assignment => ({
1803
- ...assignment,
1804
- dueDate: assignment.dueDate.toISOString(),
1805
- }));
1806
- }),
1807
- attachMarkScheme: protectedTeacherProcedure
1808
- .input(z.object({
1809
- assignmentId: z.string(),
1810
- markSchemeId: z.string().nullable(),
1811
- }))
1812
- .mutation(async ({ ctx, input }) => {
1813
- const { assignmentId, markSchemeId } = input;
1814
-
1815
- const assignment = await prisma.assignment.findFirst({
1816
- where: {
1817
- id: assignmentId,
1818
- },
1819
- });
1820
-
1821
- if (!assignment) {
1822
- throw new TRPCError({
1823
- code: "NOT_FOUND",
1824
- message: "Assignment not found",
1825
- });
1826
- }
1827
-
1828
- // If markSchemeId is provided, verify it exists
1829
- if (markSchemeId) {
1830
- const markScheme = await prisma.markScheme.findFirst({
1831
- where: {
1832
- id: markSchemeId,
1833
- },
1834
- });
1835
-
1836
- if (!markScheme) {
1837
- throw new TRPCError({
1838
- code: "NOT_FOUND",
1839
- message: "Mark scheme not found",
1840
- });
1841
- }
1842
- }
1843
-
1844
- const updatedAssignment = await prisma.assignment.update({
1845
- where: { id: assignmentId },
1846
- data: {
1847
- markScheme: markSchemeId ? {
1848
- connect: { id: markSchemeId },
1849
- } : {
1850
- disconnect: true,
1851
- },
1852
- },
1853
- include: {
1854
- attachments: true,
1855
- section: true,
1856
- teacher: true,
1857
- eventAttached: true,
1858
- markScheme: true,
1859
- },
1860
- });
1861
-
1862
- return updatedAssignment;
1863
- }),
298
+ .mutation(({ ctx, input }) =>
299
+ attachMarkSchemeRecord(ctx.user!.id, input.assignmentId, input.markSchemeId)
300
+ ),
1864
301
  detachMarkScheme: protectedTeacherProcedure
1865
- .input(z.object({
1866
- assignmentId: z.string(),
1867
- }))
1868
- .mutation(async ({ ctx, input }) => {
1869
- const { assignmentId } = input;
1870
-
1871
- const assignment = await prisma.assignment.findFirst({
1872
- where: {
1873
- id: assignmentId,
1874
- },
1875
- });
1876
-
1877
- if (!assignment) {
1878
- throw new TRPCError({
1879
- code: "NOT_FOUND",
1880
- message: "Assignment not found",
1881
- });
1882
- }
1883
-
1884
- const updatedAssignment = await prisma.assignment.update({
1885
- where: { id: assignmentId },
1886
- data: {
1887
- markScheme: {
1888
- disconnect: true,
1889
- },
1890
- },
1891
- include: {
1892
- attachments: true,
1893
- section: true,
1894
- teacher: true,
1895
- eventAttached: true,
1896
- markScheme: true,
1897
- },
1898
- });
1899
-
1900
- return updatedAssignment;
1901
- }),
302
+ .input(z.object({ assignmentId: z.string() }))
303
+ .mutation(({ ctx, input }) =>
304
+ detachMarkSchemeRecord(ctx.user!.id, input.assignmentId)
305
+ ),
1902
306
  attachGradingBoundary: protectedTeacherProcedure
1903
307
  .input(z.object({
1904
308
  assignmentId: z.string(),
1905
309
  gradingBoundaryId: z.string().nullable(),
1906
310
  }))
1907
- .mutation(async ({ ctx, input }) => {
1908
- const { assignmentId, gradingBoundaryId } = input;
1909
-
1910
- const assignment = await prisma.assignment.findFirst({
1911
- where: {
1912
- id: assignmentId,
1913
- },
1914
- });
1915
-
1916
- if (!assignment) {
1917
- throw new TRPCError({
1918
- code: "NOT_FOUND",
1919
- message: "Assignment not found",
1920
- });
1921
- }
1922
-
1923
- // If gradingBoundaryId is provided, verify it exists
1924
- if (gradingBoundaryId) {
1925
- const gradingBoundary = await prisma.gradingBoundary.findFirst({
1926
- where: {
1927
- id: gradingBoundaryId,
1928
- },
1929
- });
1930
-
1931
- if (!gradingBoundary) {
1932
- throw new TRPCError({
1933
- code: "NOT_FOUND",
1934
- message: "Grading boundary not found",
1935
- });
1936
- }
1937
- }
1938
-
1939
- const updatedAssignment = await prisma.assignment.update({
1940
- where: { id: assignmentId },
1941
- data: {
1942
- gradingBoundary: gradingBoundaryId ? {
1943
- connect: { id: gradingBoundaryId },
1944
- } : {
1945
- disconnect: true,
1946
- },
1947
- },
1948
- include: {
1949
- attachments: true,
1950
- section: true,
1951
- teacher: true,
1952
- eventAttached: true,
1953
- gradingBoundary: true,
1954
- },
1955
- });
1956
-
1957
- return updatedAssignment;
1958
- }),
311
+ .mutation(({ ctx, input }) =>
312
+ attachGradingBoundaryRecord(ctx.user!.id, input.assignmentId, input.gradingBoundaryId)
313
+ ),
1959
314
  detachGradingBoundary: protectedTeacherProcedure
1960
- .input(z.object({
1961
- classId: z.string(),
1962
- assignmentId: z.string(),
1963
- }))
1964
- .mutation(async ({ ctx, input }) => {
1965
- const { assignmentId } = input;
1966
-
1967
- const assignment = await prisma.assignment.findFirst({
1968
- where: {
1969
- id: assignmentId,
1970
- },
1971
- });
1972
-
1973
- if (!assignment) {
1974
- throw new TRPCError({
1975
- code: "NOT_FOUND",
1976
- message: "Assignment not found",
1977
- });
1978
- }
1979
-
1980
- const updatedAssignment = await prisma.assignment.update({
1981
- where: { id: assignmentId },
1982
- data: {
1983
- gradingBoundary: {
1984
- disconnect: true,
1985
- },
1986
- },
1987
- include: {
1988
- attachments: true,
1989
- section: true,
1990
- teacher: true,
1991
- eventAttached: true,
1992
- gradingBoundary: true,
1993
- },
1994
- });
1995
-
1996
- return updatedAssignment;
1997
- }),
315
+ .input(z.object({ classId: z.string(), assignmentId: z.string() }))
316
+ .mutation(({ ctx, input }) =>
317
+ detachGradingBoundaryRecord(ctx.user!.id, input.assignmentId)
318
+ ),
1998
319
 
1999
320
  // New direct upload endpoints
2000
321
  getAssignmentUploadUrls: protectedTeacherProcedure