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