@studious-lms/server 1.1.0 → 1.1.2

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.
@@ -20,11 +20,14 @@ export declare const sectionRouter: import("@trpc/server").TRPCBuiltRouter<{
20
20
  input: {
21
21
  name: string;
22
22
  classId: string;
23
+ color?: string | undefined;
23
24
  };
24
25
  output: {
25
26
  id: string;
26
27
  name: string;
28
+ color: string | null;
27
29
  classId: string;
30
+ order: number | null;
28
31
  };
29
32
  meta: object;
30
33
  }>;
@@ -33,11 +36,25 @@ export declare const sectionRouter: import("@trpc/server").TRPCBuiltRouter<{
33
36
  id: string;
34
37
  name: string;
35
38
  classId: string;
39
+ color?: string | undefined;
36
40
  };
37
41
  output: {
38
42
  id: string;
39
43
  name: string;
44
+ color: string | null;
40
45
  classId: string;
46
+ order: number | null;
47
+ };
48
+ meta: object;
49
+ }>;
50
+ reOrder: import("@trpc/server").TRPCMutationProcedure<{
51
+ input: {
52
+ id: string;
53
+ classId: string;
54
+ order: number;
55
+ };
56
+ output: {
57
+ id: string;
41
58
  };
42
59
  meta: object;
43
60
  }>;
@@ -1 +1 @@
1
- {"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../src/routers/section.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAqBxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoHxB,CAAC"}
1
+ {"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../src/routers/section.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuBxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwMxB,CAAC"}
@@ -5,11 +5,13 @@ import { prisma } from "../lib/prisma.js";
5
5
  const createSectionSchema = z.object({
6
6
  classId: z.string(),
7
7
  name: z.string(),
8
+ color: z.string().optional(),
8
9
  });
9
10
  const updateSectionSchema = z.object({
10
11
  id: z.string(),
11
12
  classId: z.string(),
12
13
  name: z.string(),
14
+ color: z.string().optional(),
13
15
  });
14
16
  const deleteSectionSchema = z.object({
15
17
  id: z.string(),
@@ -45,11 +47,41 @@ export const sectionRouter = createTRPCRouter({
45
47
  const section = await prisma.section.create({
46
48
  data: {
47
49
  name: input.name,
50
+ order: 0,
48
51
  class: {
49
52
  connect: { id: input.classId },
50
53
  },
54
+ ...(input.color && {
55
+ color: input.color,
56
+ }),
51
57
  },
52
58
  });
59
+ // find all root items in the class and reorder them
60
+ const sections = await prisma.section.findMany({
61
+ where: {
62
+ classId: input.classId,
63
+ },
64
+ });
65
+ const assignments = await prisma.assignment.findMany({
66
+ where: {
67
+ classId: input.classId,
68
+ sectionId: null,
69
+ },
70
+ });
71
+ const stack = [...sections, ...assignments].sort((a, b) => (a.order || 0) - (b.order || 0)).map((item, index) => ({
72
+ id: item.id,
73
+ order: index + 1,
74
+ })).map((item) => ({
75
+ where: { id: item.id },
76
+ data: { order: item.order },
77
+ }));
78
+ // Update sections and assignments with their new order
79
+ await Promise.all([
80
+ ...stack.filter(item => sections.some(s => s.id === item.where.id))
81
+ .map(({ where, data }) => prisma.section.update({ where, data })),
82
+ ...stack.filter(item => assignments.some(a => a.id === item.where.id))
83
+ .map(({ where, data }) => prisma.assignment.update({ where, data }))
84
+ ]);
53
85
  return section;
54
86
  }),
55
87
  update: protectedProcedure
@@ -82,10 +114,51 @@ export const sectionRouter = createTRPCRouter({
82
114
  where: { id: input.id },
83
115
  data: {
84
116
  name: input.name,
117
+ ...(input.color && {
118
+ color: input.color,
119
+ }),
85
120
  },
86
121
  });
87
122
  return section;
88
123
  }),
