@studious-lms/server 1.4.5 → 1.4.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 (167) hide show
  1. package/dist/lib/config/cors.d.ts.map +1 -1
  2. package/dist/lib/config/cors.js +8 -3
  3. package/dist/lib/config/cors.js.map +1 -1
  4. package/dist/models/access.d.ts +112 -0
  5. package/dist/models/access.d.ts.map +1 -0
  6. package/dist/models/access.js +49 -0
  7. package/dist/models/access.js.map +1 -0
  8. package/dist/models/announcement.d.ts +17 -2
  9. package/dist/models/announcement.d.ts.map +1 -1
  10. package/dist/models/announcement.js +21 -4
  11. package/dist/models/announcement.js.map +1 -1
  12. package/dist/models/assignment.d.ts +37 -37
  13. package/dist/models/assignment.d.ts.map +1 -1
  14. package/dist/models/assignment.js +15 -15
  15. package/dist/models/assignment.js.map +1 -1
  16. package/dist/models/attendance.d.ts +1 -1
  17. package/dist/models/attendance.d.ts.map +1 -1
  18. package/dist/models/attendance.js +6 -5
  19. package/dist/models/attendance.js.map +1 -1
  20. package/dist/models/auth.d.ts +5 -5
  21. package/dist/models/auth.d.ts.map +1 -1
  22. package/dist/models/auth.js +7 -11
  23. package/dist/models/auth.js.map +1 -1
  24. package/dist/models/class.d.ts +26 -26
  25. package/dist/models/class.d.ts.map +1 -1
  26. package/dist/models/class.js +7 -11
  27. package/dist/models/class.js.map +1 -1
  28. package/dist/models/comment.d.ts +233 -6
  29. package/dist/models/comment.d.ts.map +1 -1
  30. package/dist/models/comment.js +133 -3
  31. package/dist/models/comment.js.map +1 -1
  32. package/dist/models/conversation.d.ts +7 -7
  33. package/dist/models/event.d.ts +52 -3
  34. package/dist/models/event.d.ts.map +1 -1
  35. package/dist/models/event.js +56 -6
  36. package/dist/models/event.js.map +1 -1
  37. package/dist/models/file.d.ts +178 -103
  38. package/dist/models/file.d.ts.map +1 -1
  39. package/dist/models/file.js +54 -14
  40. package/dist/models/file.js.map +1 -1
  41. package/dist/models/folder.d.ts +22 -22
  42. package/dist/models/folder.d.ts.map +1 -1
  43. package/dist/models/folder.js +5 -4
  44. package/dist/models/folder.js.map +1 -1
  45. package/dist/models/labChat.d.ts +20 -20
  46. package/dist/models/labChat.d.ts.map +1 -1
  47. package/dist/models/labChat.js +8 -10
  48. package/dist/models/labChat.js.map +1 -1
  49. package/dist/models/marketing.d.ts +1 -1
  50. package/dist/models/message.d.ts +6 -6
  51. package/dist/models/newtonChat.d.ts +5 -5
  52. package/dist/models/school.d.ts +361 -0
  53. package/dist/models/school.d.ts.map +1 -0
  54. package/dist/models/school.js +314 -0
  55. package/dist/models/school.js.map +1 -0
  56. package/dist/models/section.d.ts +1 -1
  57. package/dist/models/section.d.ts.map +1 -1
  58. package/dist/models/section.js +5 -4
  59. package/dist/models/section.js.map +1 -1
  60. package/dist/models/user.d.ts +4 -4
  61. package/dist/models/worksheet.d.ts +67 -65
  62. package/dist/models/worksheet.d.ts.map +1 -1
  63. package/dist/models/worksheet.js +6 -14
  64. package/dist/models/worksheet.js.map +1 -1
  65. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  66. package/dist/pipelines/aiLabChat.js +4 -3
  67. package/dist/pipelines/aiLabChat.js.map +1 -1
  68. package/dist/pipelines/gradeWorksheet.d.ts +2 -2
  69. package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
  70. package/dist/routers/_app.d.ts +2462 -1104
  71. package/dist/routers/_app.d.ts.map +1 -1
  72. package/dist/routers/_app.js +4 -2
  73. package/dist/routers/_app.js.map +1 -1
  74. package/dist/routers/announcement.d.ts +13 -5
  75. package/dist/routers/announcement.d.ts.map +1 -1
  76. package/dist/routers/announcement.js +25 -5
  77. package/dist/routers/announcement.js.map +1 -1
  78. package/dist/routers/assignment.d.ts +266 -36
  79. package/dist/routers/assignment.d.ts.map +1 -1
  80. package/dist/routers/assignment.js +221 -3
  81. package/dist/routers/assignment.js.map +1 -1
  82. package/dist/routers/class.d.ts +21 -20
  83. package/dist/routers/class.d.ts.map +1 -1
  84. package/dist/routers/class.js +3 -2
  85. package/dist/routers/class.js.map +1 -1
  86. package/dist/routers/comment.d.ts +40 -3
  87. package/dist/routers/comment.d.ts.map +1 -1
  88. package/dist/routers/comment.js +14 -5
  89. package/dist/routers/comment.js.map +1 -1
  90. package/dist/routers/conversation.d.ts +6 -6
  91. package/dist/routers/event.d.ts +21 -2
  92. package/dist/routers/event.d.ts.map +1 -1
  93. package/dist/routers/file.d.ts +2 -2
  94. package/dist/routers/folder.d.ts +24 -24
  95. package/dist/routers/labChat.d.ts +7 -7
  96. package/dist/routers/school.d.ts +384 -0
  97. package/dist/routers/school.d.ts.map +1 -0
  98. package/dist/routers/school.js +79 -0
  99. package/dist/routers/school.js.map +1 -0
  100. package/dist/routers/search.d.ts +1 -1
  101. package/dist/routers/worksheet.d.ts +23 -21
  102. package/dist/routers/worksheet.d.ts.map +1 -1
  103. package/dist/services/announcement.d.ts.map +1 -1
  104. package/dist/services/announcement.js +10 -6
  105. package/dist/services/announcement.js.map +1 -1
  106. package/dist/services/assignment.d.ts +36 -36
  107. package/dist/services/class.d.ts +20 -19
  108. package/dist/services/class.d.ts.map +1 -1
  109. package/dist/services/class.js +22 -3
  110. package/dist/services/class.js.map +1 -1
  111. package/dist/services/comment.d.ts +31 -5
  112. package/dist/services/comment.d.ts.map +1 -1
  113. package/dist/services/comment.js +102 -19
  114. package/dist/services/comment.js.map +1 -1
  115. package/dist/services/conversation.d.ts +6 -6
  116. package/dist/services/event.d.ts +21 -2
  117. package/dist/services/event.d.ts.map +1 -1
  118. package/dist/services/event.js +11 -7
  119. package/dist/services/event.js.map +1 -1
  120. package/dist/services/file.d.ts +2 -2
  121. package/dist/services/file.d.ts.map +1 -1
  122. package/dist/services/file.js +17 -8
  123. package/dist/services/file.js.map +1 -1
  124. package/dist/services/folder.d.ts +24 -24
  125. package/dist/services/labChat.d.ts +7 -7
  126. package/dist/services/school.d.ts +299 -0
  127. package/dist/services/school.d.ts.map +1 -0
  128. package/dist/services/school.js +225 -0
  129. package/dist/services/school.js.map +1 -0
  130. package/dist/services/worksheet.d.ts +23 -21
  131. package/dist/services/worksheet.d.ts.map +1 -1
  132. package/package.json +1 -1
  133. package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +60 -0
  134. package/prisma/migrations/20260504071500_school_memberships/migration.sql +81 -0
  135. package/prisma/schema.prisma +75 -2
  136. package/src/lib/config/cors.ts +8 -1
  137. package/src/models/access.ts +48 -0
  138. package/src/models/announcement.ts +19 -2
  139. package/src/models/assignment.ts +17 -13
  140. package/src/models/attendance.ts +4 -3
  141. package/src/models/auth.ts +8 -9
  142. package/src/models/class.ts +8 -9
  143. package/src/models/comment.ts +137 -1
  144. package/src/models/event.ts +54 -4
  145. package/src/models/file.ts +53 -12
  146. package/src/models/folder.ts +3 -2
  147. package/src/models/labChat.ts +9 -8
  148. package/src/models/school.ts +361 -0
  149. package/src/models/section.ts +3 -2
  150. package/src/models/worksheet.ts +7 -12
  151. package/src/pipelines/aiLabChat.ts +2 -1
  152. package/src/routers/_app.ts +2 -0
  153. package/src/routers/announcement.ts +30 -4
  154. package/src/routers/assignment.ts +243 -2
  155. package/src/routers/class.ts +1 -0
  156. package/src/routers/comment.ts +19 -2
  157. package/src/routers/school.ts +122 -0
  158. package/src/services/announcement.ts +12 -4
  159. package/src/services/class.ts +25 -2
  160. package/src/services/comment.ts +122 -18
  161. package/src/services/event.ts +23 -6
  162. package/src/services/file.ts +20 -6
  163. package/src/services/school.ts +308 -0
  164. package/tests/lib/cors.test.ts +13 -1
  165. package/tests/routers/assignment.test.ts +68 -2
  166. package/tests/routers/comment.test.ts +42 -1
  167. package/vercel.json +8 -0
