@studious-lms/server 1.0.4 → 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.
Files changed (101) hide show
  1. package/API_SPECIFICATION.md +1117 -0
  2. package/dist/exportType.js +1 -2
  3. package/dist/index.js +25 -30
  4. package/dist/lib/fileUpload.d.ts.map +1 -1
  5. package/dist/lib/fileUpload.js +31 -29
  6. package/dist/lib/googleCloudStorage.js +9 -14
  7. package/dist/lib/prisma.js +4 -7
  8. package/dist/lib/thumbnailGenerator.js +12 -20
  9. package/dist/middleware/auth.d.ts.map +1 -1
  10. package/dist/middleware/auth.js +17 -22
  11. package/dist/middleware/logging.js +5 -9
  12. package/dist/routers/_app.d.ts +3483 -1801
  13. package/dist/routers/_app.d.ts.map +1 -1
  14. package/dist/routers/_app.js +28 -27
  15. package/dist/routers/agenda.d.ts +13 -8
  16. package/dist/routers/agenda.d.ts.map +1 -1
  17. package/dist/routers/agenda.js +14 -17
  18. package/dist/routers/announcement.d.ts +4 -3
  19. package/dist/routers/announcement.d.ts.map +1 -1
  20. package/dist/routers/announcement.js +28 -31
  21. package/dist/routers/assignment.d.ts +282 -196
  22. package/dist/routers/assignment.d.ts.map +1 -1
  23. package/dist/routers/assignment.js +256 -202
  24. package/dist/routers/attendance.d.ts +5 -4
  25. package/dist/routers/attendance.d.ts.map +1 -1
  26. package/dist/routers/attendance.js +31 -34
  27. package/dist/routers/auth.d.ts +1 -0
  28. package/dist/routers/auth.d.ts.map +1 -1
  29. package/dist/routers/auth.js +80 -75
  30. package/dist/routers/class.d.ts +284 -14
  31. package/dist/routers/class.d.ts.map +1 -1
  32. package/dist/routers/class.js +435 -164
  33. package/dist/routers/event.d.ts +47 -38
  34. package/dist/routers/event.d.ts.map +1 -1
  35. package/dist/routers/event.js +76 -79
  36. package/dist/routers/file.d.ts +71 -1
  37. package/dist/routers/file.d.ts.map +1 -1
  38. package/dist/routers/file.js +267 -32
  39. package/dist/routers/folder.d.ts +296 -0
  40. package/dist/routers/folder.d.ts.map +1 -0
  41. package/dist/routers/folder.js +693 -0
  42. package/dist/routers/notifications.d.ts +103 -0
  43. package/dist/routers/notifications.d.ts.map +1 -0
  44. package/dist/routers/notifications.js +91 -0
  45. package/dist/routers/school.d.ts +208 -0
  46. package/dist/routers/school.d.ts.map +1 -0
  47. package/dist/routers/school.js +481 -0
  48. package/dist/routers/section.d.ts +1 -0
  49. package/dist/routers/section.d.ts.map +1 -1
  50. package/dist/routers/section.js +30 -33
  51. package/dist/routers/user.d.ts +2 -1
  52. package/dist/routers/user.d.ts.map +1 -1
  53. package/dist/routers/user.js +21 -24
  54. package/dist/seedDatabase.d.ts +22 -0
  55. package/dist/seedDatabase.d.ts.map +1 -0
  56. package/dist/seedDatabase.js +57 -0
  57. package/dist/socket/handlers.js +26 -30
  58. package/dist/trpc.d.ts +5 -0
  59. package/dist/trpc.d.ts.map +1 -1
  60. package/dist/trpc.js +35 -26
  61. package/dist/types/trpc.js +1 -2
  62. package/dist/utils/email.js +2 -8
  63. package/dist/utils/generateInviteCode.js +1 -5
  64. package/dist/utils/logger.d.ts.map +1 -1
  65. package/dist/utils/logger.js +13 -9
  66. package/dist/utils/prismaErrorHandler.d.ts +9 -0
  67. package/dist/utils/prismaErrorHandler.d.ts.map +1 -0
  68. package/dist/utils/prismaErrorHandler.js +234 -0
  69. package/dist/utils/prismaWrapper.d.ts +14 -0
  70. package/dist/utils/prismaWrapper.d.ts.map +1 -0
  71. package/dist/utils/prismaWrapper.js +64 -0
  72. package/package.json +17 -4
  73. package/prisma/migrations/20250807062924_init/migration.sql +436 -0
  74. package/prisma/migrations/migration_lock.toml +3 -0
  75. package/prisma/schema.prisma +67 -0
  76. package/src/index.ts +2 -2
  77. package/src/lib/fileUpload.ts +16 -7
  78. package/src/middleware/auth.ts +0 -2
  79. package/src/routers/_app.ts +5 -1
  80. package/src/routers/assignment.ts +82 -22
  81. package/src/routers/auth.ts +80 -54
  82. package/src/routers/class.ts +330 -36
  83. package/src/routers/file.ts +283 -20
  84. package/src/routers/folder.ts +755 -0
  85. package/src/routers/notifications.ts +93 -0
  86. package/src/seedDatabase.ts +66 -0
  87. package/src/socket/handlers.ts +4 -4
  88. package/src/trpc.ts +13 -0
  89. package/src/utils/logger.ts +14 -4
  90. package/src/utils/prismaErrorHandler.ts +275 -0
  91. package/src/utils/prismaWrapper.ts +91 -0
  92. package/tests/auth.test.ts +25 -0
  93. package/tests/class.test.ts +281 -0
  94. package/tests/setup.ts +98 -0
  95. package/tests/startup.test.ts +5 -0
  96. package/tsconfig.json +2 -1
  97. package/vitest.config.ts +11 -0
  98. package/dist/logger.d.ts +0 -26
  99. package/dist/logger.d.ts.map +0 -1
  100. package/dist/logger.js +0 -135
  101. 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
+ });