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