@studious-lms/server 1.0.6 → 1.0.7

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 (101) hide show
  1. package/API_SPECIFICATION.md +1117 -0
  2. package/dist/exportType.js +1 -2
  3. package/dist/index.js +25 -30
  4. package/dist/lib/fileUpload.d.ts.map +1 -1
  5. package/dist/lib/fileUpload.js +31 -29
  6. package/dist/lib/googleCloudStorage.js +9 -14
  7. package/dist/lib/prisma.js +4 -7
  8. package/dist/lib/thumbnailGenerator.js +12 -20
  9. package/dist/middleware/auth.d.ts.map +1 -1
  10. package/dist/middleware/auth.js +17 -22
  11. package/dist/middleware/logging.js +5 -9
  12. package/dist/routers/_app.d.ts +3483 -1801
  13. package/dist/routers/_app.d.ts.map +1 -1
  14. package/dist/routers/_app.js +28 -27
  15. package/dist/routers/agenda.d.ts +13 -8
  16. package/dist/routers/agenda.d.ts.map +1 -1
  17. package/dist/routers/agenda.js +14 -17
  18. package/dist/routers/announcement.d.ts +4 -3
  19. package/dist/routers/announcement.d.ts.map +1 -1
  20. package/dist/routers/announcement.js +28 -31
  21. package/dist/routers/assignment.d.ts +282 -196
  22. package/dist/routers/assignment.d.ts.map +1 -1
  23. package/dist/routers/assignment.js +256 -202
  24. package/dist/routers/attendance.d.ts +5 -4
  25. package/dist/routers/attendance.d.ts.map +1 -1
  26. package/dist/routers/attendance.js +31 -34
  27. package/dist/routers/auth.d.ts +1 -0
  28. package/dist/routers/auth.d.ts.map +1 -1
  29. package/dist/routers/auth.js +80 -75
  30. package/dist/routers/class.d.ts +284 -14
  31. package/dist/routers/class.d.ts.map +1 -1
  32. package/dist/routers/class.js +435 -164
  33. package/dist/routers/event.d.ts +47 -38
  34. package/dist/routers/event.d.ts.map +1 -1
  35. package/dist/routers/event.js +76 -79
  36. package/dist/routers/file.d.ts +71 -1
  37. package/dist/routers/file.d.ts.map +1 -1
  38. package/dist/routers/file.js +267 -32
  39. package/dist/routers/folder.d.ts +296 -0
  40. package/dist/routers/folder.d.ts.map +1 -0
  41. package/dist/routers/folder.js +693 -0
  42. package/dist/routers/notifications.d.ts +103 -0
  43. package/dist/routers/notifications.d.ts.map +1 -0
  44. package/dist/routers/notifications.js +91 -0
  45. package/dist/routers/school.d.ts +208 -0
  46. package/dist/routers/school.d.ts.map +1 -0
  47. package/dist/routers/school.js +481 -0
  48. package/dist/routers/section.d.ts +1 -0
  49. package/dist/routers/section.d.ts.map +1 -1
  50. package/dist/routers/section.js +30 -33
  51. package/dist/routers/user.d.ts +2 -1
  52. package/dist/routers/user.d.ts.map +1 -1
  53. package/dist/routers/user.js +21 -24
  54. package/dist/seedDatabase.d.ts +22 -0
  55. package/dist/seedDatabase.d.ts.map +1 -0
  56. package/dist/seedDatabase.js +57 -0
  57. package/dist/socket/handlers.js +26 -30
  58. package/dist/trpc.d.ts +5 -0
  59. package/dist/trpc.d.ts.map +1 -1
  60. package/dist/trpc.js +35 -26
  61. package/dist/types/trpc.js +1 -2
  62. package/dist/utils/email.js +2 -8
  63. package/dist/utils/generateInviteCode.js +1 -5
  64. package/dist/utils/logger.d.ts.map +1 -1
  65. package/dist/utils/logger.js +13 -9
  66. package/dist/utils/prismaErrorHandler.d.ts +9 -0
  67. package/dist/utils/prismaErrorHandler.d.ts.map +1 -0
  68. package/dist/utils/prismaErrorHandler.js +234 -0
  69. package/dist/utils/prismaWrapper.d.ts +14 -0
  70. package/dist/utils/prismaWrapper.d.ts.map +1 -0
  71. package/dist/utils/prismaWrapper.js +64 -0
  72. package/package.json +11 -4
  73. package/prisma/migrations/20250807062924_init/migration.sql +436 -0
  74. package/prisma/migrations/migration_lock.toml +3 -0
  75. package/prisma/schema.prisma +67 -0
  76. package/src/index.ts +2 -2
  77. package/src/lib/fileUpload.ts +16 -7
  78. package/src/middleware/auth.ts +0 -2
  79. package/src/routers/_app.ts +5 -1
  80. package/src/routers/assignment.ts +82 -22
  81. package/src/routers/auth.ts +80 -54
  82. package/src/routers/class.ts +330 -36
  83. package/src/routers/file.ts +283 -20
  84. package/src/routers/folder.ts +755 -0
  85. package/src/routers/notifications.ts +93 -0
  86. package/src/seedDatabase.ts +66 -0
  87. package/src/socket/handlers.ts +4 -4
  88. package/src/trpc.ts +13 -0
  89. package/src/utils/logger.ts +14 -4
  90. package/src/utils/prismaErrorHandler.ts +275 -0
  91. package/src/utils/prismaWrapper.ts +91 -0
  92. package/tests/auth.test.ts +25 -0
  93. package/tests/class.test.ts +281 -0
  94. package/tests/setup.ts +98 -0
  95. package/tests/startup.test.ts +5 -0
  96. package/tsconfig.json +2 -1
  97. package/vitest.config.ts +11 -0
  98. package/dist/logger.d.ts +0 -26
  99. package/dist/logger.d.ts.map +0 -1
  100. package/dist/logger.js +0 -135
  101. package/src/logger.ts +0 -163