124
+ reOrder: protectedProcedure
125
+ .input(z.object({
126
+ id: z.string(),
127
+ classId: z.string(),
128
+ order: z.number(),
129
+ }))
130
+ .mutation(async ({ ctx, input }) => {
131
+ if (!ctx.user) {
132
+ throw new TRPCError({
133
+ code: "UNAUTHORIZED",
134
+ message: "User must be authenticated",
135
+ });
136
+ }
137
+ // Verify user is a teacher of the class
138
+ const classData = await prisma.class.findFirst({
139
+ where: {
140
+ id: input.classId,
141
+ teachers: {
142
+ some: {
143
+ id: ctx.user.id,
144
+ },
145
+ },
146
+ },
147
+ });
148
+ if (!classData) {
149
+ throw new TRPCError({
150
+ code: "NOT_FOUND",
151
+ message: "Class not found or you are not a teacher",
152
+ });
153
+ }
154
+ await prisma.section.update({
155
+ where: { id: input.id },
156
+ data: {
157
+ order: input.order,
158
+ },
159
+ });
160
+ return { id: input.id };
161
+ }),
89
162
  delete: protectedProcedure
90
163
  .input(deleteSectionSchema)
91
164
  .mutation(async ({ ctx, input }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -0,0 +1,24 @@
1
+ -- DropForeignKey
2
+ ALTER TABLE "Notification" DROP CONSTRAINT "Notification_receiverId_fkey";
3
+
4
+ -- DropForeignKey
5
+ ALTER TABLE "UserProfile" DROP CONSTRAINT "UserProfile_userId_fkey";
6
+
7
+ -- AlterTable
8
+ ALTER TABLE "Assignment" ADD COLUMN "order" INTEGER;
9
+
10
+ -- AlterTable
11
+ ALTER TABLE "Folder" ADD COLUMN "color" TEXT DEFAULT '#3B82F6';
12
+
13
+ -- AlterTable
14
+ ALTER TABLE "Section" ADD COLUMN "color" TEXT DEFAULT '#3B82F6',
15
+ ADD COLUMN "order" INTEGER;
16
+
17
+ -- AlterTable
18
+ ALTER TABLE "Submission" ADD COLUMN "teacherComments" TEXT;
19
+
20
+ -- AddForeignKey
21
+ ALTER TABLE "UserProfile" ADD CONSTRAINT "UserProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
22
+
23
+ -- AddForeignKey
24
+ ALTER TABLE "Notification" ADD CONSTRAINT "Notification_receiverId_fkey" FOREIGN KEY ("receiverId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -132,6 +132,7 @@ model Folder {
132
132
  childFolders Folder[] @relation("ParentChildFolders")
133
133
  parentFolder Folder? @relation("ParentChildFolders", fields: [parentFolderId], references: [id])
134
134
  parentFolderId String?
135
+ color String? @default("#3B82F6")
135
136
  class Class? @relation("ClassFiles", fields: [classId], references: [id])
136
137
  classId String? @unique
137
138
  }
@@ -194,6 +195,7 @@ model Assignment {
194
195
  eventId String?
195
196
  markScheme MarkScheme? @relation(fields: [markSchemeId], references: [id], onDelete: Cascade)
196
197
  markSchemeId String?
198
+ order Int?
197
199
  gradingBoundary GradingBoundary? @relation(fields: [gradingBoundaryId], references: [id], onDelete: Cascade)
198
200
  gradingBoundaryId String?
199
201
  }
@@ -227,6 +229,7 @@ model Submission {
227
229
  gradeReceived Int?
228
230
 
229
231
  rubricState String?
232
+ teacherComments String?
230
233
 
231
234
  submittedAt DateTime?
232
235
  submitted Boolean? @default(false)
@@ -239,6 +242,8 @@ model Section {
239
242
  classId String
240
243
  class Class @relation(fields: [classId], references: [id], onDelete: Cascade)
241
244
  assignments Assignment[]
245
+ color String? @default("#3B82F6")
246
+ order Int?
242
247
  }
243
248
 
244
249
  model Session {
@@ -84,6 +84,59 @@ const updateSubmissionSchema = z.object({
84
84
  });
85
85
 
86
86
  export const assignmentRouter = createTRPCRouter({
87
+ order: protectedTeacherProcedure
88
+ .input(z.object({
89
+ id: z.string(),
90
+ classId: z.string(),
91
+ order: z.number(),
92
+ }))
93
+ .mutation(async ({ ctx, input }) => {
94
+ const { id, order } = input;
95
+
96
+ const assignment = await prisma.assignment.update({
97
+ where: { id },
98
+ data: { order },
99
+ });
100
+
101
+ return assignment;
102
+ }),
103
+
104
+ move: protectedTeacherProcedure
105
+ .input(z.object({
106
+ id: z.string(),
107
+ classId: z.string(),
108
+ targetSectionId: z.string(),
109
+ }))
110
+ .mutation(async ({ ctx, input }) => {
111
+ const { id, targetSectionId, } = input;
112
+
113
+
114
+ const assignments = await prisma.assignment.findMany({
115
+ where: { sectionId: targetSectionId },
116
+ });
117
+
118
+ const stack = assignments.sort((a, b) => (a.order || 0) - (b.order || 0)).map((assignment, index) => ({
119
+ id: assignment.id,
120
+ order: index + 1,
121
+ })).map((assignment) => ({
122
+ where: { id: assignment.id },
123
+ data: { order: assignment.order },
124
+ }));
125
+
126
+ await Promise.all(
127
+ stack.map(({ where, data }) =>
128
+ prisma.assignment.update({ where, data })
129
+ )
130
+ );
131
+
132
+ const assignment = await prisma.assignment.update({
133
+ where: { id },
134
+ data: { sectionId: targetSectionId, order: 0 },
135
+ });
136
+
137
+ return assignment;
138
+ }),
139
+
87
140
  create: protectedProcedure
88
141
  .input(createAssignmentSchema)
89
142
  .mutation(async ({ ctx, input }) => {
@@ -132,16 +185,35 @@ export const assignmentRouter = createTRPCRouter({
132
185
  }
133
186
  console.log(markSchemeId, gradingBoundaryId);
134
187
 
188
+ // find all assignments in the section it is in (or none) and reorder them
189
+ const assignments = await prisma.assignment.findMany({
190
+ where: {
191
+ classId: classId,
192
+ ...(sectionId && {
193
+ sectionId: sectionId,
194
+ }),
195
+ },
196
+ });
197
+
198
+ const stack = assignments.sort((a, b) => (a.order || 0) - (b.order || 0)).map((assignment, index) => ({
199
+ id: assignment.id,
200
+ order: index + 1,
201
+ })).map((assignment) => ({
202
+ where: { id: assignment.id },
203
+ data: { order: assignment.order },
204
+ }));
205
+
135
206
  // Create assignment with submissions for all students
136
207
  const assignment = await prisma.assignment.create({
137
208
  data: {
138
209
  title,
139
210
  instructions,
140
211
  dueDate: new Date(dueDate),
141
- maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
212
+ maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
142
213
  graded,
143
214
  weight,
144
215
  type,
216
+ order: 0,
145
217
  inProgress: inProgress || false,
146
218
  class: {
147
219
  connect: { id: classId }
@@ -156,7 +228,7 @@ export const assignmentRouter = createTRPCRouter({
156
228
  connect: { id: markSchemeId }
157
229
  }
158
230
  }),
159
- ...(gradingBoundaryId && {
231
+ ...(gradingBoundaryId && {
160
232
  gradingBoundary: {
161
233
  connect: { id: gradingBoundaryId }
162
234
  }
@@ -209,6 +281,12 @@ export const assignmentRouter = createTRPCRouter({
209
281
  }
210
282
  });
211
283
 
284
+ await Promise.all(
285
+ stack.map(({ where, data }) =>
286
+ prisma.assignment.update({ where, data })
287
+ )
288
+ );
289
+
212
290
  // Upload files if provided
213
291
  let uploadedFiles: UploadedFile[] = [];
214
292
  if (files && files.length > 0) {
@@ -478,7 +556,7 @@ export const assignmentRouter = createTRPCRouter({
478
556
  try {
479
557
  // Delete the main file
480
558
  await deleteFile(file.path);
481
-
559
+
482
560
  // Delete thumbnail if it exists
483
561
  if (file.thumbnail) {
484
562
  await deleteFile(file.thumbnail.path);
@@ -493,7 +571,7 @@ export const assignmentRouter = createTRPCRouter({
493
571
  where: { id },
494
572
  });
495
573
 
496
- return {
574
+ return {
497
575
  id,
498
576
  };
499
577
  }),
@@ -595,7 +673,7 @@ export const assignmentRouter = createTRPCRouter({
595
673
  id: true,
596
674
  name: true,
597
675
  },
598
- });
676
+ });
599
677
 
600
678
  return { ...assignment, sections };
601
679
  }),
@@ -913,7 +991,7 @@ export const assignmentRouter = createTRPCRouter({
913
991
 
914
992
  // Delete removed attachments if any
915
993
  if (removedAttachments && removedAttachments.length > 0) {
916
- const filesToDelete = submission.attachments.filter((file) =>
994
+ const filesToDelete = submission.attachments.filter((file) =>
917
995
  removedAttachments.includes(file.id)
918
996
  );
919
997
 
@@ -922,7 +1000,7 @@ export const assignmentRouter = createTRPCRouter({
922
1000
  try {
923
1001
  // Delete the main file
924
1002
  await deleteFile(file.path);
925
-
1003
+
926
1004
  // Delete thumbnail if it exists
927
1005
  if (file.thumbnail?.path) {
928
1006
  await deleteFile(file.thumbnail.path);
@@ -1181,7 +1259,7 @@ export const assignmentRouter = createTRPCRouter({
1181
1259
 
1182
1260
  // Delete removed attachments if any
1183
1261
  if (removedAttachments && removedAttachments.length > 0) {
1184
- const filesToDelete = submission.annotations.filter((file) =>
1262
+ const filesToDelete = submission.annotations.filter((file) =>
1185
1263
  removedAttachments.includes(file.id)
1186
1264
  );
1187
1265
 
@@ -1190,7 +1268,7 @@ export const assignmentRouter = createTRPCRouter({
1190
1268
  try {
1191
1269
  // Delete the main file
1192
1270
  await deleteFile(file.path);
1193
-
1271
+
1194
1272
  // Delete thumbnail if it exists
1195
1273
  if (file.thumbnail?.path) {
1196
1274
  await deleteFile(file.thumbnail.path);
@@ -1575,7 +1653,7 @@ export const assignmentRouter = createTRPCRouter({
1575
1653
 
1576
1654
  return updatedAssignment;
1577
1655
  }),
1578
- detachMarkScheme: protectedTeacherProcedure
1656
+ detachMarkScheme: protectedTeacherProcedure
1579
1657
  .input(z.object({
1580
1658
  assignmentId: z.string(),
1581
1659
  }))
@@ -1594,7 +1672,7 @@ export const assignmentRouter = createTRPCRouter({
1594
1672
  message: "Assignment not found",
1595
1673
  });
1596
1674
  }
1597
-
1675
+
1598
1676
  const updatedAssignment = await prisma.assignment.update({
1599
1677
  where: { id: assignmentId },
1600
1678
  data: {
@@ -1613,7 +1691,7 @@ export const assignmentRouter = createTRPCRouter({
1613
1691
 
1614
1692
  return updatedAssignment;
1615
1693
  }),
1616
- attachGradingBoundary: protectedTeacherProcedure
1694
+ attachGradingBoundary: protectedTeacherProcedure
1617
1695
  .input(z.object({
1618
1696
  assignmentId: z.string(),
1619
1697
  gradingBoundaryId: z.string().nullable(),
@@ -128,6 +128,7 @@ export const classRouter = createTRPCRouter({
128
128
  dueDate: true,
129
129
  createdAt: true,
130
130
  weight: true,
131
+ order: true,
131
132
  graded: true,
132
133
  maxGrade: true,
133
134
  instructions: true,
@@ -172,7 +173,7 @@ export const classRouter = createTRPCRouter({
172
173
  if (!classData) {
173
174
  throw new Error('Class not found');
174
175
  }
175
-
176
+
176
177
  const formattedClassData = {
177
178
  ...classData,
178
179
  assignments: classData.assignments.map(assignment => ({
@@ -14,6 +14,7 @@ const fileSchema = z.object({
14
14
  const createFolderSchema = z.object({
15
15
  name: z.string(),
16
16
  parentFolderId: z.string().optional(),
17
+ color: z.string().optional(),
17
18
  });
18
19
 
19
20
  const uploadFilesToFolderSchema = z.object({
@@ -29,7 +30,7 @@ export const folderRouter = createTRPCRouter({
29
30
  create: protectedTeacherProcedure
30
31
  .input(createFolderSchema)
31
32
  .mutation(async ({ ctx, input }) => {
32
- const { classId, name } = input;
33
+ const { classId, name, color } = input;
33
34
  let parentFolderId = input.parentFolderId || null;
34
35
 
35
36
  if (!ctx.user) {
@@ -75,6 +76,9 @@ export const folderRouter = createTRPCRouter({
75
76
  class: {
76
77
  connect: { id: classId },
77
78
  },
79
+ ...(color && {
80
+ color: color,
81
+ }),
78
82
  },
79
83
  });
80
84
  }
@@ -104,6 +108,9 @@ export const folderRouter = createTRPCRouter({
104
108
  connect: { id: parentFolderId },
105
109
  },
106
110
  }),
111
+ ...(color && {
112
+ color: color,
113
+ }),
107
114
  },
108
115
  include: {
109
116
  files: {
@@ -317,6 +324,7 @@ export const folderRouter = createTRPCRouter({
317
324
  select: {
318
325
  id: true,
319
326
  name: true,
327
+ color: true,
320
328
  _count: {
321
329
  select: {
322
330
  files: true,
@@ -391,6 +399,7 @@ export const folderRouter = createTRPCRouter({
391
399
  select: {
392
400
  id: true,
393
401
  name: true,
402
+ color: true,
394
403
  files: {
395
404
  select: {
396
405
  id: true,
@@ -682,14 +691,15 @@ export const folderRouter = createTRPCRouter({
682
691
  return updatedFolder;
683
692
  }),
684
693
 
685
- rename: protectedTeacherProcedure
694
+ update: protectedTeacherProcedure
686
695
  .input(z.object({
687
696
  folderId: z.string(),
688
- newName: z.string(),
697
+ name: z.string(),
698
+ color: z.string().optional(),
689
699
  classId: z.string(),
690
700
  }))
691
701
  .mutation(async ({ ctx, input }) => {
692
- const { folderId, newName, classId } = input;
702
+ const { folderId, name, color, classId } = input;
693
703
 
694
704
  // Get the folder
695
705
  const folder = await prisma.folder.findFirst({
@@ -706,7 +716,7 @@ export const folderRouter = createTRPCRouter({
706
716
  }
707
717
 
708
718
  // Validate new name
709
- if (!newName.trim()) {
719
+ if (!name.trim()) {
710
720
  throw new TRPCError({
711
721
  code: "BAD_REQUEST",
712
722
  message: "Folder name cannot be empty",
@@ -717,7 +727,10 @@ export const folderRouter = createTRPCRouter({
717
727
  const updatedFolder = await prisma.folder.update({
718
728
  where: { id: folderId },
719
729
  data: {
720
- name: newName.trim(),
730
+ name: name.trim(),
731
+ ...(color && {
732
+ color: color,
733
+ }),
721
734
  },
722
735
  include: {
723
736
  files: {
@@ -6,12 +6,14 @@ import { prisma } from "../lib/prisma.js";
6
6
  const createSectionSchema = z.object({
7
7
  classId: z.string(),
8
8
  name: z.string(),
9
+ color: z.string().optional(),
9
10
  });
10
11
 
11
12
  const updateSectionSchema = z.object({
12
13
  id: z.string(),
13
14
  classId: z.string(),
14
15
  name: z.string(),
16
+ color: z.string().optional(),
15
17
  });
16
18
 
17
19
  const deleteSectionSchema = z.object({
@@ -52,12 +54,50 @@ export const sectionRouter = createTRPCRouter({
52
54
  const section = await prisma.section.create({
53
55
  data: {
54
56
  name: input.name,
57
+ order: 0,
55
58
  class: {
56
59
  connect: { id: input.classId },
57
60
  },
61
+ ...(input.color && {
62
+ color: input.color,
63
+ }),
58
64
  },
59
65
  });
60
66
 
67
+ // find all root items in the class and reorder them
68
+ const sections = await prisma.section.findMany({
69
+ where: {
70
+ classId: input.classId,
71
+ },
72
+ });
73
+
74
+ const assignments = await prisma.assignment.findMany({
75
+ where: {
76
+ classId: input.classId,
77
+ sectionId: null,
78
+ },
79
+ });
80
+
81
+ const stack = [...sections, ...assignments].sort((a, b) => (a.order || 0) - (b.order || 0)).map((item, index) => ({
82
+ id: item.id,
83
+ order: index + 1,
84
+ })).map((item) => ({
85
+ where: { id: item.id },
86
+ data: { order: item.order },
87
+ }));
88
+
89
+ // Update sections and assignments with their new order
90
+ await Promise.all([
91
+ ...stack.filter(item => sections.some(s => s.id === item.where.id))
92
+ .map(({ where, data }) =>
93
+ prisma.section.update({ where, data })
94
+ ),
95
+ ...stack.filter(item => assignments.some(a => a.id === item.where.id))
96
+ .map(({ where, data }) =>
97
+ prisma.assignment.update({ where, data })
98
+ )
99
+ ]);
100
+
61
101
  return section;
62
102
  }),
63
103
 
@@ -94,12 +134,58 @@ export const sectionRouter = createTRPCRouter({
94
134
  where: { id: input.id },
95
135
  data: {
96
136
  name: input.name,
137
+ ...(input.color && {
138
+ color: input.color,
139
+ }),
97
140
  },
98
141
  });
99
142
 
100
143
  return section;
101
144
  }),
102
145
 
146
+ reOrder: protectedProcedure
147
+ .input(z.object({
148
+ id: z.string(),
149
+ classId: z.string(),
150
+ order: z.number(),
151
+ }))
152
+ .mutation(async ({ ctx, input }) => {
153
+ if (!ctx.user) {
154
+ throw new TRPCError({
155
+ code: "UNAUTHORIZED",
156
+ message: "User must be authenticated",
157
+ });
158
+ }
159
+
160
+ // Verify user is a teacher of the class
161
+ const classData = await prisma.class.findFirst({
162
+ where: {
163
+ id: input.classId,
164
+ teachers: {
165
+ some: {
166
+ id: ctx.user.id,
167
+ },
168
+ },
169
+ },
170
+ });
171
+
172
+ if (!classData) {
173
+ throw new TRPCError({
174
+ code: "NOT_FOUND",
175
+ message: "Class not found or you are not a teacher",
176
+ });
177
+ }
178
+
179
+ await prisma.section.update({
180
+ where: { id: input.id },
181
+ data: {
182
+ order: input.order,
183
+ },
184
+ });
185
+
186
+ return { id: input.id };
187
+ }),
188
+
103
189
  delete: protectedProcedure
104
190
  .input(deleteSectionSchema)
105
191
  .mutation(async ({ ctx, input }) => {