@studious-lms/server 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API_SPECIFICATION.md +1117 -0
- package/dist/exportType.js +1 -2
- package/dist/index.js +25 -30
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +31 -29
- package/dist/lib/googleCloudStorage.js +9 -14
- package/dist/lib/prisma.js +4 -7
- package/dist/lib/thumbnailGenerator.js +12 -20
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +17 -22
- package/dist/middleware/logging.js +5 -9
- package/dist/routers/_app.d.ts +3483 -1801
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +28 -27
- package/dist/routers/agenda.d.ts +13 -8
- package/dist/routers/agenda.d.ts.map +1 -1
- package/dist/routers/agenda.js +14 -17
- package/dist/routers/announcement.d.ts +4 -3
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +28 -31
- package/dist/routers/assignment.d.ts +282 -196
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +256 -202
- package/dist/routers/attendance.d.ts +5 -4
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +31 -34
- package/dist/routers/auth.d.ts +1 -0
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +80 -75
- package/dist/routers/class.d.ts +284 -14
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +435 -164
- package/dist/routers/event.d.ts +47 -38
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/event.js +76 -79
- package/dist/routers/file.d.ts +71 -1
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +267 -32
- package/dist/routers/folder.d.ts +296 -0
- package/dist/routers/folder.d.ts.map +1 -0
- package/dist/routers/folder.js +693 -0
- package/dist/routers/notifications.d.ts +103 -0
- package/dist/routers/notifications.d.ts.map +1 -0
- package/dist/routers/notifications.js +91 -0
- package/dist/routers/school.d.ts +208 -0
- package/dist/routers/school.d.ts.map +1 -0
- package/dist/routers/school.js +481 -0
- package/dist/routers/section.d.ts +1 -0
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +30 -33
- package/dist/routers/user.d.ts +2 -1
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +21 -24
- package/dist/seedDatabase.d.ts +22 -0
- package/dist/seedDatabase.d.ts.map +1 -0
- package/dist/seedDatabase.js +57 -0
- package/dist/socket/handlers.js +26 -30
- package/dist/trpc.d.ts +5 -0
- package/dist/trpc.d.ts.map +1 -1
- package/dist/trpc.js +35 -26
- package/dist/types/trpc.js +1 -2
- package/dist/utils/email.js +2 -8
- package/dist/utils/generateInviteCode.js +1 -5
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +13 -9
- package/dist/utils/prismaErrorHandler.d.ts +9 -0
- package/dist/utils/prismaErrorHandler.d.ts.map +1 -0
- package/dist/utils/prismaErrorHandler.js +234 -0
- package/dist/utils/prismaWrapper.d.ts +14 -0
- package/dist/utils/prismaWrapper.d.ts.map +1 -0
- package/dist/utils/prismaWrapper.js +64 -0
- package/package.json +11 -4
- package/prisma/migrations/20250807062924_init/migration.sql +436 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +67 -0
- package/src/index.ts +2 -2
- package/src/lib/fileUpload.ts +16 -7
- package/src/middleware/auth.ts +0 -2
- package/src/routers/_app.ts +5 -1
- package/src/routers/assignment.ts +82 -22
- package/src/routers/auth.ts +80 -54
- package/src/routers/class.ts +330 -36
- package/src/routers/file.ts +283 -20
- package/src/routers/folder.ts +755 -0
- package/src/routers/notifications.ts +93 -0
- package/src/seedDatabase.ts +66 -0
- package/src/socket/handlers.ts +4 -4
- package/src/trpc.ts +13 -0
- package/src/utils/logger.ts +14 -4
- package/src/utils/prismaErrorHandler.ts +275 -0
- package/src/utils/prismaWrapper.ts +91 -0
- package/tests/auth.test.ts +25 -0
- package/tests/class.test.ts +281 -0
- package/tests/setup.ts +98 -0
- package/tests/startup.test.ts +5 -0
- package/tsconfig.json +2 -1
- package/vitest.config.ts +11 -0
- package/dist/logger.d.ts +0 -26
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js +0 -135
- package/src/logger.ts +0 -163
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createTRPCRouter, protectedClassMemberProcedure, protectedTeacherProcedure } from "../trpc";
|
|
3
|
+
import { TRPCError } from "@trpc/server";
|
|
4
|
+
import { prisma } from "../lib/prisma";
|
|
5
|
+
import { uploadFiles } from "../lib/fileUpload";
|
|
6
|
+
const fileSchema = z.object({
|
|
7
|
+
name: z.string(),
|
|
8
|
+
type: z.string(),
|
|
9
|
+
size: z.number(),
|
|
10
|
+
data: z.string(), // base64 encoded file data
|
|
11
|
+
});
|
|
12
|
+
const createFolderSchema = z.object({
|
|
13
|
+
name: z.string(),
|
|
14
|
+
parentFolderId: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
const uploadFilesToFolderSchema = z.object({
|
|
17
|
+
folderId: z.string(),
|
|
18
|
+
files: z.array(fileSchema),
|
|
19
|
+
});
|
|
20
|
+
const getRootFolderSchema = z.object({
|
|
21
|
+
classId: z.string(),
|
|
22
|
+
});
|
|
23
|
+
export const folderRouter = createTRPCRouter({
|
|
24
|
+
create: protectedTeacherProcedure
|
|
25
|
+
.input(createFolderSchema)
|
|
26
|
+
.mutation(async ({ ctx, input }) => {
|
|
27
|
+
const { classId, name } = input;
|
|
28
|
+
let parentFolderId = input.parentFolderId || null;
|
|
29
|
+
if (!ctx.user) {
|
|
30
|
+
throw new TRPCError({
|
|
31
|
+
code: "UNAUTHORIZED",
|
|
32
|
+
message: "You must be logged in to create a folder",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
// Verify user is a teacher of the class
|
|
36
|
+
const classData = await prisma.class.findFirst({
|
|
37
|
+
where: {
|
|
38
|
+
id: classId,
|
|
39
|
+
teachers: {
|
|
40
|
+
some: {
|
|
41
|
+
id: ctx.user.id,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if (!classData) {
|
|
47
|
+
throw new TRPCError({
|
|
48
|
+
code: "NOT_FOUND",
|
|
49
|
+
message: "Class not found or you are not a teacher",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// If no parent folder specified, find or create the class parent folder
|
|
53
|
+
if (!parentFolderId) {
|
|
54
|
+
let classParentFolder = await prisma.folder.findFirst({
|
|
55
|
+
where: {
|
|
56
|
+
classId: classId,
|
|
57
|
+
parentFolderId: null,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
if (!classParentFolder) {
|
|
61
|
+
// Create parent folder if it doesn't exist
|
|
62
|
+
classParentFolder = await prisma.folder.create({
|
|
63
|
+
data: {
|
|
64
|
+
name: "Class Files",
|
|
65
|
+
class: {
|
|
66
|
+
connect: { id: classId },
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
parentFolderId = classParentFolder.id;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Check if specified parent folder exists and belongs to the class
|
|
75
|
+
const parentFolder = await prisma.folder.findFirst({
|
|
76
|
+
where: {
|
|
77
|
+
id: parentFolderId,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
if (!parentFolder) {
|
|
81
|
+
throw new TRPCError({
|
|
82
|
+
code: "NOT_FOUND",
|
|
83
|
+
message: "Parent folder not found",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const folder = await prisma.folder.create({
|
|
88
|
+
data: {
|
|
89
|
+
name,
|
|
90
|
+
...(parentFolderId && {
|
|
91
|
+
parentFolder: {
|
|
92
|
+
connect: { id: parentFolderId },
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
include: {
|
|
97
|
+
files: {
|
|
98
|
+
select: {
|
|
99
|
+
id: true,
|
|
100
|
+
name: true,
|
|
101
|
+
type: true,
|
|
102
|
+
size: true,
|
|
103
|
+
uploadedAt: true,
|
|
104
|
+
user: {
|
|
105
|
+
select: {
|
|
106
|
+
id: true,
|
|
107
|
+
username: true,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
childFolders: {
|
|
113
|
+
select: {
|
|
114
|
+
id: true,
|
|
115
|
+
name: true,
|
|
116
|
+
_count: {
|
|
117
|
+
select: {
|
|
118
|
+
files: true,
|
|
119
|
+
childFolders: true,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return folder;
|
|
127
|
+
}),
|
|
128
|
+
get: protectedClassMemberProcedure
|
|
129
|
+
.input(z.object({
|
|
130
|
+
folderId: z.string(),
|
|
131
|
+
classId: z.string(),
|
|
132
|
+
}))
|
|
133
|
+
.query(async ({ ctx, input }) => {
|
|
134
|
+
const { classId, folderId } = input;
|
|
135
|
+
// Get specific folder
|
|
136
|
+
const folder = await prisma.folder.findFirst({
|
|
137
|
+
where: {
|
|
138
|
+
id: folderId,
|
|
139
|
+
},
|
|
140
|
+
include: {
|
|
141
|
+
files: {
|
|
142
|
+
select: {
|
|
143
|
+
id: true,
|
|
144
|
+
name: true,
|
|
145
|
+
type: true,
|
|
146
|
+
size: true,
|
|
147
|
+
uploadedAt: true,
|
|
148
|
+
user: {
|
|
149
|
+
select: {
|
|
150
|
+
id: true,
|
|
151
|
+
username: true,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
orderBy: {
|
|
156
|
+
uploadedAt: 'desc',
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
childFolders: {
|
|
160
|
+
select: {
|
|
161
|
+
id: true,
|
|
162
|
+
name: true,
|
|
163
|
+
_count: {
|
|
164
|
+
select: {
|
|
165
|
+
files: true,
|
|
166
|
+
childFolders: true,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
orderBy: {
|
|
171
|
+
name: 'asc',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
parentFolder: {
|
|
175
|
+
select: {
|
|
176
|
+
id: true,
|
|
177
|
+
name: true,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
if (!folder) {
|
|
183
|
+
throw new TRPCError({
|
|
184
|
+
code: "NOT_FOUND",
|
|
185
|
+
message: "Folder not found",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return folder;
|
|
189
|
+
}),
|
|
190
|
+
getChildFolders: protectedClassMemberProcedure
|
|
191
|
+
.input(z.object({
|
|
192
|
+
classId: z.string(),
|
|
193
|
+
}))
|
|
194
|
+
.query(async ({ ctx, input }) => {
|
|
195
|
+
const { classId } = input;
|
|
196
|
+
// Get the parent folder for the class (or create it if it doesn't exist)
|
|
197
|
+
let parentFolder = await prisma.folder.findFirst({
|
|
198
|
+
where: {
|
|
199
|
+
classId: classId,
|
|
200
|
+
parentFolderId: null,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
if (!parentFolder) {
|
|
204
|
+
// Create parent folder if it doesn't exist
|
|
205
|
+
parentFolder = await prisma.folder.create({
|
|
206
|
+
data: {
|
|
207
|
+
name: "Class Files",
|
|
208
|
+
class: {
|
|
209
|
+
connect: { id: classId },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
// Get all child folders of the parent
|
|
215
|
+
const childFolders = await prisma.folder.findMany({
|
|
216
|
+
where: {
|
|
217
|
+
parentFolderId: parentFolder.id,
|
|
218
|
+
},
|
|
219
|
+
include: {
|
|
220
|
+
files: {
|
|
221
|
+
select: {
|
|
222
|
+
id: true,
|
|
223
|
+
name: true,
|
|
224
|
+
type: true,
|
|
225
|
+
size: true,
|
|
226
|
+
uploadedAt: true,
|
|
227
|
+
user: {
|
|
228
|
+
select: {
|
|
229
|
+
id: true,
|
|
230
|
+
username: true,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
orderBy: {
|
|
235
|
+
uploadedAt: 'desc',
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
childFolders: {
|
|
239
|
+
select: {
|
|
240
|
+
id: true,
|
|
241
|
+
name: true,
|
|
242
|
+
_count: {
|
|
243
|
+
select: {
|
|
244
|
+
files: true,
|
|
245
|
+
childFolders: true,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
orderBy: {
|
|
250
|
+
name: 'asc',
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
orderBy: {
|
|
255
|
+
name: 'asc',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
return childFolders;
|
|
259
|
+
}),
|
|
260
|
+
getFolderChildren: protectedClassMemberProcedure
|
|
261
|
+
.input(z.object({
|
|
262
|
+
folderId: z.string(),
|
|
263
|
+
classId: z.string(),
|
|
264
|
+
}))
|
|
265
|
+
.query(async ({ ctx, input }) => {
|
|
266
|
+
const { folderId, classId } = input;
|
|
267
|
+
// Get direct children of the specified folder
|
|
268
|
+
const children = await prisma.folder.findMany({
|
|
269
|
+
where: {
|
|
270
|
+
parentFolderId: folderId,
|
|
271
|
+
classId: classId,
|
|
272
|
+
},
|
|
273
|
+
include: {
|
|
274
|
+
files: {
|
|
275
|
+
select: {
|
|
276
|
+
id: true,
|
|
277
|
+
name: true,
|
|
278
|
+
type: true,
|
|
279
|
+
size: true,
|
|
280
|
+
uploadedAt: true,
|
|
281
|
+
user: {
|
|
282
|
+
select: {
|
|
283
|
+
id: true,
|
|
284
|
+
username: true,
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
orderBy: {
|
|
289
|
+
uploadedAt: 'desc',
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
childFolders: {
|
|
293
|
+
select: {
|
|
294
|
+
id: true,
|
|
295
|
+
name: true,
|
|
296
|
+
_count: {
|
|
297
|
+
select: {
|
|
298
|
+
files: true,
|
|
299
|
+
childFolders: true,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
orderBy: {
|
|
304
|
+
name: 'asc',
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
orderBy: {
|
|
309
|
+
name: 'asc',
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
return children;
|
|
313
|
+
}),
|
|
314
|
+
getRootFolder: protectedClassMemberProcedure
|
|
315
|
+
.input(getRootFolderSchema)
|
|
316
|
+
.query(async ({ ctx, input }) => {
|
|
317
|
+
const { classId } = input;
|
|
318
|
+
// Get or create the parent folder for the class
|
|
319
|
+
let parentFolder = await prisma.folder.findFirst({
|
|
320
|
+
where: {
|
|
321
|
+
classId: classId,
|
|
322
|
+
parentFolderId: null,
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
if (!parentFolder) {
|
|
326
|
+
// Create parent folder if it doesn't exist
|
|
327
|
+
parentFolder = await prisma.folder.create({
|
|
328
|
+
data: {
|
|
329
|
+
name: "Class Files",
|
|
330
|
+
class: {
|
|
331
|
+
connect: { id: classId },
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// Get the parent folder with its files and child folders
|
|
337
|
+
const rootFolder = await prisma.folder.findFirst({
|
|
338
|
+
where: {
|
|
339
|
+
id: parentFolder.id,
|
|
340
|
+
classId: classId,
|
|
341
|
+
},
|
|
342
|
+
include: {
|
|
343
|
+
files: {
|
|
344
|
+
select: {
|
|
345
|
+
id: true,
|
|
346
|
+
name: true,
|
|
347
|
+
type: true,
|
|
348
|
+
size: true,
|
|
349
|
+
uploadedAt: true,
|
|
350
|
+
user: {
|
|
351
|
+
select: {
|
|
352
|
+
id: true,
|
|
353
|
+
username: true,
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
orderBy: {
|
|
358
|
+
uploadedAt: 'desc',
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
childFolders: {
|
|
362
|
+
select: {
|
|
363
|
+
id: true,
|
|
364
|
+
name: true,
|
|
365
|
+
files: {
|
|
366
|
+
select: {
|
|
367
|
+
id: true,
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
childFolders: {
|
|
371
|
+
select: {
|
|
372
|
+
id: true,
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
orderBy: {
|
|
377
|
+
name: 'asc',
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
return rootFolder;
|
|
383
|
+
}),
|
|
384
|
+
uploadFiles: protectedTeacherProcedure
|
|
385
|
+
.input(uploadFilesToFolderSchema)
|
|
386
|
+
.mutation(async ({ ctx, input }) => {
|
|
387
|
+
const { classId, folderId, files } = input;
|
|
388
|
+
if (!ctx.user) {
|
|
389
|
+
throw new TRPCError({
|
|
390
|
+
code: "UNAUTHORIZED",
|
|
391
|
+
message: "You must be logged in to upload files",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// Verify user is a teacher of the class
|
|
395
|
+
const classData = await prisma.class.findFirst({
|
|
396
|
+
where: {
|
|
397
|
+
id: classId,
|
|
398
|
+
teachers: {
|
|
399
|
+
some: {
|
|
400
|
+
id: ctx.user.id,
|
|
401
|
+
},
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
if (!classData) {
|
|
406
|
+
throw new TRPCError({
|
|
407
|
+
code: "NOT_FOUND",
|
|
408
|
+
message: "Class not found or you are not a teacher",
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
// Verify folder exists and belongs to the class
|
|
412
|
+
const folder = await prisma.folder.findFirst({
|
|
413
|
+
where: {
|
|
414
|
+
id: folderId,
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
if (!folder) {
|
|
418
|
+
throw new TRPCError({
|
|
419
|
+
code: "NOT_FOUND",
|
|
420
|
+
message: "Folder not found",
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
// Upload files
|
|
424
|
+
const uploadedFiles = await uploadFiles(files, ctx.user.id, folder.id);
|
|
425
|
+
// Create file records in database
|
|
426
|
+
// const fileRecords = await prisma.file.createMany({
|
|
427
|
+
// data: uploadedFiles.map(file => ({
|
|
428
|
+
// name: file.name,
|
|
429
|
+
// type: file.type,
|
|
430
|
+
// size: file.size,
|
|
431
|
+
// path: file.path,
|
|
432
|
+
// userId: ctx.user!.id,
|
|
433
|
+
// folderId: folderId,
|
|
434
|
+
// ...(file.thumbnailId && {
|
|
435
|
+
// thumbnailId: file.thumbnailId,
|
|
436
|
+
// }),
|
|
437
|
+
// })),
|
|
438
|
+
// });
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
uploadedCount: uploadedFiles.length,
|
|
442
|
+
};
|
|
443
|
+
}),
|
|
444
|
+
delete: protectedTeacherProcedure
|
|
445
|
+
.input(z.object({
|
|
446
|
+
classId: z.string(),
|
|
447
|
+
folderId: z.string(),
|
|
448
|
+
}))
|
|
449
|
+
.mutation(async ({ ctx, input }) => {
|
|
450
|
+
const { classId, folderId } = input;
|
|
451
|
+
// Verify user is a teacher of the class
|
|
452
|
+
const classData = await prisma.class.findFirst({
|
|
453
|
+
where: {
|
|
454
|
+
id: classId,
|
|
455
|
+
teachers: {
|
|
456
|
+
some: {
|
|
457
|
+
id: ctx.user.id,
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
});
|
|
462
|
+
if (!classData) {
|
|
463
|
+
throw new TRPCError({
|
|
464
|
+
code: "FORBIDDEN",
|
|
465
|
+
message: "Class not found or you are not a teacher",
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// Verify folder exists and belongs to the class
|
|
469
|
+
const folder = await prisma.folder.findFirst({
|
|
470
|
+
where: {
|
|
471
|
+
id: folderId,
|
|
472
|
+
classId: classId,
|
|
473
|
+
},
|
|
474
|
+
include: {
|
|
475
|
+
_count: {
|
|
476
|
+
select: {
|
|
477
|
+
files: true,
|
|
478
|
+
childFolders: true,
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
if (!folder) {
|
|
484
|
+
throw new TRPCError({
|
|
485
|
+
code: "NOT_FOUND",
|
|
486
|
+
message: "Folder not found",
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
// Delete folder and all its contents (cascade)
|
|
490
|
+
await prisma.folder.delete({
|
|
491
|
+
where: {
|
|
492
|
+
id: folderId,
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
return {
|
|
496
|
+
success: true,
|
|
497
|
+
deletedFiles: folder._count.files,
|
|
498
|
+
deletedFolders: folder._count.childFolders + 1, // +1 for the folder itself
|
|
499
|
+
};
|
|
500
|
+
}),
|
|
501
|
+
move: protectedTeacherProcedure
|
|
502
|
+
.input(z.object({
|
|
503
|
+
folderId: z.string(),
|
|
504
|
+
targetParentFolderId: z.string().optional(),
|
|
505
|
+
classId: z.string(),
|
|
506
|
+
}))
|
|
507
|
+
.mutation(async ({ ctx, input }) => {
|
|
508
|
+
const { folderId, targetParentFolderId, classId } = input;
|
|
509
|
+
// Verify user is a teacher of the class
|
|
510
|
+
const classData = await prisma.class.findFirst({
|
|
511
|
+
where: {
|
|
512
|
+
id: classId,
|
|
513
|
+
teachers: {
|
|
514
|
+
some: {
|
|
515
|
+
id: ctx.user.id,
|
|
516
|
+
},
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
if (!classData) {
|
|
521
|
+
throw new TRPCError({
|
|
522
|
+
code: "FORBIDDEN",
|
|
523
|
+
message: "You must be a teacher of this class to move folders",
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// Get the folder to move
|
|
527
|
+
const folder = await prisma.folder.findFirst({
|
|
528
|
+
where: {
|
|
529
|
+
id: folderId,
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
if (!folder) {
|
|
533
|
+
throw new TRPCError({
|
|
534
|
+
code: "NOT_FOUND",
|
|
535
|
+
message: "Folder not found",
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
// Prevent moving the root folder
|
|
539
|
+
if (!folder.parentFolderId) {
|
|
540
|
+
throw new TRPCError({
|
|
541
|
+
code: "BAD_REQUEST",
|
|
542
|
+
message: "Cannot move the root folder",
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
// If target parent folder is specified, verify it exists and belongs to the class
|
|
546
|
+
if (targetParentFolderId) {
|
|
547
|
+
const targetParentFolder = await prisma.folder.findFirst({
|
|
548
|
+
where: {
|
|
549
|
+
id: targetParentFolderId,
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
if (!targetParentFolder) {
|
|
553
|
+
throw new TRPCError({
|
|
554
|
+
code: "NOT_FOUND",
|
|
555
|
+
message: "Target parent folder not found",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
// Prevent moving a folder into itself or its descendants
|
|
559
|
+
if (targetParentFolderId === folderId) {
|
|
560
|
+
throw new TRPCError({
|
|
561
|
+
code: "BAD_REQUEST",
|
|
562
|
+
message: "Cannot move a folder into itself",
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
// Check if target is a descendant of the folder being moved
|
|
566
|
+
let currentParent = targetParentFolder;
|
|
567
|
+
while (currentParent?.parentFolderId) {
|
|
568
|
+
if (currentParent.parentFolderId === folderId) {
|
|
569
|
+
throw new TRPCError({
|
|
570
|
+
code: "BAD_REQUEST",
|
|
571
|
+
message: "Cannot move a folder into its descendant",
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
currentParent = await prisma.folder.findUnique({
|
|
575
|
+
where: { id: currentParent.parentFolderId },
|
|
576
|
+
});
|
|
577
|
+
if (!currentParent)
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
else {
|
|
582
|
+
// Moving to root - verify the folder isn't already at root
|
|
583
|
+
if (!folder.parentFolderId) {
|
|
584
|
+
throw new TRPCError({
|
|
585
|
+
code: "BAD_REQUEST",
|
|
586
|
+
message: "Folder is already at root level",
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
// Move the folder
|
|
591
|
+
const updatedFolder = await prisma.folder.update({
|
|
592
|
+
where: { id: folderId },
|
|
593
|
+
data: {
|
|
594
|
+
parentFolderId: targetParentFolderId || null,
|
|
595
|
+
},
|
|
596
|
+
include: {
|
|
597
|
+
files: {
|
|
598
|
+
select: {
|
|
599
|
+
id: true,
|
|
600
|
+
name: true,
|
|
601
|
+
type: true,
|
|
602
|
+
size: true,
|
|
603
|
+
uploadedAt: true,
|
|
604
|
+
user: {
|
|
605
|
+
select: {
|
|
606
|
+
id: true,
|
|
607
|
+
username: true,
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
childFolders: {
|
|
613
|
+
select: {
|
|
614
|
+
id: true,
|
|
615
|
+
name: true,
|
|
616
|
+
_count: {
|
|
617
|
+
select: {
|
|
618
|
+
files: true,
|
|
619
|
+
childFolders: true,
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
});
|
|
626
|
+
return updatedFolder;
|
|
627
|
+
}),
|
|
628
|
+
rename: protectedTeacherProcedure
|
|
629
|
+
.input(z.object({
|
|
630
|
+
folderId: z.string(),
|
|
631
|
+
newName: z.string(),
|
|
632
|
+
classId: z.string(),
|
|
633
|
+
}))
|
|
634
|
+
.mutation(async ({ ctx, input }) => {
|
|
635
|
+
const { folderId, newName, classId } = input;
|
|
636
|
+
// Get the folder
|
|
637
|
+
const folder = await prisma.folder.findFirst({
|
|
638
|
+
where: {
|
|
639
|
+
id: folderId,
|
|
640
|
+
},
|
|
641
|
+
});
|
|
642
|
+
if (!folder) {
|
|
643
|
+
throw new TRPCError({
|
|
644
|
+
code: "NOT_FOUND",
|
|
645
|
+
message: "Folder not found",
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
// Validate new name
|
|
649
|
+
if (!newName.trim()) {
|
|
650
|
+
throw new TRPCError({
|
|
651
|
+
code: "BAD_REQUEST",
|
|
652
|
+
message: "Folder name cannot be empty",
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
// Rename the folder
|
|
656
|
+
const updatedFolder = await prisma.folder.update({
|
|
657
|
+
where: { id: folderId },
|
|
658
|
+
data: {
|
|
659
|
+
name: newName.trim(),
|
|
660
|
+
},
|
|
661
|
+
include: {
|
|
662
|
+
files: {
|
|
663
|
+
select: {
|
|
664
|
+
id: true,
|
|
665
|
+
name: true,
|
|
666
|
+
type: true,
|
|
667
|
+
size: true,
|
|
668
|
+
uploadedAt: true,
|
|
669
|
+
user: {
|
|
670
|
+
select: {
|
|
671
|
+
id: true,
|
|
672
|
+
username: true,
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
},
|
|
677
|
+
childFolders: {
|
|
678
|
+
select: {
|
|
679
|
+
id: true,
|
|
680
|
+
name: true,
|
|
681
|
+
_count: {
|
|
682
|
+
select: {
|
|
683
|
+
files: true,
|
|
684
|
+
childFolders: true,
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
return updatedFolder;
|
|
692
|
+
}),
|
|
693
|
+
});
|