@studious-lms/server 1.2.32 → 1.2.33

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.
@@ -82,6 +82,7 @@ const submissionSchema = z.object({
82
82
  submissionId: z.string(),
83
83
  submit: z.boolean().optional(),
84
84
  newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
85
+ extendedResponse: z.string().optional(),
85
86
  existingFileIds: z.array(z.string()).optional(),
86
87
  removedAttachments: z.array(z.string()).optional(),
87
88
  });
@@ -169,16 +170,42 @@ async function getUnifiedList(tx: any, classId: string) {
169
170
  }
170
171
 
171
172
  // Helper function to normalize unified list to 1..n
173
+ // Updated to batch updates to prevent timeouts with large lists
172
174
  async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Array<{ id: string; type: 'section' | 'assignment' }>) {
173
- await Promise.all(
174
- orderedItems.map((item, index) => {
175
- if (item.type === 'section') {
176
- return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
177
- } else {
178
- return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
179
- }
180
- })
181
- );
175
+ const BATCH_SIZE = 10; // Process 10 items at a time to avoid overwhelming the transaction
176
+
177
+ // Group items by type for more efficient updates
178
+ const sections: Array<{ id: string; order: number }> = [];
179
+ const assignments: Array<{ id: string; order: number }> = [];
180
+
181
+ orderedItems.forEach((item, index) => {
182
+ const orderData = { id: item.id, order: index + 1 };
183
+ if (item.type === 'section') {
184
+ sections.push(orderData);
185
+ } else {
186
+ assignments.push(orderData);
187
+ }
188
+ });
189
+
190
+ // Process updates in batches
191
+ const processBatch = async (items: Array<{ id: string; order: number }>, type: 'section' | 'assignment') => {
192
+ for (let i = 0; i < items.length; i += BATCH_SIZE) {
193
+ const batch = items.slice(i, i + BATCH_SIZE);
194
+ await Promise.all(
195
+ batch.map(item => {
196
+ if (type === 'section') {
197
+ return tx.section.update({ where: { id: item.id }, data: { order: item.order } });
198
+ } else {
199
+ return tx.assignment.update({ where: { id: item.id }, data: { order: item.order } });
200
+ }
201
+ })
202
+ );
203
+ }
204
+ };
205
+
206
+ // Process sections and assignments sequentially to avoid transaction overload
207
+ await processBatch(sections, 'section');
208
+ await processBatch(assignments, 'assignment');
182
209
  }
183
210
 