@@ -21,12 +21,14 @@ export const classRouter = createTRPCRouter({
21
21
  assignments: {
22
22
  where: {
23
23
  dueDate: {
24
- equals: new Date(new Date().setHours(0, 0, 0, 0)),
24
+ lte: new Date(new Date().setHours(23, 59, 59, 999)),
25
25
  },
26
+ template: false,
26
27
  },
27
28
  select: {
28
29
  id: true,
29
30
  title: true,
31
+ type: true,
30
32
  },
31
33
  },
32
34
  },
@@ -42,42 +44,21 @@ export const classRouter = createTRPCRouter({
42
44
  include: {
43
45
  assignments: {
44
46
  where: {
45
- dueDate: {
46
- equals: new Date(new Date().setHours(0, 0, 0, 0)),
47
+ dueDate: {
48
+ lte: new Date(new Date().setHours(23, 59, 59, 999)),
47
49
  },
50
+ template: false,
48
51
  },
49
52
  select: {
50
53
  id: true,
51
54
  title: true,
55
+ type: true,
52
56
  },
53
57
  },
54
58
  },
55
59
  }),
56
60
  ]);
57
61
 
58
- const adminClasses = await prisma.class.findMany({
59
- where: {
60
- teachers: {
61
- some: {
62
- id: ctx.user?.id,
63
- },
64
- },
65
- },
66
- include: {
67
- assignments: {
68
- where: {
69
- dueDate: {
70
- equals: new Date(new Date().setHours(0, 0, 0, 0)),
71
- },
72
- },
73
- select: {
74
- id: true,
75
- title: true,
76
- },
77
- },
78
- },
79
- });
80
-
81
62
  return {
82
63
  teacherInClass: teacherClasses.map(cls => ({
83
64
  id: cls.id,
@@ -85,6 +66,7 @@ export const classRouter = createTRPCRouter({
85
66
  section: cls.section,
86
67
  subject: cls.subject,
87
68
  dueToday: cls.assignments,
69
+ assignments: cls.assignments,
88
70
  color: cls.color,
89
71
  })),
90
72
  studentInClass: studentClasses.map(cls => ({
@@ -93,14 +75,7 @@ export const classRouter = createTRPCRouter({
93
75
  section: cls.section,
94
76
  subject: cls.subject,
95
77
  dueToday: cls.assignments,
96
- color: cls.color,
97
- })),
98
- adminInClass: adminClasses.map(cls => ({
99
- id: cls.id,
100
- name: cls.name,
101
- section: cls.section,
102
- subject: cls.subject,
103
- dueToday: cls.assignments,
78
+ assignments: cls.assignments,
104
79
  color: cls.color,
105
80
  })),
106
81
  };
@@ -155,7 +130,9 @@ export const classRouter = createTRPCRouter({
155
130
  weight: true,
156
131
  graded: true,
157
132
  maxGrade: true,
158
- instructions: true,
133
+ instructions: true,
134
+ inProgress: true,
135
+ template: false,
159
136
  section: {
160
137
  select: {
161
138
  id: true,
@@ -248,9 +225,10 @@ export const classRouter = createTRPCRouter({
248
225
  name: z.string(),
249
226
  section: z.string(),
250
227
  subject: z.string(),
228
+ color: z.string().optional(),
251
229
  }))
252
230
  .mutation(async ({ ctx, input }) => {
253
- const { students, teachers, name, section, subject } = input;
231
+ const { students, teachers, name, section, subject, color } = input;
254
232
 
255
233
  if (teachers && teachers.length > 0 && students && students.length > 0) {
256
234
  const newClass = await prisma.class.create({
@@ -258,6 +236,7 @@ export const classRouter = createTRPCRouter({
258
236
  name,
259
237
  section,
260
238
  subject,
239
+ color,
261
240
  teachers: {
262
241
  connect: teachers.map(teacher => ({ id: teacher })),
263
242
  },
@@ -278,6 +257,7 @@ export const classRouter = createTRPCRouter({
278
257
  name,
279
258
  section,
280
259
  subject,
260
+ color,
281
261
  teachers: {
282
262
  connect: {
283
263
  id: ctx.user?.id,
@@ -750,4 +730,318 @@ export const classRouter = createTRPCRouter({
750
730
 
751
731
  return gradingBoundary;
752
732
  }),
733
+ getSyllabus: protectedClassMemberProcedure
734
+ .input(z.object({
735
+ classId: z.string(),
736
+ }))
737
+ .query(async ({input}) => {
738
+ const {classId} = input;
739
+
740
+ const syllabus = (await prisma.class.findUnique({
741
+ where: {
742
+ id: classId,
743
+ },
744
+ }))?.syllabus;
745
+
746
+ const markSchemes = await prisma.markScheme.findMany({
747
+ where: {
748
+ classId,
749
+ }
750
+ });
751
+
752
+ const gradingBoundaries = await prisma.gradingBoundary.findMany({
753
+ where: {
754
+ classId,
755
+ }
756
+ });
757
+
758
+ return {syllabus, gradingBoundaries, markSchemes};
759
+ }),
760
+ updateSyllabus: protectedTeacherProcedure
761
+ .input(z.object({
762
+ classId: z.string(),
763
+ contents: z.string(),
764
+ }))
765
+ .mutation(async ({ input }) => {
766
+ const { contents, classId } = input;
767
+
768
+ if (!contents) throw new TRPCError({
769
+ code: 'BAD_REQUEST',
770
+ message: "Missing key contents",
771
+ });
772
+
773
+ const updated = await prisma.class.update({
774
+ where: {
775
+ id: classId
776
+ },
777
+ data: {
778
+ syllabus: contents,
779
+ }
780
+ });
781
+
782
+ return updated;
783
+ }),
784
+ // Lab Management Endpoints (Assignment-based)
785
+ listLabDrafts: protectedTeacherProcedure
786
+ .input(z.object({
787
+ classId: z.string(),
788
+ }))
789
+ .query(async ({ ctx, input }) => {
790
+ const { classId } = input;
791
+
792
+ const labDrafts = await prisma.assignment.findMany({
793
+ where: {
794
+ classId: classId,
795
+ teacherId: ctx.user?.id,
796
+ inProgress: true,
797
+ },
798
+ orderBy: {
799
+ modifiedAt: 'desc',
800
+ },
801
+ });
802
+
803
+ return labDrafts;
804
+ }),
805
+ createLabDraft: protectedTeacherProcedure
806
+ .input(z.object({
807
+ classId: z.string(),
808
+ title: z.string(),
809
+ type: z.enum(['LAB', 'HOMEWORK', 'QUIZ', 'TEST', 'PROJECT', 'ESSAY', 'DISCUSSION', 'PRESENTATION', 'OTHER']),
810
+ instructions: z.string(),
811
+ dueDate: z.date().optional(),
812
+ maxGrade: z.number().optional(),
813
+ weight: z.number().optional(),
814
+ graded: z.boolean().optional(),
815
+ sectionId: z.string().optional(),
816
+ markSchemeId: z.string().optional(),
817
+ gradingBoundaryId: z.string().optional(),
818
+ }))
819
+ .mutation(async ({ ctx, input }) => {
820
+ const { classId, ...draftData } = input;
821
+
822
+ const labDraft = await prisma.assignment.create({
823
+ data: {
824
+ classId: classId,
825
+ teacherId: ctx.user?.id!,
826
+ inProgress: true,
827
+ graded: draftData.graded ?? false,
828
+ maxGrade: draftData.maxGrade ?? 0,
829
+ weight: draftData.weight ?? 1,
830
+ dueDate: draftData.dueDate || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // Default 1 week from now
831
+ title: draftData.title,
832
+ instructions: draftData.instructions,
833
+ type: draftData.type,
834
+ ...(draftData.sectionId && { sectionId: draftData.sectionId }),
835
+ ...(draftData.markSchemeId && { markSchemeId: draftData.markSchemeId }),
836
+ ...(draftData.gradingBoundaryId && { gradingBoundaryId: draftData.gradingBoundaryId }),
837
+ },
838
+ });
839
+
840
+ return labDraft;
841
+ }),
842
+ updateLabDraft: protectedTeacherProcedure
843
+ .input(z.object({
844
+ classId: z.string(),
845
+ draftId: z.string(),
846
+ title: z.string().optional(),
847
+ instructions: z.string().optional(),
848
+ dueDate: z.date().optional(),
849
+ maxGrade: z.number().optional(),
850
+ weight: z.number().optional(),
851
+ graded: z.boolean().optional(),
852
+ sectionId: z.string().optional(),
853
+ markSchemeId: z.string().optional(),
854
+ gradingBoundaryId: z.string().optional(),
855
+ }))
856
+ .mutation(async ({ ctx, input }) => {
857
+ const { classId, draftId, ...updateData } = input;
858
+
859
+ const labDraft = await prisma.assignment.update({
860
+ where: {
861
+ id: draftId,
862
+ classId: classId,
863
+ teacherId: ctx.user?.id!,
864
+ inProgress: true,
865
+ },
866
+ data: {
867
+ ...(updateData.title && { title: updateData.title }),
868
+ ...(updateData.instructions && { instructions: updateData.instructions }),
869
+ ...(updateData.dueDate && { dueDate: updateData.dueDate }),
870
+ ...(updateData.maxGrade !== undefined && { maxGrade: updateData.maxGrade }),
871
+ ...(updateData.weight !== undefined && { weight: updateData.weight }),
872
+ ...(updateData.graded !== undefined && { graded: updateData.graded }),
873
+ ...(updateData.sectionId !== undefined && { sectionId: updateData.sectionId }),
874
+ ...(updateData.markSchemeId !== undefined && { markSchemeId: updateData.markSchemeId }),
875
+ ...(updateData.gradingBoundaryId !== undefined && { gradingBoundaryId: updateData.gradingBoundaryId }),
876
+ modifiedAt: new Date(),
877
+ },
878
+ });
879
+
880
+ return labDraft;
881
+ }),
882
+ deleteLabDraft: protectedTeacherProcedure
883
+ .input(z.object({
884
+ classId: z.string(),
885
+ draftId: z.string(),
886
+ }))
887
+ .mutation(async ({ ctx, input }) => {
888
+ const { classId, draftId } = input;
889
+
890
+ const labDraft = await prisma.assignment.delete({
891
+ where: {
892
+ id: draftId,
893
+ classId: classId,
894
+ teacherId: ctx.user?.id!,
895
+ inProgress: true,
896
+ },
897
+ });
898
+
899
+ return labDraft;
900
+ }),
901
+ publishLabDraft: protectedTeacherProcedure
902
+ .input(z.object({
903
+ classId: z.string(),
904
+ draftId: z.string(),
905
+ dueDate: z.date().optional(),
906
+ maxGrade: z.number().optional(),
907
+ weight: z.number().optional(),
908
+ graded: z.boolean().optional(),
909
+ }))
910
+ .mutation(async ({ ctx, input }) => {
911
+ const { classId, draftId, ...publishData } = input;
912
+
913
+ // Get the lab draft
914
+ const labDraft = await prisma.assignment.findUnique({
915
+ where: {
916
+ id: draftId,
917
+ classId: classId,
918
+ teacherId: ctx.user?.id!,
919
+ inProgress: true,
920
+ },
921
+ });
922
+
923
+ if (!labDraft) {
924
+ throw new TRPCError({
925
+ code: 'NOT_FOUND',
926
+ message: 'Lab draft not found',
927
+ });
928
+ }
929
+
930
+ // Publish the draft by updating it to not be in progress
931
+ const publishedAssignment = await prisma.assignment.update({
932
+ where: { id: draftId },
933
+ data: {
934
+ inProgress: false,
935
+ dueDate: publishData.dueDate || labDraft.dueDate,
936
+ maxGrade: publishData.maxGrade || labDraft.maxGrade || 100,
937
+ weight: publishData.weight || labDraft.weight || 1,
938
+ graded: publishData.graded !== undefined ? publishData.graded : true,
939
+ modifiedAt: new Date(),
940
+ },
941
+ });
942
+
943
+ return publishedAssignment;
944
+ }),
945
+ getFiles: protectedClassMemberProcedure
946
+ .input(z.object({
947
+ classId: z.string(),
948
+ }))
949
+ .query(async ({ ctx, input }) => {
950
+ const { classId } = input;
951
+
952
+ // Get all assignments with their files and submissions
953
+ const assignments = await prisma.assignment.findMany({
954
+ where: {
955
+ classId: classId,
956
+ },
957
+ include: {
958
+ attachments: {
959
+ select: {
960
+ id: true,
961
+ name: true,
962
+ type: true,
963
+ size: true,
964
+ path: true,
965
+ thumbnailId: true,
966
+ uploadedAt: true,
967
+ user: {
968
+ select: {
969
+ id: true,
970
+ username: true,
971
+ },
972
+ },
973
+ },
974
+ },
975
+ submissions: {
976
+ include: {
977
+ attachments: {
978
+ select: {
979
+ id: true,
980
+ name: true,
981
+ type: true,
982
+ size: true,
983
+ path: true,
984
+ thumbnailId: true,
985
+ uploadedAt: true,
986
+ user: {
987
+ select: {
988
+ id: true,
989
+ username: true,
990
+ },
991
+ },
992
+ },
993
+ },
994
+ annotations: {
995
+ select: {
996
+ id: true,
997
+ name: true,
998
+ type: true,
999
+ size: true,
1000
+ path: true,
1001
+ thumbnailId: true,
1002
+ uploadedAt: true,
1003
+ user: {
1004
+ select: {
1005
+ id: true,
1006
+ username: true,
1007
+ },
1008
+ },
1009
+ },
1010
+ },
1011
+ student: {
1012
+ select: {
1013
+ id: true,
1014
+ username: true,
1015
+ },
1016
+ },
1017
+ },
1018
+ },
1019
+ teacher: {
1020
+ select: {
1021
+ id: true,
1022
+ username: true,
1023
+ },
1024
+ },
1025
+ },
1026
+ orderBy: {
1027
+ createdAt: 'desc',
1028
+ },
1029
+ });
1030
+
1031
+ // Organize files by assignment structure
1032
+ const organizedFiles = assignments.map(assignment => ({
1033
+ id: assignment.id,
1034
+ title: assignment.title,
1035
+ teacher: assignment.teacher,
1036
+ teacherAttachments: assignment.attachments,
1037
+ students: assignment.submissions.map(submission => ({
1038
+ id: submission.student.id,
1039
+ username: submission.student.username,
1040
+ attachments: submission.attachments,
1041
+ annotations: submission.annotations,
1042
+ })),
1043
+ }));
1044
+
1045
+ return organizedFiles;
1046
+ }),
753
1047
  });