@studious-lms/server 1.7.0 → 1.8.0

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 (196) hide show
  1. package/dist/models/access.d.ts +21 -66
  2. package/dist/models/access.d.ts.map +1 -1
  3. package/dist/models/access.js +10 -26
  4. package/dist/models/access.js.map +1 -1
  5. package/dist/models/agenda.d.ts +2 -0
  6. package/dist/models/agenda.d.ts.map +1 -1
  7. package/dist/models/announcement.d.ts +2 -1
  8. package/dist/models/announcement.d.ts.map +1 -1
  9. package/dist/models/announcement.js +2 -3
  10. package/dist/models/announcement.js.map +1 -1
  11. package/dist/models/assignment.d.ts +7 -0
  12. package/dist/models/assignment.d.ts.map +1 -1
  13. package/dist/models/auth.d.ts +2 -4
  14. package/dist/models/auth.d.ts.map +1 -1
  15. package/dist/models/auth.js +2 -3
  16. package/dist/models/auth.js.map +1 -1
  17. package/dist/models/class.d.ts +32 -4
  18. package/dist/models/class.d.ts.map +1 -1
  19. package/dist/models/class.js +15 -2
  20. package/dist/models/class.js.map +1 -1
  21. package/dist/models/cloudStorage.d.ts +140 -0
  22. package/dist/models/cloudStorage.d.ts.map +1 -0
  23. package/dist/models/cloudStorage.js +175 -0
  24. package/dist/models/cloudStorage.js.map +1 -0
  25. package/dist/models/comment.d.ts +0 -24
  26. package/dist/models/comment.d.ts.map +1 -1
  27. package/dist/models/comment.js +2 -6
  28. package/dist/models/comment.js.map +1 -1
  29. package/dist/models/conversation.d.ts +22 -22
  30. package/dist/models/department.d.ts +154 -0
  31. package/dist/models/department.d.ts.map +1 -0
  32. package/dist/models/department.js +135 -0
  33. package/dist/models/department.js.map +1 -0
  34. package/dist/models/event.d.ts +7 -1
  35. package/dist/models/event.d.ts.map +1 -1
  36. package/dist/models/event.js +2 -5
  37. package/dist/models/event.js.map +1 -1
  38. package/dist/models/file.d.ts +25 -10
  39. package/dist/models/file.d.ts.map +1 -1
  40. package/dist/models/file.js +2 -5
  41. package/dist/models/file.js.map +1 -1
  42. package/dist/models/folder.d.ts +47 -0
  43. package/dist/models/folder.d.ts.map +1 -1
  44. package/dist/models/labChat.d.ts +5 -3
  45. package/dist/models/labChat.d.ts.map +1 -1
  46. package/dist/models/message.d.ts +46 -12
  47. package/dist/models/message.d.ts.map +1 -1
  48. package/dist/models/message.js +19 -10
  49. package/dist/models/message.js.map +1 -1
  50. package/dist/models/newtonChat.d.ts +2 -1
  51. package/dist/models/newtonChat.d.ts.map +1 -1
  52. package/dist/models/school.d.ts +45 -12
  53. package/dist/models/school.d.ts.map +1 -1
  54. package/dist/models/school.js +19 -2
  55. package/dist/models/school.js.map +1 -1
  56. package/dist/models/section.d.ts +1 -0
  57. package/dist/models/section.d.ts.map +1 -1
  58. package/dist/models/tag.d.ts +117 -0
  59. package/dist/models/tag.d.ts.map +1 -0
  60. package/dist/models/tag.js +113 -0
  61. package/dist/models/tag.js.map +1 -0
  62. package/dist/models/worksheet.d.ts +3 -0
  63. package/dist/models/worksheet.d.ts.map +1 -1
  64. package/dist/pipelines/aiLabChatContract.d.ts +8 -8
  65. package/dist/routers/_app.d.ts +3200 -1972
  66. package/dist/routers/_app.d.ts.map +1 -1
  67. package/dist/routers/_app.js +6 -2
  68. package/dist/routers/_app.js.map +1 -1
  69. package/dist/routers/agenda.d.ts +2 -0
  70. package/dist/routers/agenda.d.ts.map +1 -1
  71. package/dist/routers/announcement.d.ts.map +1 -1
  72. package/dist/routers/announcement.js +3 -5
  73. package/dist/routers/announcement.js.map +1 -1
  74. package/dist/routers/assignment.d.ts +18 -9
  75. package/dist/routers/assignment.d.ts.map +1 -1
  76. package/dist/routers/assignment.js +2 -3
  77. package/dist/routers/assignment.js.map +1 -1
  78. package/dist/routers/class.d.ts +87 -14
  79. package/dist/routers/class.d.ts.map +1 -1
  80. package/dist/routers/class.js +5 -2
  81. package/dist/routers/class.js.map +1 -1
  82. package/dist/routers/conversation.d.ts +5 -5
  83. package/dist/routers/department.d.ts +293 -0
  84. package/dist/routers/department.d.ts.map +1 -0
  85. package/dist/routers/department.js +93 -0
  86. package/dist/routers/department.js.map +1 -0
  87. package/dist/routers/event.d.ts +2 -1
  88. package/dist/routers/event.d.ts.map +1 -1
  89. package/dist/routers/file.d.ts +8 -6
  90. package/dist/routers/file.d.ts.map +1 -1
  91. package/dist/routers/file.js +26 -17
  92. package/dist/routers/file.js.map +1 -1
  93. package/dist/routers/folder.d.ts +139 -186
  94. package/dist/routers/folder.d.ts.map +1 -1
  95. package/dist/routers/folder.js +82 -90
  96. package/dist/routers/folder.js.map +1 -1
  97. package/dist/routers/labChat.d.ts +11 -2
  98. package/dist/routers/labChat.d.ts.map +1 -1
  99. package/dist/routers/message.d.ts +20 -2
  100. package/dist/routers/message.d.ts.map +1 -1
  101. package/dist/routers/newtonChat.d.ts +9 -0
  102. package/dist/routers/newtonChat.d.ts.map +1 -1
  103. package/dist/routers/school.d.ts +160 -17
  104. package/dist/routers/school.d.ts.map +1 -1
  105. package/dist/routers/school.js +46 -3
  106. package/dist/routers/school.js.map +1 -1
  107. package/dist/routers/tag.d.ts +119 -0
  108. package/dist/routers/tag.d.ts.map +1 -0
  109. package/dist/routers/tag.js +52 -0
  110. package/dist/routers/tag.js.map +1 -0
  111. package/dist/routers/worksheet.d.ts +1 -0
  112. package/dist/routers/worksheet.d.ts.map +1 -1
  113. package/dist/seedDatabase.d.ts +0 -1
  114. package/dist/seedDatabase.d.ts.map +1 -1
  115. package/dist/services/agenda.d.ts +2 -0
  116. package/dist/services/agenda.d.ts.map +1 -1
  117. package/dist/services/assignment.d.ts +18 -9
  118. package/dist/services/assignment.d.ts.map +1 -1
  119. package/dist/services/class.d.ts +87 -14
  120. package/dist/services/class.d.ts.map +1 -1
  121. package/dist/services/class.js +91 -4
  122. package/dist/services/class.js.map +1 -1
  123. package/dist/services/conversation.d.ts +5 -5
  124. package/dist/services/department.d.ts +200 -0
  125. package/dist/services/department.d.ts.map +1 -0
  126. package/dist/services/department.js +368 -0
  127. package/dist/services/department.js.map +1 -0
  128. package/dist/services/event.d.ts +2 -1
  129. package/dist/services/event.d.ts.map +1 -1
  130. package/dist/services/file.d.ts +85 -0
  131. package/dist/services/file.d.ts.map +1 -1
  132. package/dist/services/file.js +92 -2
  133. package/dist/services/file.js.map +1 -1
  134. package/dist/services/folder.d.ts +125 -149
  135. package/dist/services/folder.d.ts.map +1 -1
  136. package/dist/services/folder.js +258 -199
  137. package/dist/services/folder.js.map +1 -1
  138. package/dist/services/labChat.d.ts +11 -2
  139. package/dist/services/labChat.d.ts.map +1 -1
  140. package/dist/services/labChat.js +4 -3
  141. package/dist/services/labChat.js.map +1 -1
  142. package/dist/services/message.d.ts +20 -2
  143. package/dist/services/message.d.ts.map +1 -1
  144. package/dist/services/message.js +7 -23
  145. package/dist/services/message.js.map +1 -1
  146. package/dist/services/newtonChat.d.ts +9 -0
  147. package/dist/services/newtonChat.d.ts.map +1 -1
  148. package/dist/services/newtonChat.js +4 -3
  149. package/dist/services/newtonChat.js.map +1 -1
  150. package/dist/services/school.d.ts +142 -15
  151. package/dist/services/school.d.ts.map +1 -1
  152. package/dist/services/school.js +308 -54
  153. package/dist/services/school.js.map +1 -1
  154. package/dist/services/tag.d.ts +71 -0
  155. package/dist/services/tag.d.ts.map +1 -0
  156. package/dist/services/tag.js +178 -0
  157. package/dist/services/tag.js.map +1 -0
  158. package/dist/services/worksheet.d.ts +1 -0
  159. package/dist/services/worksheet.d.ts.map +1 -1
  160. package/dist/utils/aiUser.d.ts.map +1 -1
  161. package/dist/utils/aiUser.js +2 -3
  162. package/dist/utils/aiUser.js.map +1 -1
  163. package/package.json +1 -1
  164. package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +184 -0
  165. package/prisma/schema.prisma +119 -35
  166. package/src/models/access.ts +8 -24
  167. package/src/models/announcement.ts +0 -1
  168. package/src/models/auth.ts +0 -1
  169. package/src/models/class.ts +13 -0
  170. package/src/models/cloudStorage.ts +194 -0
  171. package/src/models/comment.ts +0 -4
  172. package/src/models/department.ts +159 -0
  173. package/src/models/event.ts +0 -3
  174. package/src/models/file.ts +0 -3
  175. package/src/models/message.ts +19 -5
  176. package/src/models/school.ts +26 -0
  177. package/src/models/tag.ts +140 -0
  178. package/src/routers/_app.ts +4 -0
  179. package/src/routers/announcement.ts +1 -3
  180. package/src/routers/assignment.ts +0 -1
  181. package/src/routers/class.ts +3 -0
  182. package/src/routers/department.ts +174 -0
  183. package/src/routers/file.ts +29 -26
  184. package/src/routers/folder.ts +100 -121
  185. package/src/routers/school.ts +83 -0
  186. package/src/routers/tag.ts +78 -0
  187. package/src/services/class.ts +108 -3
  188. package/src/services/department.ts +478 -0
  189. package/src/services/file.ts +100 -0
  190. package/src/services/folder.ts +313 -240
  191. package/src/services/labChat.ts +2 -1
  192. package/src/services/message.ts +5 -21
  193. package/src/services/newtonChat.ts +2 -1
  194. package/src/services/school.ts +369 -57
  195. package/src/services/tag.ts +229 -0
  196. package/src/utils/aiUser.ts +0 -1
