@studious-lms/server 1.0.6 → 1.0.8

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