184
211
  export const assignmentRouter = createTRPCRouter({
@@ -249,6 +276,9 @@ export const assignmentRouter = createTRPCRouter({
249
276
  await normalizeUnifiedList(tx, classId, next);
250
277
 
251
278
  return tx.assignment.findUnique({ where: { id: movedId } });
279
+ }, {
280
+ maxWait: 10000, // 10 seconds max wait time
281
+ timeout: 30000, // 30 seconds timeout for reordering operations
252
282
  });
253
283
 
254
284
  return result;
@@ -280,6 +310,9 @@ export const assignmentRouter = createTRPCRouter({
280
310
  await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
281
311
 
282
312
  return tx.assignment.findUnique({ where: { id } });
313
+ }, {
314
+ maxWait: 10000, // 10 seconds max wait time
315
+ timeout: 30000, // 30 seconds timeout for reordering operations
283
316
  });
284
317
 
285
318
  return updated;
@@ -313,6 +346,9 @@ export const assignmentRouter = createTRPCRouter({
313
346
  // If frontend wants to change position, they should call reorder after move
314
347
 
315
348
  return tx.assignment.findUnique({ where: { id } });
349
+ }, {
350
+ maxWait: 5000, // 5 seconds max wait time
351
+ timeout: 10000, // 10 seconds timeout
316
352
  });
317
353
 
318
354
  return updated;
@@ -330,15 +366,23 @@ export const assignmentRouter = createTRPCRouter({
330
366
  });
331
367
  }
332
368
 
333
- // Get all students in the class
334
- const classData = await prisma.class.findUnique({
335
- where: { id: classId },
336
- include: {
337
- students: {
338
- select: { id: true }
369
+ // Pre-fetch data needed for the transaction (outside transaction scope)
370
+ const [classData, rubricData] = await Promise.all([
371
+ prisma.class.findUnique({
372
+ where: { id: classId },
373
+ include: {
374
+ students: {
375
+ select: { id: true }
376
+ }
339
377
  }
340
- }
341
- });
378
+ }),
379
+ markSchemeId ? prisma.markScheme.findUnique({
380
+ where: { id: markSchemeId },
381
+ select: {
382
+ structured: true,
383
+ }
384
+ }) : null
385
+ ]);
342
386
 
343
387
  if (!classData) {
344
388
  throw new TRPCError({
@@ -347,172 +391,198 @@ export const assignmentRouter = createTRPCRouter({
347
391
  });
348
392
  }
349
393
 
394
+ // Calculate max grade outside transaction
350
395
  let computedMaxGrade = maxGrade;
351
- if (markSchemeId) {
352
- const rubric = await prisma.markScheme.findUnique({
353
- where: { id: markSchemeId },
354
- select: {
355
- structured: true,
356
- }
357
- });
358
-
359
- const parsedRubric = JSON.parse(rubric?.structured || "{}");
360
-
361
- // Calculate max grade from rubric criteria levels
396
+ if (markSchemeId && rubricData) {
397
+ const parsedRubric = JSON.parse(rubricData.structured || "{}");
362
398
  computedMaxGrade = parsedRubric.criteria.reduce((acc: number, criterion: any) => {
363
399
  const maxPoints = Math.max(...criterion.levels.map((level: any) => level.points));
364
400
  return acc + maxPoints;
365
401
  }, 0);
366
402
  }
367
- console.log(markSchemeId, gradingBoundaryId);
368
403
 
369
- // Create assignment and place at top of its scope within a single transaction
370
- const teacherId = ctx.user!.id;
404
+ console.log(studentIds);
405
+
406
+ // Prepare submission data outside transaction
407
+ const submissionData = studentIds && studentIds.length > 0
408
+ ? studentIds.map((studentId) => ({
409
+ student: { connect: { id: studentId } }
410
+ }))
411
+ : classData.students.map((student) => ({
412
+ student: { connect: { id: student.id } }
413
+ }));
414
+
415
+ const teacherId = ctx.user.id;
416
+
417
+ // Minimal transaction - only for critical operations
371
418
  const assignment = await prisma.$transaction(async (tx) => {
419
+ // Create assignment with order 0 (will be at top)
372
420
  const created = await tx.assignment.create({
373
- data: {
374
- title,
375
- instructions,
376
- dueDate: new Date(dueDate),
377
- maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
378
- graded,
379
- weight,
380
- type,
381
- ...(aiPolicyLevel !== undefined && { aiPolicyLevel }),
382
- acceptFiles,
383
- acceptExtendedResponse,
384
- acceptWorksheet,
385
- ...(worksheetIds && {
386
- worksheets: {
387
- connect: worksheetIds.map(id => ({ id }))
388
- }
389
- }),
390
- gradeWithAI,
391
- ...(studentIds && {
392
- assignedTo: {
393
- connect: studentIds.map(id => ({ id }))
394
- }
395
- }),
396
- order: 1,
397
- inProgress: inProgress || false,
398
- class: {
399
- connect: { id: classId }
400
- },
401
- ...(sectionId && {
402
- section: {
403
- connect: { id: sectionId }
404
- }
405
- }),
406
- ...(markSchemeId && {
407
- markScheme: {
408
- connect: { id: markSchemeId }
409
- }
410
- }),
411
- ...(gradingBoundaryId && {
412
- gradingBoundary: {
413
- connect: { id: gradingBoundaryId }
414
- }
415
- }),
416
- submissions: {
417
- create: classData.students.map((student) => ({
418
- student: {
419
- connect: { id: student.id }
421
+ data: {
422
+ title,
423
+ instructions,
424
+ dueDate: new Date(dueDate),
425
+ maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
426
+ graded,
427
+ weight,
428
+ type,
429
+ ...(aiPolicyLevel && { aiPolicyLevel }),
430
+ acceptFiles,
431
+ acceptExtendedResponse,
432
+ acceptWorksheet,
433
+ ...(worksheetIds && {
434
+ worksheets: {
435
+ connect: worksheetIds.map(id => ({ id }))
420
436
  }
421
- }))
422
- },
437
+ }),
438
+ gradeWithAI,
439
+ ...(studentIds && {
440
+ assignedTo: {
441
+ connect: studentIds.map(id => ({ id }))
442
+ }
443
+ }),
444
+ order: 0, // Set to 0 to place at top
445
+ inProgress: inProgress || false,
446
+ class: {
447
+ connect: { id: classId }
448
+ },
449
+ ...(sectionId && {
450
+ section: {
451
+ connect: { id: sectionId }
452
+ }
453
+ }),
454
+ ...(markSchemeId && {
455
+ markScheme: {
456
+ connect: { id: markSchemeId }
457
+ }
458
+ }),
459
+ ...(gradingBoundaryId && {
460
+ gradingBoundary: {
461
+ connect: { id: gradingBoundaryId }
462
+ }
463
+ }),
464
+ submissions: {
465
+ create: submissionData
466
+ },
423
467
  teacher: {
424
468
  connect: { id: teacherId }
425
469
  }
426
- },
427
- select: {
428
- id: true,
429
- title: true,
430
- instructions: true,
431
- dueDate: true,
432
- maxGrade: true,
433
- graded: true,
434
- weight: true,
435
- type: true,
436
- attachments: {
437
- select: {
438
- id: true,
439
- name: true,
440
- type: true,
441
- }
442
- },
443
- section: {
444
- select: {
445
- id: true,
446
- name: true
447
- }
448
470
  },
449
- teacher: {
450
- select: {
451
- id: true,
452
- username: true
471
+ select: {
472
+ id: true,
473
+ title: true,
474
+ instructions: true,
475
+ dueDate: true,
476
+ maxGrade: true,
477
+ graded: true,
478
+ weight: true,
479
+ type: true,
480
+ attachments: {
481
+ select: {
482
+ id: true,
483
+ name: true,
484
+ type: true,
485
+ }
486
+ },
487
+ section: {
488
+ select: {
489
+ id: true,
490
+ name: true
491
+ }
492
+ },
493
+ teacher: {
494
+ select: {
495
+ id: true,
496
+ username: true
497
+ }
498
+ },
499
+ class: {
500
+ select: {
501
+ id: true,
502
+ name: true
503
+ }
453
504
  }
505
+ }
506
+ });
507
+
508
+ // Optimized reordering - only increment existing items by 1
509
+ // This is much faster than reordering everything
510
+ await tx.assignment.updateMany({
511
+ where: {
512
+ classId: classId,
513
+ id: { not: created.id },
454
514
  },
455
- class: {
456
- select: {
457
- id: true,
458
- name: true
459
- }
515
+ data: {
516
+ order: { increment: 1 }
460
517
  }
518
+ });
519
+
520
+ await tx.section.updateMany({
521
+ where: {
522
+ classId: classId,
523
+ },
524
+ data: {
525
+ order: { increment: 1 }
461
526
  }
462
527
  });
463
528
 
464
- // Insert new assignment at top of unified list and normalize
465
- const unified = await getUnifiedList(tx, classId);
466
- const withoutNew = unified.filter(item => !(item.id === created.id && item.type === 'assignment'));
467
- const reindexed = [{ id: created.id, type: 'assignment' as const }, ...withoutNew.map(item => ({ id: item.id, type: item.type }))];
468
- await normalizeUnifiedList(tx, classId, reindexed);
529
+ // Update the created assignment to have order 1
530
+ await tx.assignment.update({
531
+ where: { id: created.id },
532
+ data: { order: 1 }
533
+ });
469
534
 
470
535
  return created;
536
+ }, {
537
+ maxWait: 10000, // Increased to 10 seconds max wait time
538
+ timeout: 20000, // Increased to 20 seconds timeout for safety
471
539
  });
472
540
 
473
- // NOTE: Files are now handled via direct upload endpoints
474
- // The files field in the schema is for metadata only
475
- // Actual file uploads should use getAssignmentUploadUrls endpoint
476
- let uploadedFiles: UploadedFile[] = [];
477
- if (files && files.length > 0) {
478
- // Create direct upload files instead of processing base64
479
- uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id);
480
- }
541
+ // Handle file operations outside transaction
542
+ const fileOperations: Promise<any>[] = [];
481
543
 
482
- // Update assignment with new file attachments
483
- if (uploadedFiles.length > 0) {
484
- await prisma.assignment.update({
485
- where: { id: assignment.id },
486
- data: {
487
- attachments: {
488
- create: uploadedFiles.map(file => ({
489
- name: file.name,
490
- type: file.type,
491
- size: file.size,
492
- path: file.path,
493
- ...(file.thumbnailId && {
494
- thumbnail: {
495
- connect: { id: file.thumbnailId }
544
+ // Process direct upload files
545
+ if (files && files.length > 0) {
546
+ fileOperations.push(
547
+ createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id)
548
+ .then(uploadedFiles => {
549
+ if (uploadedFiles.length > 0) {
550
+ return prisma.assignment.update({
551
+ where: { id: assignment.id },
552
+ data: {
553
+ attachments: {
554
+ create: uploadedFiles.map(file => ({
555
+ name: file.name,
556
+ type: file.type,
557
+ size: file.size,
558
+ path: file.path,
559
+ }))
560
+ }
496
561
  }
497
- })
498
- }))
499
- }
500
- }
501
- });
562
+ });
563
+ }
564
+ })
565
+ );
502
566
  }
503
567
 
504
- // Connect existing files if provided
568
+ // Connect existing files
505
569
  if (existingFileIds && existingFileIds.length > 0) {
506
- await prisma.assignment.update({
507
- where: { id: assignment.id },
508
- data: {
509
- attachments: {
510
- connect: existingFileIds.map(fileId => ({ id: fileId }))
570
+ fileOperations.push(
571
+ prisma.assignment.update({
572
+ where: { id: assignment.id },
573
+ data: {
574
+ attachments: {
575
+ connect: existingFileIds.map(fileId => ({ id: fileId }))
576
+ }
511
577
  }
512
- }
513
- });
578
+ })
579
+ );
514
580
  }
581
+
582
+ // Execute file operations in parallel
583
+ await Promise.all(fileOperations);
515
584
 
585
+ // Send notifications asynchronously (non-blocking)
516
586
  sendNotifications(classData.students.map(student => student.id), {
517
587
  title: `🔔 New assignment for ${classData.name}`,
518
588
  content:
@@ -520,7 +590,7 @@ export const assignmentRouter = createTRPCRouter({
520
590
  Due date: ${new Date(dueDate).toLocaleDateString()}.
521
591
  [Link to assignment](/class/${classId}/assignments/${assignment.id})`
522
592
  }).catch(error => {
523
- logger.error('Failed to send assignment notifications:');
593
+ logger.error('Failed to send assignment notifications:', error);
524
594
  });
525
595
 
526
596
  return assignment;
@@ -537,36 +607,51 @@ export const assignmentRouter = createTRPCRouter({
537
607
  });
538
608
  }
539
609
 
540
- // Get the assignment with current attachments
541
- const assignment = await prisma.assignment.findFirst({
542
- where: {
543
- id,
544
- teacherId: ctx.user.id,
545
- },
546
- include: {
547
- attachments: {
548
- select: {
549
- id: true,
550
- name: true,
551
- type: true,
552
- path: true,
553
- size: true,
554
- uploadStatus: true,
555
- thumbnail: {
556
- select: {
557
- path: true
610
+ // Pre-fetch all necessary data outside transaction
611
+ const [assignment, classData] = await Promise.all([
612
+ prisma.assignment.findFirst({
613
+ where: {
614
+ id,
615
+ teacherId: ctx.user.id,
616
+ },
617
+ include: {
618
+ attachments: {
619
+ select: {
620
+ id: true,
621
+ name: true,
622
+ type: true,
623
+ path: true,
624
+ size: true,
625
+ uploadStatus: true,
626
+ thumbnail: {
627
+ select: {
628
+ path: true
629
+ }
558
630
  }
631
+ },
632
+ },
633
+ class: {
634
+ select: {
635
+ id: true,
636
+ name: true
559
637
  }
560
638
  },
639
+ markScheme: true,
561
640
  },
562
- class: {
563
- select: {
564
- id: true,
565
- name: true
566
- }
641
+ }),
642
+ prisma.class.findFirst({
643
+ where: {
644
+ assignments: {
645
+ some: { id }
646
+ }
567
647
  },
568
- },
569
- });
648
+ include: {
649
+ students: {
650
+ select: { id: true }
651
+ }
652
+ }
653
+ })
654
+ ]);
570
655
 
571
656
  if (!assignment) {
572
657
  throw new TRPCError({
@@ -575,161 +660,190 @@ export const assignmentRouter = createTRPCRouter({
575
660
  });
576
661
  }
577
662
 
578
- // NOTE: Files are now handled via direct upload endpoints
579
- let uploadedFiles: UploadedFile[] = [];
580
- if (files && files.length > 0) {
581
- // Create direct upload files instead of processing base64
582
- uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
583
- }
663
+ // Prepare submission data outside transaction if needed
664
+ const submissionData = studentIds && studentIds.length > 0
665
+ ? studentIds.map((studentId) => ({
666
+ student: { connect: { id: studentId } }
667
+ }))
668
+ : classData?.students.map((student) => ({
669
+ student: { connect: { id: student.id } }
670
+ }));
584
671
 
585
- // Delete removed attachments from storage before updating database
672
+ // Handle file deletion operations outside transaction
673
+ const fileDeletionPromises: Promise<void>[] = [];
586
674
  if (input.removedAttachments && input.removedAttachments.length > 0) {
587
675
  const filesToDelete = assignment.attachments.filter((file) =>
588
676
  input.removedAttachments!.includes(file.id)
589
677
  );
590
678
 
591
- // Delete files from storage (only if they were actually uploaded)
592
- await Promise.all(filesToDelete.map(async (file) => {
593
- try {
594
- // Only delete from GCS if the file was successfully uploaded
595
- if (file.uploadStatus === 'COMPLETED') {
596
- // Delete the main file
597
- await deleteFile(file.path);
598
-
599
- // Delete thumbnail if it exists
600
- if (file.thumbnail?.path) {
601
- await deleteFile(file.thumbnail.path);
602
- }
679
+ filesToDelete.forEach((file) => {
680
+ if (file.uploadStatus === 'COMPLETED') {
681
+ fileDeletionPromises.push(
682
+ deleteFile(file.path).catch(error => {
683
+ console.warn(`Failed to delete file ${file.path}:`, error);
684
+ })
685
+ );
686
+ if (file.thumbnail?.path) {
687
+ fileDeletionPromises.push(
688
+ deleteFile(file.thumbnail.path).catch(error => {
689
+ console.warn(`Failed to delete thumbnail ${file.thumbnail!.path}:`, error);
690
+ })
691
+ );
603
692
  }
604
- } catch (error) {
605
- console.warn(`Failed to delete file ${file.path}:`, error);
606
693
  }
607
- }));
694
+ });
608
695
  }
609
696
 
610
- // Update assignment
611
- const updatedAssignment = await prisma.assignment.update({
612
- where: { id },
613
- data: {
614
- ...(title && { title }),
615
- ...(instructions && { instructions }),
616
- ...(dueDate && { dueDate: new Date(dueDate) }),
617
- ...(maxGrade && { maxGrade }),
618
- ...(graded !== undefined && { graded }),
619
- ...(weight && { weight }),
620
- ...(type && { type }),
621
- ...(inProgress !== undefined && { inProgress }),
622
- ...(acceptFiles !== undefined && { acceptFiles }),
623
- ...(acceptExtendedResponse !== undefined && { acceptExtendedResponse }),
624
- ...(acceptWorksheet !== undefined && { acceptWorksheet }),
625
- ...(gradeWithAI !== undefined && { gradeWithAI }),
626
- ...(studentIds && {
627
- assignedTo: {
628
- connect: studentIds.map(id => ({ id }))
629
- }
630
- }),
631
- ...(aiPolicyLevel !== undefined && { aiPolicyLevel }),
632
- ...(sectionId !== undefined && {
633
- section: sectionId ? {
634
- connect: { id: sectionId }
635
- } : {
636
- disconnect: true
637
- }
638
- }),
639
- ...(worksheetIds && {
640
- worksheets: {
641
- connect: worksheetIds.map(id => ({ id }))
642
- }
643
- }),
644
- ...(uploadedFiles.length > 0 && {
645
- attachments: {
646
- create: uploadedFiles.map(file => ({
647
- name: file.name,
648
- type: file.type,
649
- size: file.size,
650
- path: file.path,
651
- ...(file.thumbnailId && {
652
- thumbnail: {
653
- connect: { id: file.thumbnailId }
654
- }
655
- })
656
- }))
657
- }
658
- }),
659
- ...(existingFileIds && existingFileIds.length > 0 && {
660
- attachments: {
661
- connect: existingFileIds.map(fileId => ({ id: fileId }))
662
- }
663
- }),
664
- ...(input.removedAttachments && input.removedAttachments.length > 0 && {
665
- attachments: {
666
- deleteMany: {
667
- id: { in: input.removedAttachments }
697
+ // Execute file deletions in parallel
698
+ await Promise.all(fileDeletionPromises);
699
+
700
+ // Prepare file upload operations
701
+ let uploadedFiles: UploadedFile[] = [];
702
+ if (files && files.length > 0) {
703
+ uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
704
+ }
705
+
706
+ // Minimal transaction for database update
707
+ const updatedAssignment = await prisma.$transaction(async (tx) => {
708
+ return tx.assignment.update({
709
+ where: { id },
710
+ data: {
711
+ ...(title && { title }),
712
+ ...(instructions && { instructions }),
713
+ ...(dueDate && { dueDate: new Date(dueDate) }),
714
+ ...(maxGrade && { maxGrade }),
715
+ ...(graded !== undefined && { graded }),
716
+ ...(weight && { weight }),
717
+ ...(type && { type }),
718
+ ...(inProgress !== undefined && { inProgress }),
719
+ ...(acceptFiles !== undefined && { acceptFiles }),
720
+ ...(acceptExtendedResponse !== undefined && { acceptExtendedResponse }),
721
+ ...(acceptWorksheet !== undefined && { acceptWorksheet }),
722
+ ...(gradeWithAI !== undefined && { gradeWithAI }),
723
+ ...(studentIds && {
724
+ assignedTo: {
725
+ set: [], // Clear existing
726
+ connect: studentIds.map(id => ({ id }))
668
727
  }
669
- }
670
- }),
671
- },
672
- select: {
673
- id: true,
674
- title: true,
675
- instructions: true,
676
- dueDate: true,
677
- maxGrade: true,
678
- graded: true,
679
- weight: true,
680
- type: true,
681
- createdAt: true,
682
- submissions: {
683
- select: {
684
- student: {
685
- select: {
686
- id: true,
687
- username: true
728
+ }),
729
+ ...(submissionData && {
730
+ submissions: {
731
+ deleteMany: {}, // Clear existing submissions
732
+ create: submissionData
733
+ }
734
+ }),
735
+ ...(aiPolicyLevel !== undefined && { aiPolicyLevel }),
736
+ ...(sectionId !== undefined && {
737
+ section: sectionId ? {
738
+ connect: { id: sectionId }
739
+ } : {
740
+ disconnect: true
741
+ }
742
+ }),
743
+ ...(worksheetIds && {
744
+ worksheets: {
745
+ set: [], // Clear existing
746
+ connect: worksheetIds.map(id => ({ id }))
747
+ }
748
+ }),
749
+ ...(uploadedFiles.length > 0 && {
750
+ attachments: {
751
+ create: uploadedFiles.map(file => ({
752
+ name: file.name,
753
+ type: file.type,
754
+ size: file.size,
755
+ path: file.path,
756
+ ...(file.thumbnailId && {
757
+ thumbnail: {
758
+ connect: { id: file.thumbnailId }
759
+ }
760
+ })
761
+ }))
762
+ }
763
+ }),
764
+ ...(existingFileIds && existingFileIds.length > 0 && {
765
+ attachments: {
766
+ connect: existingFileIds.map(fileId => ({ id: fileId }))
767
+ }
768
+ }),
769
+ ...(input.removedAttachments && input.removedAttachments.length > 0 && {
770
+ attachments: {
771
+ deleteMany: {
772
+ id: { in: input.removedAttachments }
688
773
  }
689
774
  }
690
- }
691
- },
692
- attachments: {
693
- select: {
694
- id: true,
695
- name: true,
696
- type: true,
697
- thumbnail: true,
698
- size: true,
699
- path: true,
700
- uploadedAt: true,
701
- thumbnailId: true,
702
- }
775
+ }),
703
776
  },
704
- section: true,
705
- teacher: true,
706
- class: true
707
- }
777
+ select: {
778
+ id: true,
779
+ title: true,
780
+ instructions: true,
781
+ dueDate: true,
782
+ maxGrade: true,
783
+ graded: true,
784
+ weight: true,
785
+ type: true,
786
+ createdAt: true,
787
+ markSchemeId: true,
788
+ submissions: {
789
+ select: {
790
+ student: {
791
+ select: {
792
+ id: true,
793
+ username: true
794
+ }
795
+ }
796
+ }
797
+ },
798
+ attachments: {
799
+ select: {
800
+ id: true,
801
+ name: true,
802
+ type: true,
803
+ thumbnail: true,
804
+ size: true,
805
+ path: true,
806
+ uploadedAt: true,
807
+ thumbnailId: true,
808
+ }
809
+ },
810
+ section: true,
811
+ teacher: true,
812
+ class: true
813
+ }
814
+ });
815
+ }, {
816
+ maxWait: 5000, // 5 seconds max wait time
817
+ timeout: 10000, // 10 seconds timeout
708
818
  });
709
819
 
710
-
711
- if (assignment.markSchemeId) {
712
- const rubric = await prisma.markScheme.findUnique({
713
- where: { id: assignment.markSchemeId },
820
+ // Handle rubric max grade calculation outside transaction
821
+ if (updatedAssignment.markSchemeId) {
822
+ prisma.markScheme.findUnique({
823
+ where: { id: updatedAssignment.markSchemeId },
714
824
  select: {
715
825
  structured: true,
716
826
  }
717
- });
718
- const parsedRubric = JSON.parse(rubric?.structured || "{}");
719
- const computedMaxGrade = parsedRubric.criteria.reduce((acc: number, criterion: any) => {
720
- const maxPoints = Math.max(...criterion.levels.map((level: any) => level.points));
721
- return acc + maxPoints;
722
- }, 0);
723
-
724
- await prisma.assignment.update({
725
- where: { id },
726
- data: {
727
- maxGrade: computedMaxGrade,
827
+ }).then(rubric => {
828
+ if (rubric) {
829
+ const parsedRubric = JSON.parse(rubric.structured || "{}");
830
+ const computedMaxGrade = parsedRubric.criteria?.reduce((acc: number, criterion: any) => {
831
+ const maxPoints = Math.max(...criterion.levels.map((level: any) => level.points));
832
+ return acc + maxPoints;
833
+ }, 0) || 0;
834
+
835
+ return prisma.assignment.update({
836
+ where: { id },
837
+ data: {
838
+ maxGrade: computedMaxGrade,
839
+ }
840
+ });
728
841
  }
842
+ }).catch(error => {
843
+ logger.error('Failed to update max grade from rubric:', error);
729
844
  });
730
845
  }
731
846
 
732
-
733
847
  return updatedAssignment;
734
848
  }),
735
849
 
@@ -866,6 +980,12 @@ export const assignmentRouter = createTRPCRouter({
866
980
  username: true
867
981
  }
868
982
  },
983
+ worksheets: {
984
+ select: {
985
+ id: true,
986
+ name: true,
987
+ }
988
+ },
869
989
  class: {
870
990
  select: {
871
991
  id: true,
@@ -955,6 +1075,12 @@ export const assignmentRouter = createTRPCRouter({
955
1075
  structured: true,
956
1076
  }
957
1077
  },
1078
+ worksheets: {
1079
+ select: {
1080
+ id: true,
1081
+ name: true,
1082
+ }
1083
+ },
958
1084
  gradingBoundary: {
959
1085
  select: {
960
1086
  id: true,
@@ -1067,6 +1193,12 @@ export const assignmentRouter = createTRPCRouter({
1067
1193
  id: true,
1068
1194
  structured: true,
1069
1195
  }
1196
+ },
1197
+ worksheets: {
1198
+ select: {
1199
+ id: true,
1200
+ name: true,
1201
+ }
1070
1202
  }
1071
1203
  },
1072
1204
  },
@@ -1096,7 +1228,7 @@ export const assignmentRouter = createTRPCRouter({
1096
1228
  });
1097
1229
  }
1098
1230
 
1099
- const { submissionId, submit, newAttachments, existingFileIds, removedAttachments } = input;
1231
+ const { submissionId, submit, newAttachments, existingFileIds, removedAttachments, extendedResponse } = input;
1100
1232
 
1101
1233
  const submission = await prisma.submission.findFirst({
1102
1234
  where: {
@@ -1266,6 +1398,7 @@ export const assignmentRouter = createTRPCRouter({
1266
1398
  },
1267
1399
  },
1268
1400
  }),
1401
+ ...(extendedResponse !== undefined && { extendedResponse }),
1269
1402
  },
1270
1403
  include: {
1271
1404
  attachments: {