@@ -111,18 +111,70 @@ enum SchoolInviteStatus {
111
111
  }
112
112
 
113
113
  model School {
114
- id String @id @default(uuid())
115
- name String
116
- logo File? @relation(references: [id], fields: [logoId])
117
- logoId String?
118
- subdomain String? @unique
119
- createdAt DateTime @default(now())
120
- updatedAt DateTime @updatedAt
121
-
122
- users User[]
123
- memberships SchoolMembership[]
124
- invites SchoolInvite[]
125
- classes Class[]
114
+ id String @id @default(uuid())
115
+ name String
116
+ description String?
117
+ logo File? @relation("SchoolLogo", references: [id], fields: [logoId])
118
+ logoId String?
119
+ subdomain String? @unique
120
+ createdAt DateTime @default(now())
121
+ updatedAt DateTime @updatedAt
122
+
123
+ users User[]
124
+ memberships SchoolMembership[]
125
+ invites SchoolInvite[]
126
+ classes Class[]
127
+ departments Department[]
128
+ tags Tag[]
129
+ storageRoot Folder? @relation("SchoolStorageRoot")
130
+ }
131
+
132
+ model Department {
133
+ id String @id @default(uuid())
134
+ schoolId String
135
+ name String
136
+ description String?
137
+ color String? @default("#3B82F6")
138
+ createdAt DateTime @default(now())
139
+ updatedAt DateTime @updatedAt
140
+
141
+ school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
142
+ members User[] @relation("UserDepartments")
143
+ classes Class[]
144
+ tags Tag[]
145
+ rubricTemplates MarkScheme[]
146
+ boundaryTemplates GradingBoundary[]
147
+
148
+ @@unique([schoolId, name])
149
+ @@index([schoolId])
150
+ }
151
+
152
+ enum TagTarget {
153
+ USER
154
+ CLASS
155
+ RUBRIC
156
+ BOUNDARY
157
+ }
158
+
159
+ model Tag {
160
+ id String @id @default(uuid())
161
+ schoolId String
162
+ departmentId String?
163
+ name String
164
+ color String @default("#3B82F6")
165
+ createdAt DateTime @default(now())
166
+ updatedAt DateTime @updatedAt
167
+
168
+ school School @relation(fields: [schoolId], references: [id], onDelete: Cascade)
169
+ department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull)
170
+ users User[] @relation("UserTags")
171
+ classes Class[] @relation("ClassTags")
172
+ rubrics MarkScheme[] @relation("MarkSchemeTags")
173
+ boundaries GradingBoundary[] @relation("GradingBoundaryTags")
174
+
175
+ @@unique([schoolId, name])
176
+ @@index([schoolId])
177
+ @@index([departmentId])
126
178
  }