@@ -2,6 +2,11 @@
2
2
  * Assignment model – assignments, submissions, attachments.
3
3
  */
4
4
  import { prisma } from "../lib/prisma.js";
5
+ import {
6
+ classMemberOrSchoolAdminWhere,
7
+ classTeacherOrSchoolAdminWhere,
8
+ schoolAdminForClassWhere,
9
+ } from "./access.js";
5
10
 
6
11
  const assignmentGetInclude = {
7
12
  submissions: {
@@ -57,7 +62,13 @@ export function findAssignmentWithDetails(id: string) {
57
62
  /** @returns Assignment with attachments for delete. */
58
63
  export function findAssignmentForDelete(id: string, teacherId: string) {
59
64
  return prisma.assignment.findFirst({
60
- where: { id, teacherId },
65
+ where: {
66
+ id,
67
+ OR: [
68
+ { teacherId },
69
+ { class: schoolAdminForClassWhere(teacherId) },
70
+ ],
71
+ },
61
72
  include: {
62
73
  attachments: { include: { thumbnail: true } },
63
74
  submissions: {
@@ -115,12 +126,7 @@ export function findSubmissionByIdForClassMember(submissionId: string, classId:
115
126
  id: submissionId,
116
127
  assignment: {
117
128
  classId,
118
- class: {
119
- OR: [
120
- { teachers: { some: { id: userId } } },
121
- { students: { some: { id: userId } } },
122
- ],
123
- },
129
+ class: classMemberOrSchoolAdminWhere(userId),
124
130
  },
125
131
  },
126
132
  include: {
@@ -148,9 +154,7 @@ export function findSubmissionForUpdate(submissionId: string, userId: string) {
148
154
  { studentId: userId },
149
155
  {
150
156
  assignment: {
151
- class: {
152
- teachers: { some: { id: userId } },
153
- },
157
+ class: classTeacherOrSchoolAdminWhere(userId),
154
158
  },
155
159
  },
156
160
  ],
@@ -204,7 +208,7 @@ export function findAssignmentWithClassForTeacher(assignmentId: string, teacherI
204
208
  return prisma.assignment.findFirst({
205
209
  where: {
206
210
  id: assignmentId,
207
- class: { teachers: { some: { id: teacherId } } },
211
+ class: classTeacherOrSchoolAdminWhere(teacherId),
208
212
  },
209
213
  include: { class: true },
210
214
  });
@@ -216,7 +220,7 @@ export function findSubmissionsByAssignmentForTeacher(assignmentId: string, teac
216
220
  where: {
217
221
  assignment: {
218
222
  id: assignmentId,
219
- class: { teachers: { some: { id: teacherId } } },
223
+ class: classTeacherOrSchoolAdminWhere(teacherId),
220
224
  },
221
225
  },
222
226
  include: {
@@ -251,7 +255,7 @@ export function findSubmissionForTeacherUpdate(submissionId: string, teacherId:
251
255
  where: {
252
256
  id: submissionId,
253
257
  assignment: {
254
- class: { teachers: { some: { id: teacherId } } },
258
+ class: classTeacherOrSchoolAdminWhere(teacherId),
255
259
  },
256
260
  },
257
261
  include: {
@@ -2,6 +2,7 @@
2
2
  * Attendance model – attendance records, present/late/absent students per event.
3
3
  */
4
4
  import { prisma } from "../lib/prisma.js";
5
+ import { classTeacherOrSchoolAdminWhere } from "./access.js";
5
6
 
6
7
  const attendanceInclude = {
7
8
  event: {
@@ -105,12 +106,12 @@ export function findClassWithStudents(classId: string) {
105
106
  });
106
107
  }
107
108
 
108
- /** @returns Class if user is a teacher. */
109
+ /** @returns Class if user is a teacher or school admin. */
109
110
  export function findClassWithTeacher(classId: string, teacherId: string) {
110
- return prisma.class.findUnique({
111
+ return prisma.class.findFirst({
111
112
  where: {
112
113
  id: classId,
113
- teachers: { some: { id: teacherId } },
114
+ ...classTeacherOrSchoolAdminWhere(teacherId),
114
115
  },
115
116
  });
116
117
  }
@@ -5,6 +5,10 @@
5
5
  import { SessionType } from "@prisma/client";
6
6
  import { getRedis } from "../lib/redis.js";
7
7
  import { prisma } from "../lib/prisma.js";
8
+ import {
9
+ classMemberOrSchoolAdminWhere,
10
+ classTeacherOrSchoolAdminWhere,
11
+ } from "./access.js";
8
12
 
9
13
  const AUTH_SESSION_CACHE_PREFIX = "auth-session:";
10
14
 
@@ -246,9 +250,7 @@ export async function findUserBySessionToken(token: string) {
246
250
  /** @returns Class IDs where user is a teacher. */
247
251
  export function findTeacherClassesByUserId(userId: string) {
248
252
  return prisma.class.findMany({
249
- where: {
250
- teachers: { some: { id: userId } },
251
- },
253
+ where: classTeacherOrSchoolAdminWhere(userId),
252
254
  select: { id: true },
253
255
  });
254
256
  }
@@ -258,20 +260,17 @@ export function findClassWithMember(classId: string, userId: string) {
258
260
  return prisma.class.findFirst({
259
261
  where: {
260
262
  id: classId,
261
- OR: [
262
- { students: { some: { id: userId } } },
263
- { teachers: { some: { id: userId } } },
264
- ],
263
+ ...classMemberOrSchoolAdminWhere(userId),
265
264
  },
266
265
  });
267
266
  }
268
267
 
269
- /** @returns Class if user is a teacher. */
268
+ /** @returns Class if user is a teacher or school admin. */
270
269
  export function findClassWithTeacher(classId: string, teacherId: string) {
271
270
  return prisma.class.findFirst({
272
271
  where: {
273
272
  id: classId,
274
- teachers: { some: { id: teacherId } },
273
+ ...classTeacherOrSchoolAdminWhere(teacherId),
275
274
  },
276
275
  });
277
276
  }
@@ -10,6 +10,10 @@ import type { PrismaClient, WorksheetQuestion } from "@prisma/client";
10
10
  import { prisma } from "../lib/prisma.js";
11
11
  import { v4 as uuidv4 } from 'uuid';
12
12
  import { copyFile } from "../lib/googleCloudStorage.js";
13
+ import {
14
+ classMemberOrSchoolAdminWhere,
15
+ classTeacherOrSchoolAdminWhere,
16
+ } from "./access.js";
13
17
 
14
18
  const classIncludeForGetAll = {
15
19
  assignments: {
@@ -55,7 +59,7 @@ const classIncludeForGetAll = {
55
59
  /** @returns Classes where user is a teacher. */
56
60
  export function findTeacherClasses(userId: string) {
57
61
  return prisma.class.findMany({
58
- where: { teachers: { some: { id: userId } } },
62
+ where: classTeacherOrSchoolAdminWhere(userId),
59
63
  include: classIncludeForGetAll,
60
64
  });
61
65
  }
@@ -68,12 +72,12 @@ export function findStudentClasses(userId: string) {
68
72
  });
69
73
  }
70
74
 
71
- /** @returns Class if user is a teacher. */
75
+ /** @returns Class if user is a teacher or school admin. */
72
76
  export function isTeacherInClass(classId: string, userId: string) {
73
77
  return prisma.class.findFirst({
74
78
  where: {
75
79
  id: classId,
76
- teachers: { some: { id: userId } },
80
+ ...classTeacherOrSchoolAdminWhere(userId),
77
81
  },
78
82
  });
79
83
  }
@@ -218,12 +222,7 @@ export function findClassWithAssignments(
218
222
  where: {
219
223
  id: classId,
220
224
  ...(options.userId
221
- ? {
222
- OR: [
223
- { students: { some: { id: options.userId } } },
224
- { teachers: { some: { id: options.userId } } },
225
- ],
226
- }
225
+ ? classMemberOrSchoolAdminWhere(options.userId)
227
226
  : {}),
228
227
  },
229
228
  include: {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Comment model – comments, replies, and reactions (for announcements/worksheets).
2
+ * Comment model – comments, replies, and reactions.
3
3
  */
4
4
  import { prisma } from "../lib/prisma.js";
5
5
  import type { ReactionType } from "@prisma/client";
@@ -7,6 +7,10 @@ import type { ReactionType } from "@prisma/client";
7
7
  export const commentSelect = {
8
8
  id: true,
9
9
  content: true,
10
+ announcementId: true,
11
+ assignmentId: true,
12
+ submissionId: true,
13
+ parentCommentId: true,
10
14
  createdAt: true,
11
15
  modifiedAt: true,
12
16
  author: {
@@ -42,6 +46,27 @@ export const commentSelect = {
42
46
  },
43
47
  } as const;
44
48
 
49
+ const commentClassAuthSelect = {
50
+ id: true,
51
+ teachers: { select: { id: true } },
52
+ students: { select: { id: true } },
53
+ school: {
54
+ select: {
55
+ users: {
56
+ where: { role: "ADMIN" as const },
57
+ select: { id: true },
58
+ },
59
+ memberships: {
60
+ where: {
61
+ role: "ADMIN" as const,
62
+ status: "ACTIVE" as const,
63
+ },
64
+ select: { userId: true },
65
+ },
66
+ },
67
+ },
68
+ } as const;
69
+
45
70
  /** @returns Comment by ID with author and reactions. */
46
71
  export function findCommentById(id: string) {
47
72
  return prisma.comment.findUnique({
@@ -77,6 +102,10 @@ export function createComment(data: {
77
102
  parentCommentId: string;
78
103
  content: string;
79
104
  authorId: string;
105
+ announcementId?: string | null;
106
+ assignmentId?: string | null;
107
+ submissionId?: string | null;
108
+ studentQuestionProgressId?: string | null;
80
109
  }) {
81
110
  return prisma.comment.create({
82
111
  data,
@@ -142,6 +171,113 @@ export function findCommentWithAnnouncement(commentId: string) {
142
171
  });
143
172
  }
144
173
 
174
+ /** @returns Comment with the owning class context for authorization. */
175
+ export function findCommentWithContext(commentId: string) {
176
+ return prisma.comment.findUnique({
177
+ where: { id: commentId },
178
+ include: {
179
+ announcement: {
180
+ select: {
181
+ class: {
182
+ select: commentClassAuthSelect,
183
+ },
184
+ },
185
+ },
186
+ assignment: {
187
+ select: {
188
+ class: {
189
+ select: commentClassAuthSelect,
190
+ },
191
+ },
192
+ },
193
+ submission: {
194
+ select: {
195
+ studentId: true,
196
+ assignment: {
197
+ select: {
198
+ class: {
199
+ select: commentClassAuthSelect,
200
+ },
201
+ },
202
+ },
203
+ },
204
+ },
205
+ studentQuestionProgress: {
206
+ select: {
207
+ studentId: true,
208
+ studentWorksheetResponse: {
209
+ select: {
210
+ submission: {
211
+ select: {
212
+ studentId: true,
213
+ assignment: {
214
+ select: {
215
+ class: {
216
+ select: commentClassAuthSelect,
217
+ },
218
+ },
219
+ },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ },
225
+ },
226
+ parentComment: {
227
+ include: {
228
+ announcement: {
229
+ select: {
230
+ class: {
231
+ select: commentClassAuthSelect,
232
+ },
233
+ },
234
+ },
235
+ assignment: {
236
+ select: {
237
+ class: {
238
+ select: commentClassAuthSelect,
239
+ },
240
+ },
241
+ },
242
+ submission: {
243
+ select: {
244
+ studentId: true,
245
+ assignment: {
246
+ select: {
247
+ class: {
248
+ select: commentClassAuthSelect,
249
+ },
250
+ },
251
+ },
252
+ },
253
+ },
254
+ studentQuestionProgress: {
255
+ select: {
256
+ studentId: true,
257
+ studentWorksheetResponse: {
258
+ select: {
259
+ submission: {
260
+ select: {
261
+ studentId: true,
262
+ assignment: {
263
+ select: {
264
+ class: {
265
+ select: commentClassAuthSelect,
266
+ },
267
+ },
268
+ },
269
+ },
270
+ },
271
+ },
272
+ },
273
+ },
274
+ },
275
+ },
276
+ },
277
+ },
278
+ });
279
+ }
280
+
145
281
  /** @returns Reaction counts by type for a comment. */
146
282
  export function getReactionCountsByCommentId(commentId: string) {
147
283
  return prisma.reaction.groupBy({
@@ -2,9 +2,29 @@
2
2
  * Event model – calendar events, assignment attachment/detachment.
3
3
  */
4
4
  import { prisma } from "../lib/prisma.js";
5
+ import { classTeacherOrSchoolAdminWhere } from "./access.js";
5
6
 
6
7
  const eventGetInclude = {
7
- class: true,
8
+ class: {
9
+ include: {
10
+ teachers: { select: { id: true } },
11
+ school: {
12
+ include: {
13
+ users: {
14
+ where: { role: "ADMIN" as const },
15
+ select: { id: true },
16
+ },
17
+ memberships: {
18
+ where: {
19
+ role: "ADMIN" as const,
20
+ status: "ACTIVE" as const,
21
+ },
22
+ select: { userId: true },
23
+ },
24
+ },
25
+ },
26
+ },
27
+ },
8
28
  user: true,
9
29
  assignmentsAttached: {
10
30
  select: {
@@ -50,12 +70,12 @@ export function findEventById(id: string) {
50
70
  });
51
71
  }
52
72
 
53
- /** @returns Class if user is a teacher. */
73
+ /** @returns Class if user is a teacher or school admin. */
54
74
  export function findClassWithTeachers(classId: string, teacherId: string) {
55
- return prisma.class.findUnique({
75
+ return prisma.class.findFirst({
56
76
  where: {
57
77
  id: classId,
58
- teachers: { some: { id: teacherId } },
78
+ ...classTeacherOrSchoolAdminWhere(teacherId),
59
79
  },
60
80
  });
61
81
  }
@@ -111,6 +131,21 @@ export function findEventWithClass(eventId: string) {
111
131
  class: {
112
132
  include: {
113
133
  teachers: { select: { id: true } },
134
+ school: {
135
+ include: {
136
+ users: {
137
+ where: { role: "ADMIN" as const },
138
+ select: { id: true },
139
+ },
140
+ memberships: {
141
+ where: {
142
+ role: "ADMIN" as const,
143
+ status: "ACTIVE" as const,
144
+ },
145
+ select: { userId: true },
146
+ },
147
+ },
148
+ },
114
149
  },
115
150
  },
116
151
  },
@@ -125,6 +160,21 @@ export function findAssignmentWithClass(assignmentId: string) {
125
160
  class: {
126
161
  include: {
127
162
  teachers: { select: { id: true } },
163
+ school: {
164
+ include: {
165
+ users: {
166
+ where: { role: "ADMIN" as const },
167
+ select: { id: true },
168
+ },
169
+ memberships: {
170
+ where: {
171
+ role: "ADMIN" as const,
172
+ status: "ACTIVE" as const,
173
+ },
174
+ select: { userId: true },
175
+ },
176
+ },
177
+ },
128
178
  },
129
179
  },
130
180
  },
@@ -3,6 +3,26 @@
3
3
  */
4
4
  import { prisma } from "../lib/prisma.js";
5
5
 
6
+ const classFileAccessInclude = {
7
+ students: true,
8
+ teachers: true,
9
+ school: {
10
+ include: {
11
+ users: {
12
+ where: { role: "ADMIN" as const },
13
+ select: { id: true },
14
+ },
15
+ memberships: {
16
+ where: {
17
+ role: "ADMIN" as const,
18
+ status: "ACTIVE" as const,
19
+ },
20
+ select: { userId: true },
21
+ },
22
+ },
23
+ },
24
+ } as const;
25
+
6
26
  /** @returns File with assignment, submission, annotations, folder, announcement for permission checks. */
7
27
  export function findFileById(fileId: string) {
8
28
  return prisma.file.findUnique({
@@ -11,10 +31,7 @@ export function findFileById(fileId: string) {
11
31
  assignment: {
12
32
  include: {
13
33
  class: {
14
- include: {
15
- students: true,
16
- teachers: true,
17
- },
34
+ include: classFileAccessInclude,
18
35
  },
19
36
  },
20
37
  },
@@ -26,6 +43,21 @@ export function findFileById(fileId: string) {
26
43
  class: {
27
44
  include: {
28
45
  teachers: true,
46
+ school: {
47
+ include: {
48
+ users: {
49
+ where: { role: "ADMIN" as const },
50
+ select: { id: true },
51
+ },
52
+ memberships: {
53
+ where: {
54
+ role: "ADMIN" as const,
55
+ status: "ACTIVE" as const,
56
+ },
57
+ select: { userId: true },
58
+ },
59
+ },
60
+ },
29
61
  },
30
62
  },
31
63
  },
@@ -40,6 +72,21 @@ export function findFileById(fileId: string) {
40
72
  class: {
41
73
  include: {
42
74
  teachers: true,
75
+ school: {
76
+ include: {
77
+ users: {
78
+ where: { role: "ADMIN" as const },
79
+ select: { id: true },
80
+ },
81
+ memberships: {
82
+ where: {
83
+ role: "ADMIN" as const,
84
+ status: "ACTIVE" as const,
85
+ },
86
+ select: { userId: true },
87
+ },
88
+ },
89
+ },
43
90
  },
44
91
  },
45
92
  },
@@ -49,20 +96,14 @@ export function findFileById(fileId: string) {
49
96
  folder: {
50
97
  include: {
51
98
  class: {
52
- include: {
53
- students: true,
54
- teachers: true,
55
- },
99
+ include: classFileAccessInclude,
56
100
  },
57
101
  },
58
102
  },
59
103
  announcement: {
60
104
  include: {
61
105
  class: {
62
- include: {
63
- students: true,
64
- teachers: true,
65
- },
106
+ include: classFileAccessInclude,
66
107
  },
67
108
  },
68
109
  },
@@ -2,6 +2,7 @@
2
2
  * Folder model – class folders, hierarchy, contents. Teacher permission checks.
3
3
  */
4
4
  import { prisma } from "../lib/prisma.js";
5
+ import { classTeacherOrSchoolAdminWhere } from "./access.js";
5
6
 
6
7
  const fileSelect = {
7
8
  id: true,
@@ -19,12 +20,12 @@ const childFolderSelect = {
19
20
  _count: { select: { files: true, childFolders: true } },
20
21
  };
21
22
 
22
- /** @returns Class if user is a teacher. */
23
+ /** @returns Class if user is a teacher or school admin. */
23
24
  export function findClassWithTeacher(classId: string, teacherId: string) {
24
25
  return prisma.class.findFirst({
25
26
  where: {
26
27
  id: classId,
27
- teachers: { some: { id: teacherId } },
28
+ ...classTeacherOrSchoolAdminWhere(teacherId),
28
29
  },
29
30
  });
30
31
  }
@@ -2,6 +2,10 @@
2
2
  * Lab chat model – AI lab conversations, members, posts. Teacher/student access.
3
3
  */
4
4
  import { prisma } from "../lib/prisma.js";
5
+ import {
6
+ classMemberOrSchoolAdminWhere,
7
+ classTeacherOrSchoolAdminWhere,
8
+ } from "./access.js";
5
9
 
6
10
  const labChatInclude = {
7
11
  conversation: {
@@ -51,7 +55,7 @@ export function findClassWithTeachersForLab(classId: string, teacherId: string)
51
55
  return prisma.class.findFirst({
52
56
  where: {
53
57
  id: classId,
54
- teachers: { some: { id: teacherId } },
58
+ ...classTeacherOrSchoolAdminWhere(teacherId),
55
59
  },
56
60
  include: {
57
61
  students: {
@@ -95,13 +99,13 @@ export function findLabChatByMember(labChatId: string, userId: string) {
95
99
  });
96
100
  }
97
101
 
98
- /** @returns Lab chat if user is class teacher. */
102
+ /** @returns Lab chat if user is class teacher or school admin. */
99
103
  export function findLabChatByTeacher(labChatId: string, userId: string) {
100
104
  return prisma.labChat.findFirst({
101
105
  where: {
102
106
  id: labChatId,
103
107
  class: {
104
- teachers: { some: { id: userId } },
108
+ ...classTeacherOrSchoolAdminWhere(userId),
105
109
  },
106
110
  },
107
111
  include: {
@@ -118,15 +122,12 @@ export function findLabChatById(labChatId: string) {
118
122
  });
119
123
  }
120
124
 
121
- /** @returns Class if user is student or teacher. */
125
+ /** @returns Class if user is student, teacher, or school admin. */
122
126
  export function findClassMembership(classId: string, userId: string) {
123
127
  return prisma.class.findFirst({
124
128
  where: {
125
129
  id: classId,
126
- OR: [
127
- { students: { some: { id: userId } } },
128
- { teachers: { some: { id: userId } } },
129
- ],
130
+ ...classMemberOrSchoolAdminWhere(userId),
130
131
  },
131
132
  });
132
133
  }