127
179
 
128
180
  model User {
@@ -132,7 +184,6 @@ model User {
132
184
  password String
133
185
  profile UserProfile?
134
186
  verified Boolean @default(false)
135
- role UserRole @default(NONE)
136
187
 
137
188
  teacherIn Class[] @relation("UserTeacherToClass")
138
189
  studentIn Class[] @relation("UserStudentToClass")
@@ -158,6 +209,10 @@ model User {
158
209
  schoolMemberships SchoolMembership[]
159
210
  sentSchoolInvites SchoolInvite[] @relation("SchoolInviteInvitedBy")
160
211
 
212
+ departments Department[] @relation("UserDepartments")
213
+ tags Tag[] @relation("UserTags")
214
+ personalStorage Folder? @relation("UserStorageRoot")
215
+
161
216
  // Chat relations
162
217
  conversationMemberships ConversationMember[]
163
218
  sentMessages Message[] @relation("SentMessages")
@@ -238,8 +293,6 @@ model Class {
238
293
  worksheets Worksheet[]
239
294
  students User[] @relation("UserStudentToClass")
240
295
  teachers User[] @relation("UserTeacherToClass")
241
- // markSchemes MarkScheme[] @relation("ClassToMarkScheme")
242
- // gradingBoundaries GradingBoundary[] @relation("ClassToGradingBoundary")
243
296
  draftFiles File[] @relation("ClassDraftFiles")
244
297
  classFiles Folder? @relation("ClassFiles")
245
298
  labChats LabChat[] @relation("ClassLabChats")
@@ -247,36 +300,67 @@ model Class {
247
300
  gradingBoundaries GradingBoundary[]
248
301
  school School? @relation(fields: [schoolId], references: [id])
249
302
  schoolId String?
303
+ department Department? @relation(fields: [departmentId], references: [id], onDelete: SetNull)
304
+ departmentId String?
305
+ tags Tag[] @relation("ClassTags")
306
+
307
+ @@index([schoolId])
308
+ @@index([departmentId])
250
309
  }
251
310
 
252
311
  model MarkScheme {
253
- id String @id @default(uuid())
254
- classId String
255
- // class Class[] @relation("ClassToMarkScheme")
256
- class Class? @relation(fields: [classId], references: [id], onDelete: Cascade)
257
- structured String
258
- assignments Assignment[]
312
+ id String @id @default(uuid())
313
+ classId String?
314
+ class Class? @relation(fields: [classId], references: [id], onDelete: Cascade)
315
+ departmentId String?
316
+ department Department? @relation(fields: [departmentId], references: [id], onDelete: Cascade)
317
+ name String?
318
+ structured String
319
+ assignments Assignment[]
320
+ tags Tag[] @relation("MarkSchemeTags")
321
+
322
+ @@index([classId])
323
+ @@index([departmentId])
259
324
  }
260
325
 
261
326
  model GradingBoundary {
262
- id String @id @default(uuid())
263
- classId String
264
- // class Class[] @relation("ClassToGradingBoundary")
265
- class Class? @relation(fields: [classId], references: [id], onDelete: Cascade)
266
- structured String
267
- assignments Assignment[]
327
+ id String @id @default(uuid())
328
+ classId String?
329
+ class Class? @relation(fields: [classId], references: [id], onDelete: Cascade)
330
+ departmentId String?
331
+ department Department? @relation(fields: [departmentId], references: [id], onDelete: Cascade)
332
+ name String?
333
+ structured String
334
+ assignments Assignment[]
335
+ tags Tag[] @relation("GradingBoundaryTags")
336
+
337
+ @@index([classId])
338
+ @@index([departmentId])
339
+ }
340
+
341
+ enum FolderScope {
342
+ CLASS
343
+ SCHOOL_STORAGE
344
+ PERSONAL
345
+ GENERIC
268
346
  }
269
347
 
270
348
  model Folder {
271
- id String @id @default(uuid())
349
+ id String @id @default(uuid())
272
350
  name String
273
- files File[] @relation("FolderFiles")
274
- childFolders Folder[] @relation("ParentChildFolders")
275
- parentFolder Folder? @relation("ParentChildFolders", fields: [parentFolderId], references: [id])
351
+ scope FolderScope @default(GENERIC)
352
+ files File[] @relation("FolderFiles")
353
+ childFolders Folder[] @relation("ParentChildFolders")
354
+ parentFolder Folder? @relation("ParentChildFolders", fields: [parentFolderId], references: [id])
276
355
  parentFolderId String?
277
- color String? @default("#3B82F6")
278
- class Class? @relation("ClassFiles", fields: [classId], references: [id])
279
- classId String? @unique
356
+ color String? @default("#3B82F6")
357
+
358
+ class Class? @relation("ClassFiles", fields: [classId], references: [id])
359
+ classId String? @unique
360
+ schoolStorage School? @relation("SchoolStorageRoot", fields: [schoolStorageId], references: [id], onDelete: Cascade)
361
+ schoolStorageId String? @unique
362
+ personalOwner User? @relation("UserStorageRoot", fields: [personalOwnerId], references: [id], onDelete: Cascade)
363
+ personalOwnerId String? @unique
280
364
  }
281
365
 
282
366
  model File {
@@ -328,7 +412,7 @@ model File {
328
412
  announcement Announcement? @relation("AnnouncementAttachments", fields: [announcementId], references: [id], onDelete: Cascade)
329
413
  announcementId String?
330
414
 
331
- schools School[]
415
+ schools School[] @relation("SchoolLogo")
332
416
 
333
417
  schoolDevelopementProgram SchoolDevelopementProgram? @relation("SchoolDevelopementProgramSupportingDocumentation", fields: [schoolDevelopementProgramId], references: [id], onDelete: Cascade)
334
418
  schoolDevelopementProgramId String?
@@ -3,33 +3,17 @@
3
3
  */
4
4
 
5
5
  export const schoolAdminForClassWhere = (userId: string) => ({
6
- OR: [
7
- {
8
- school: {
9
- is: {
10
- memberships: {
11
- some: {
12
- userId,
13
- role: "ADMIN" as const,
14
- status: "ACTIVE" as const,
15
- },
16
- },
6
+ school: {
7
+ is: {
8
+ memberships: {
9
+ some: {
10
+ userId,
11
+ role: "ADMIN" as const,
12
+ status: "ACTIVE" as const,
17
13
  },
18
14
  },
19
15
  },
20
- {
21
- school: {
22
- is: {
23
- users: {
24
- some: {
25
- id: userId,
26
- role: "ADMIN" as const,
27
- },
28
- },
29
- },
30
- },
31
- },
32
- ],
16
+ },
33
17
  });
34
18
 
35
19
  export const classTeacherOrSchoolAdminWhere = (userId: string) => ({
@@ -67,7 +67,6 @@ export function findAnnouncementWithClass(id: string) {
67
67
  school: {
68
68
  include: {
69
69
  users: {
70
- where: { role: "ADMIN" as const },
71
70
  select: { id: true },
72
71
  },
73
72
  memberships: {
@@ -61,7 +61,6 @@ export function findUserByEmail(email: string) {
61
61
  select: {
62
62
  id: true,
63
63
  email: true,
64
- role: true,
65
64
  profile: {
66
65
  select: {
67
66
  displayName: true,
@@ -54,6 +54,19 @@ const classIncludeForGetAll = {
54
54
  },
55
55
  },
56
56
  },
57
+ school: {
58
+ select: {
59
+ id: true,
60
+ name: true,
61
+ },
62
+ },
63
+ department: {
64
+ select: {
65
+ id: true,
66
+ name: true,
67
+ color: true,
68
+ },
69
+ },
57
70
  } as const;
58
71
 
59
72
  /** @returns Classes where user is a teacher. */
@@ -0,0 +1,194 @@
1
+ /**
2
+ * CloudStorage model — scope-aware folder helpers used by the institution and
3
+ * personal cloud storage UI. Sits on top of the same Folder/File tables that
4
+ * back class folders, but adds scope (school/personal) and ownership semantics.
5
+ */
6
+ import type { FolderScope } from "@prisma/client";
7
+ import { prisma } from "../lib/prisma.js";
8
+
9
+ export type CloudScope =
10
+ | { kind: "CLASS"; classId: string }
11
+ | { kind: "SCHOOL_STORAGE"; schoolId: string }
12
+ | { kind: "PERSONAL"; userId: string };
13
+
14
+ const folderSelect = {
15
+ id: true,
16
+ name: true,
17
+ color: true,
18
+ scope: true,
19
+ classId: true,
20
+ schoolStorageId: true,
21
+ personalOwnerId: true,
22
+ parentFolderId: true,
23
+ } as const;
24
+
25
+ export async function findOrCreateScopeRoot(scope: CloudScope) {
26
+ switch (scope.kind) {
27
+ case "CLASS": {
28
+ let root = await prisma.folder.findFirst({
29
+ where: { classId: scope.classId, parentFolderId: null },
30
+ });
31
+ if (!root) {
32
+ root = await prisma.folder.create({
33
+ data: {
34
+ name: "Class Files",
35
+ scope: "CLASS" as FolderScope,
36
+ class: { connect: { id: scope.classId } },
37
+ },
38
+ });
39
+ } else if (root.scope !== "CLASS") {
40
+ root = await prisma.folder.update({
41
+ where: { id: root.id },
42
+ data: { scope: "CLASS" as FolderScope },
43
+ });
44
+ }
45
+ return root;
46
+ }
47
+ case "SCHOOL_STORAGE": {
48
+ let root = await prisma.folder.findFirst({
49
+ where: { schoolStorageId: scope.schoolId },
50
+ });
51
+ if (!root) {
52
+ root = await prisma.folder.create({
53
+ data: {
54
+ name: "Cloud Storage",
55
+ scope: "SCHOOL_STORAGE" as FolderScope,
56
+ schoolStorage: { connect: { id: scope.schoolId } },
57
+ },
58
+ });
59
+ }
60
+ return root;
61
+ }
62
+ case "PERSONAL": {
63
+ let root = await prisma.folder.findFirst({
64
+ where: { personalOwnerId: scope.userId },
65
+ });
66
+ if (!root) {
67
+ root = await prisma.folder.create({
68
+ data: {
69
+ name: "My Files",
70
+ scope: "PERSONAL" as FolderScope,
71
+ personalOwner: { connect: { id: scope.userId } },
72
+ },
73
+ });
74
+ }
75
+ return root;
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Walk up the folder tree until we reach a root with a scope owner. Used to
82
+ * permission-check sub-folders (which only carry a parentFolderId).
83
+ */
84
+ export async function resolveFolderScope(
85
+ folderId: string
86
+ ): Promise<CloudScope | null> {
87
+ let currentId: string | null = folderId;
88
+ let visits = 0;
89
+ while (currentId && visits < 64) {
90
+ const folder = await prisma.folder.findUnique({
91
+ where: { id: currentId },
92
+ select: folderSelect,
93
+ });
94
+ if (!folder) return null;
95
+ if (folder.classId) return { kind: "CLASS", classId: folder.classId };
96
+ if (folder.schoolStorageId)
97
+ return { kind: "SCHOOL_STORAGE", schoolId: folder.schoolStorageId };
98
+ if (folder.personalOwnerId)
99
+ return { kind: "PERSONAL", userId: folder.personalOwnerId };
100
+ if (!folder.parentFolderId) return null;
101
+ currentId = folder.parentFolderId;
102
+ visits += 1;
103
+ }
104
+ return null;
105
+ }
106
+
107
+ const fileSelect = {
108
+ id: true,
109
+ name: true,
110
+ type: true,
111
+ size: true,
112
+ path: true,
113
+ uploadedAt: true,
114
+ user: { select: { id: true, username: true } },
115
+ } as const;
116
+
117
+ const childFolderSelect = {
118
+ id: true,
119
+ name: true,
120
+ color: true,
121
+ _count: { select: { files: true, childFolders: true } },
122
+ } as const;
123
+
124
+ export function findFolderWithContents(folderId: string) {
125
+ return prisma.folder.findFirst({
126
+ where: { id: folderId },
127
+ include: {
128
+ files: {
129
+ where: { uploadStatus: "COMPLETED" },
130
+ select: fileSelect,
131
+ orderBy: { uploadedAt: "desc" },
132
+ },
133
+ childFolders: {
134
+ select: childFolderSelect,
135
+ orderBy: { name: "asc" },
136
+ },
137
+ parentFolder: { select: { id: true, name: true } },
138
+ },
139
+ });
140
+ }
141
+
142
+ export function createSubFolder(data: {
143
+ name: string;
144
+ parentFolderId: string;
145
+ color?: string;
146
+ }) {
147
+ return prisma.folder.create({
148
+ data: {
149
+ name: data.name,
150
+ color: data.color,
151
+ parentFolder: { connect: { id: data.parentFolderId } },
152
+ },
153
+ select: folderSelect,
154
+ });
155
+ }
156
+
157
+ export function findFolderById(folderId: string) {
158
+ return prisma.folder.findUnique({
159
+ where: { id: folderId },
160
+ select: folderSelect,
161
+ });
162
+ }
163
+
164
+ export function deleteFolderById(folderId: string) {
165
+ return prisma.folder.delete({ where: { id: folderId } });
166
+ }
167
+
168
+ export function renameFolder(folderId: string, name: string, color?: string) {
169
+ return prisma.folder.update({
170
+ where: { id: folderId },
171
+ data: {
172
+ name,
173
+ ...(color ? { color } : {}),
174
+ },
175
+ select: folderSelect,
176
+ });
177
+ }
178
+
179
+ export async function listFolderBreadcrumbs(folderId: string) {
180
+ const breadcrumbs: { id: string; name: string }[] = [];
181
+ let currentId: string | null = folderId;
182
+ let visits = 0;
183
+ while (currentId && visits < 64) {
184
+ const folder = await prisma.folder.findUnique({
185
+ where: { id: currentId },
186
+ select: { id: true, name: true, parentFolderId: true },
187
+ });
188
+ if (!folder) break;
189
+ breadcrumbs.unshift({ id: folder.id, name: folder.name });
190
+ currentId = folder.parentFolderId;
191
+ visits += 1;
192
+ }
193
+ return breadcrumbs;
194
+ }
@@ -52,10 +52,6 @@ const commentClassAuthSelect = {
52
52
  students: { select: { id: true } },
53
53
  school: {
54
54
  select: {
55
- users: {
56
- where: { role: "ADMIN" as const },
57
- select: { id: true },
58
- },
59
55
  memberships: {
60
56
  where: {
61
57
  role: "ADMIN" as const,
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Department model — department CRUD, members, and template artefacts.
3
+ *
4
+ * Departments scope rubric/boundary templates and tags inside an institution.
5
+ * They are an admin-managed taxonomy; teachers/students can be members of
6
+ * many departments.
7
+ */
8
+ import { prisma } from "../lib/prisma.js";
9
+
10
+ const departmentSelect = {
11
+ id: true,
12
+ schoolId: true,
13
+ name: true,
14
+ description: true,
15
+ color: true,
16
+ createdAt: true,
17
+ updatedAt: true,
18
+ } as const;
19
+
20
+ const memberSelect = {
21
+ id: true,
22
+ username: true,
23
+ email: true,
24
+ profile: {
25
+ select: {
26
+ displayName: true,
27
+ profilePicture: true,
28
+ profilePictureThumbnail: true,
29
+ },
30
+ },
31
+ } as const;
32
+
33
+ export function listDepartmentsForSchool(schoolId: string) {
34
+ return prisma.department.findMany({
35
+ where: { schoolId },
36
+ orderBy: { name: "asc" },
37
+ select: {
38
+ ...departmentSelect,
39
+ _count: {
40
+ select: {
41
+ members: true,
42
+ classes: true,
43
+ tags: true,
44
+ rubricTemplates: true,
45
+ boundaryTemplates: true,
46
+ },
47
+ },
48
+ },
49
+ });
50
+ }
51
+
52
+ export function listDepartmentsForUser(userId: string, schoolId: string) {
53
+ return prisma.department.findMany({
54
+ where: {
55
+ schoolId,
56
+ members: { some: { id: userId } },
57
+ },
58
+ orderBy: { name: "asc" },
59
+ select: departmentSelect,
60
+ });
61
+ }
62
+
63
+ export function getDepartment(departmentId: string) {
64
+ return prisma.department.findUnique({
65
+ where: { id: departmentId },
66
+ select: {
67
+ ...departmentSelect,
68
+ members: { select: memberSelect },
69
+ tags: {
70
+ select: { id: true, name: true, color: true },
71
+ },
72
+ rubricTemplates: {
73
+ select: { id: true, name: true, structured: true },
74
+ },
75
+ boundaryTemplates: {
76
+ select: { id: true, name: true, structured: true },
77
+ },
78
+ classes: {
79
+ select: {
80
+ id: true,
81
+ name: true,
82
+ subject: true,
83
+ section: true,
84
+ color: true,
85
+ },
86
+ },
87
+ },
88
+ });
89
+ }
90
+
91
+ export function createDepartment(data: {
92
+ schoolId: string;
93
+ name: string;
94
+ description?: string;
95
+ color?: string;
96
+ }) {
97
+ return prisma.department.create({
98
+ data: {
99
+ schoolId: data.schoolId,
100
+ name: data.name,
101
+ description: data.description,
102
+ color: data.color,
103
+ },
104
+ select: departmentSelect,
105
+ });
106
+ }
107
+
108
+ export function updateDepartment(
109
+ departmentId: string,
110
+ data: { name?: string; description?: string | null; color?: string }
111
+ ) {
112
+ return prisma.department.update({
113
+ where: { id: departmentId },
114
+ data,
115
+ select: departmentSelect,
116
+ });
117
+ }
118
+
119
+ export function deleteDepartment(departmentId: string) {
120
+ return prisma.department.delete({
121
+ where: { id: departmentId },
122
+ });
123
+ }
124
+
125
+ export function addDepartmentMembers(
126
+ departmentId: string,
127
+ userIds: string[]
128
+ ) {
129
+ return prisma.department.update({
130
+ where: { id: departmentId },
131
+ data: {
132
+ members: { connect: userIds.map((id) => ({ id })) },
133
+ },
134
+ select: { id: true },
135
+ });
136
+ }
137
+
138
+ export function removeDepartmentMember(
139
+ departmentId: string,
140
+ userId: string
141
+ ) {
142
+ return prisma.department.update({
143
+ where: { id: departmentId },
144
+ data: {
145
+ members: { disconnect: { id: userId } },
146
+ },
147
+ select: { id: true },
148
+ });
149
+ }
150
+
151
+ export function findDepartmentMembership(
152
+ departmentId: string,
153
+ userId: string
154
+ ) {
155
+ return prisma.department.findFirst({
156
+ where: { id: departmentId, members: { some: { id: userId } } },
157
+ select: { id: true },
158
+ });
159
+ }
@@ -11,7 +11,6 @@ const eventGetInclude = {
11
11
  school: {
12
12
  include: {
13
13
  users: {
14
- where: { role: "ADMIN" as const },
15
14
  select: { id: true },
16
15
  },
17
16
  memberships: {
@@ -134,7 +133,6 @@ export function findEventWithClass(eventId: string) {
134
133
  school: {
135
134
  include: {
136
135
  users: {
137
- where: { role: "ADMIN" as const },
138
136
  select: { id: true },
139
137
  },
140
138
  memberships: {
@@ -163,7 +161,6 @@ export function findAssignmentWithClass(assignmentId: string) {
163
161
  school: {
164
162
  include: {
165
163
  users: {
166
- where: { role: "ADMIN" as const },
167
164
  select: { id: true },
168
165
  },
169
166
  memberships: {
@@ -9,7 +9,6 @@ const classFileAccessInclude = {
9
9
  school: {
10
10
  include: {
11
11
  users: {
12
- where: { role: "ADMIN" as const },
13
12
  select: { id: true },
14
13
  },
15
14
  memberships: {
@@ -46,7 +45,6 @@ export function findFileById(fileId: string) {
46
45
  school: {
47
46
  include: {
48
47
  users: {
49
- where: { role: "ADMIN" as const },
50
48
  select: { id: true },
51
49
  },
52
50
  memberships: {
@@ -75,7 +73,6 @@ export function findFileById(fileId: string) {
75
73
  school: {
76
74
  include: {
77
75
  users: {
78
- where: { role: "ADMIN" as const },
79
76
  select: { id: true },
80
77
  },
81
78
  memberships: {