@studious-lms/server 1.2.32 → 1.2.34

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.
@@ -75,6 +75,7 @@ const submissionSchema = z.object({
75
75
  submissionId: z.string(),
76
76
  submit: z.boolean().optional(),
77
77
  newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
78
+ extendedResponse: z.string().optional(),
78
79
  existingFileIds: z.array(z.string()).optional(),
79
80
  removedAttachments: z.array(z.string()).optional(),
80
81
  });
@@ -150,15 +151,38 @@ async function getUnifiedList(tx, classId) {
150
151
  return unified;
151
152
  }
152
153
  // Helper function to normalize unified list to 1..n
154
+ // Updated to batch updates to prevent timeouts with large lists
153
155
  async function normalizeUnifiedList(tx, classId, orderedItems) {
154
- await Promise.all(orderedItems.map((item, index) => {
156
+ const BATCH_SIZE = 10; // Process 10 items at a time to avoid overwhelming the transaction
157
+ // Group items by type for more efficient updates
158
+ const sections = [];
159
+ const assignments = [];
160
+ orderedItems.forEach((item, index) => {
161
+ const orderData = { id: item.id, order: index + 1 };
155
162
  if (item.type === 'section') {
156
- return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
163
+ sections.push(orderData);
157
164
  }
158
165
  else {
159
- return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
166
+ assignments.push(orderData);
167
+ }
168
+ });
169
+ // Process updates in batches
170
+ const processBatch = async (items, type) => {
171
+ for (let i = 0; i < items.length; i += BATCH_SIZE) {
172
+ const batch = items.slice(i, i + BATCH_SIZE);
173
+ await Promise.all(batch.map(item => {
174
+ if (type === 'section') {
175
+ return tx.section.update({ where: { id: item.id }, data: { order: item.order } });
176
+ }
177
+ else {
178
+ return tx.assignment.update({ where: { id: item.id }, data: { order: item.order } });
179
+ }
180
+ }));
160
181
  }
161
- }));
182
+ };
183
+ // Process sections and assignments sequentially to avoid transaction overload
184
+ await processBatch(sections, 'section');
185
+ await processBatch(assignments, 'assignment');
162
186
  }
163
187
  export const assignmentRouter = createTRPCRouter({
164
188
  // Reorder an assignment within the unified list (sections + assignments)
@@ -221,6 +245,9 @@ export const assignmentRouter = createTRPCRouter({
221
245
  // Normalize to 1..n
222
246
  await normalizeUnifiedList(tx, classId, next);
223
247
  return tx.assignment.findUnique({ where: { id: movedId } });
248
+ }, {
249
+ maxWait: 10000, // 10 seconds max wait time
250
+ timeout: 30000, // 30 seconds timeout for reordering operations
224
251
  });
225
252
  return result;
226
253
  }),
@@ -246,6 +273,9 @@ export const assignmentRouter = createTRPCRouter({
246
273
  const unified = await getUnifiedList(tx, current.classId);
247
274
  await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
248
275
  return tx.assignment.findUnique({ where: { id } });
276
+ }, {
277
+ maxWait: 10000, // 10 seconds max wait time
278
+ timeout: 30000, // 30 seconds timeout for reordering operations
249
279
  });
250
280
  return updated;
251
281
  }),
@@ -272,6 +302,9 @@ export const assignmentRouter = createTRPCRouter({
272
302
  // No need to reorder since we're keeping the same position in the unified list
273
303
  // If frontend wants to change position, they should call reorder after move
274
304
  return tx.assignment.findUnique({ where: { id } });
305
+ }, {
306
+ maxWait: 5000, // 5 seconds max wait time
307
+ timeout: 10000, // 10 seconds timeout
275
308
  });
276
309
  return updated;
277
310
  }),
@@ -285,40 +318,51 @@ export const assignmentRouter = createTRPCRouter({
285
318
  message: "User must be authenticated",
286
319
  });
287
320
  }
288
- // Get all students in the class
289
- const classData = await prisma.class.findUnique({
290
- where: { id: classId },
291
- include: {
292
- students: {
293
- select: { id: true }
321
+ // Pre-fetch data needed for the transaction (outside transaction scope)
322
+ const [classData, rubricData] = await Promise.all([
323
+ prisma.class.findUnique({
324
+ where: { id: classId },
325
+ include: {
326
+ students: {
327
+ select: { id: true }
328
+ }
294
329
  }
295
- }
296
- });
330
+ }),
331
+ markSchemeId ? prisma.markScheme.findUnique({
332
+ where: { id: markSchemeId },
333
+ select: {
334
+ structured: true,
335
+ }
336
+ }) : null
337
+ ]);
297
338
  if (!classData) {
298
339
  throw new TRPCError({
299
340
  code: "NOT_FOUND",
300
341
  message: "Class not found",
301
342
  });
302
343
  }
344
+ // Calculate max grade outside transaction
303
345
  let computedMaxGrade = maxGrade;
304
- if (markSchemeId) {
305
- const rubric = await prisma.markScheme.findUnique({
306
- where: { id: markSchemeId },
307
- select: {
308
- structured: true,
309
- }
310
- });
311
- const parsedRubric = JSON.parse(rubric?.structured || "{}");
312
- // Calculate max grade from rubric criteria levels
346
+ if (markSchemeId && rubricData) {
347
+ const parsedRubric = JSON.parse(rubricData.structured || "{}");
313
348
  computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
314
349
  const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
315
350
  return acc + maxPoints;
316
351
  }, 0);
317
352
  }
318
- console.log(markSchemeId, gradingBoundaryId);
319
- // Create assignment and place at top of its scope within a single transaction
353
+ console.log(studentIds);
354
+ // Prepare submission data outside transaction
355
+ const submissionData = studentIds && studentIds.length > 0
356
+ ? studentIds.map((studentId) => ({
357
+ student: { connect: { id: studentId } }
358
+ }))
359
+ : classData.students.map((student) => ({
360
+ student: { connect: { id: student.id } }
361
+ }));
320
362
  const teacherId = ctx.user.id;
363
+ // Minimal transaction - only for critical operations
321
364
  const assignment = await prisma.$transaction(async (tx) => {
365
+ // Create assignment with order 0 (will be at top)
322
366
  const created = await tx.assignment.create({
323
367
  data: {
324
368
  title,
@@ -328,7 +372,7 @@ export const assignmentRouter = createTRPCRouter({
328
372
  graded,
329
373
  weight,
330
374
  type,
331
- ...(aiPolicyLevel !== undefined && { aiPolicyLevel }),
375
+ ...(aiPolicyLevel && { aiPolicyLevel }),
332
376
  acceptFiles,
333
377
  acceptExtendedResponse,
334
378
  acceptWorksheet,
@@ -343,7 +387,7 @@ export const assignmentRouter = createTRPCRouter({
343
387
  connect: studentIds.map(id => ({ id }))
344
388
  }
345
389
  }),
346
- order: 1,
390
+ order: 0, // Set to 0 to place at top
347
391
  inProgress: inProgress || false,
348
392
  class: {
349
393
  connect: { id: classId }
@@ -364,11 +408,7 @@ export const assignmentRouter = createTRPCRouter({
364
408
  }
365
409
  }),
366
410
  submissions: {
367
- create: classData.students.map((student) => ({
368
- student: {
369
- connect: { id: student.id }
370
- }
371
- }))
411
+ create: submissionData
372
412
  },
373
413
  teacher: {
374
414
  connect: { id: teacherId }
@@ -410,60 +450,79 @@ export const assignmentRouter = createTRPCRouter({
410
450
  }
411
451
  }
412
452
  });
413
- // Insert new assignment at top of unified list and normalize
414
- const unified = await getUnifiedList(tx, classId);
415
- const withoutNew = unified.filter(item => !(item.id === created.id && item.type === 'assignment'));
416
- const reindexed = [{ id: created.id, type: 'assignment' }, ...withoutNew.map(item => ({ id: item.id, type: item.type }))];
417
- await normalizeUnifiedList(tx, classId, reindexed);
453
+ // Optimized reordering - only increment existing items by 1
454
+ // This is much faster than reordering everything
455
+ await tx.assignment.updateMany({
456
+ where: {
457
+ classId: classId,
458
+ id: { not: created.id },
459
+ },
460
+ data: {
461
+ order: { increment: 1 }
462
+ }
463
+ });
464
+ await tx.section.updateMany({
465
+ where: {
466
+ classId: classId,
467
+ },
468
+ data: {
469
+ order: { increment: 1 }
470
+ }
471
+ });
472
+ // Update the created assignment to have order 1
473
+ await tx.assignment.update({
474
+ where: { id: created.id },
475
+ data: { order: 1 }
476
+ });
418
477
  return created;
478
+ }, {
479
+ maxWait: 10000, // Increased to 10 seconds max wait time
480
+ timeout: 20000, // Increased to 20 seconds timeout for safety
419
481
  });
420
- // NOTE: Files are now handled via direct upload endpoints
421
- // The files field in the schema is for metadata only
422
- // Actual file uploads should use getAssignmentUploadUrls endpoint
423
- let uploadedFiles = [];
482
+ // Handle file operations outside transaction
483
+ const fileOperations = [];
484
+ // Process direct upload files
424
485
  if (files && files.length > 0) {
425
- // Create direct upload files instead of processing base64
426
- uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id);
427
- }
428
- // Update assignment with new file attachments
429
- if (uploadedFiles.length > 0) {
430
- await prisma.assignment.update({
431
- where: { id: assignment.id },
432
- data: {
433
- attachments: {
434
- create: uploadedFiles.map(file => ({
435
- name: file.name,
436
- type: file.type,
437
- size: file.size,
438
- path: file.path,
439
- ...(file.thumbnailId && {
440
- thumbnail: {
441
- connect: { id: file.thumbnailId }
442
- }
443
- })
444
- }))
445
- }
486
+ fileOperations.push(createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id)
487
+ .then(uploadedFiles => {
488
+ if (uploadedFiles.length > 0) {
489
+ return prisma.assignment.update({
490
+ where: { id: assignment.id },
491
+ data: {
492
+ attachments: {
493
+ create: uploadedFiles.map(file => ({
494
+ name: file.name,
495
+ type: file.type,
496
+ size: file.size,
497
+ path: file.path,
498
+ }))
499
+ }
500
+ }
501
+ });
446
502
  }
447
- });
503
+ }));
448
504
  }
449
- // Connect existing files if provided
505
+ // Connect existing files
450
506
  if (existingFileIds && existingFileIds.length > 0) {
451
- await prisma.assignment.update({
507
+ fileOperations.push(prisma.assignment.update({
452
508
  where: { id: assignment.id },
453
509
  data: {
454
510
  attachments: {
455
511
  connect: existingFileIds.map(fileId => ({ id: fileId }))
456
512
  }
457
513
  }
458
- });
514
+ }));
459
515
  }
516
+ // Execute file operations in parallel
517
+ await Promise.all(fileOperations);
518
+ // Send notifications asynchronously (non-blocking)
460
519
  sendNotifications(classData.students.map(student => student.id), {
461
520
  title: `🔔 New assignment for ${classData.name}`,
462
521
  content: `The assignment "${title}" has been created in ${classData.name}.\n
463
522
  Due date: ${new Date(dueDate).toLocaleDateString()}.
464
523
  [Link to assignment](/class/${classId}/assignments/${assignment.id})`
465
524
  }).catch(error => {
466
- logger.error('Failed to send assignment notifications:');
525
+ logger.error('Failed to send assignment notifications:', error);
467
526
  });
468
527
  return assignment;
469
528
  }),
@@ -477,185 +536,225 @@ export const assignmentRouter = createTRPCRouter({
477
536
  message: "User must be authenticated",
478
537
  });
479
538
  }
480
- // Get the assignment with current attachments
481
- const assignment = await prisma.assignment.findFirst({
482
- where: {
483
- id,
484
- teacherId: ctx.user.id,
485
- },
486
- include: {
487
- attachments: {
488
- select: {
489
- id: true,
490
- name: true,
491
- type: true,
492
- path: true,
493
- size: true,
494
- uploadStatus: true,
495
- thumbnail: {
496
- select: {
497
- path: true
539
+ // Pre-fetch all necessary data outside transaction
540
+ const [assignment, classData] = await Promise.all([
541
+ 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
558
+ }
498
559
  }
560
+ },
561
+ },
562
+ class: {
563
+ select: {
564
+ id: true,
565
+ name: true
499
566
  }
500
567
  },
568
+ markScheme: true,
501
569
  },
502
- class: {
503
- select: {
504
- id: true,
505
- name: true
570
+ }),
571
+ prisma.class.findFirst({
572
+ where: {
573
+ assignments: {
574
+ some: { id }
506
575
  }
507
576
  },
508
- },
509
- });
577
+ include: {
578
+ students: {
579
+ select: { id: true }
580
+ }
581
+ }
582
+ })
583
+ ]);
510
584
  if (!assignment) {
511
585
  throw new TRPCError({
512
586
  code: "NOT_FOUND",
513
587
  message: "Assignment not found",
514
588
  });
515
589
  }
516
- // NOTE: Files are now handled via direct upload endpoints
517
- let uploadedFiles = [];
518
- if (files && files.length > 0) {
519
- // Create direct upload files instead of processing base64
520
- uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
521
- }
522
- // Delete removed attachments from storage before updating database
590
+ // Prepare submission data outside transaction if needed
591
+ const submissionData = studentIds && studentIds.length > 0
592
+ ? studentIds.map((studentId) => ({
593
+ student: { connect: { id: studentId } }
594
+ }))
595
+ : classData?.students.map((student) => ({
596
+ student: { connect: { id: student.id } }
597
+ }));
598
+ // Handle file deletion operations outside transaction
599
+ const fileDeletionPromises = [];
523
600
  if (input.removedAttachments && input.removedAttachments.length > 0) {
524
601
  const filesToDelete = assignment.attachments.filter((file) => input.removedAttachments.includes(file.id));
525
- // Delete files from storage (only if they were actually uploaded)
526
- await Promise.all(filesToDelete.map(async (file) => {
527
- try {
528
- // Only delete from GCS if the file was successfully uploaded
529
- if (file.uploadStatus === 'COMPLETED') {
530
- // Delete the main file
531
- await deleteFile(file.path);
532
- // Delete thumbnail if it exists
533
- if (file.thumbnail?.path) {
534
- await deleteFile(file.thumbnail.path);
535
- }
602
+ filesToDelete.forEach((file) => {
603
+ if (file.uploadStatus === 'COMPLETED') {
604
+ fileDeletionPromises.push(deleteFile(file.path).catch(error => {
605
+ console.warn(`Failed to delete file ${file.path}:`, error);
606
+ }));
607
+ if (file.thumbnail?.path) {
608
+ fileDeletionPromises.push(deleteFile(file.thumbnail.path).catch(error => {
609
+ console.warn(`Failed to delete thumbnail ${file.thumbnail.path}:`, error);
610
+ }));
536
611
  }
537
612
  }
538
- catch (error) {
539
- console.warn(`Failed to delete file ${file.path}:`, error);
540
- }
541
- }));
613
+ });
542
614
  }
543
- // Update assignment
544
- const updatedAssignment = await prisma.assignment.update({
545
- where: { id },
546
- data: {
547
- ...(title && { title }),
548
- ...(instructions && { instructions }),
549
- ...(dueDate && { dueDate: new Date(dueDate) }),
550
- ...(maxGrade && { maxGrade }),
551
- ...(graded !== undefined && { graded }),
552
- ...(weight && { weight }),
553
- ...(type && { type }),
554
- ...(inProgress !== undefined && { inProgress }),
555
- ...(acceptFiles !== undefined && { acceptFiles }),
556
- ...(acceptExtendedResponse !== undefined && { acceptExtendedResponse }),
557
- ...(acceptWorksheet !== undefined && { acceptWorksheet }),
558
- ...(gradeWithAI !== undefined && { gradeWithAI }),
559
- ...(studentIds && {
560
- assignedTo: {
561
- connect: studentIds.map(id => ({ id }))
562
- }
563
- }),
564
- ...(aiPolicyLevel !== undefined && { aiPolicyLevel }),
565
- ...(sectionId !== undefined && {
566
- section: sectionId ? {
567
- connect: { id: sectionId }
568
- } : {
569
- disconnect: true
570
- }
571
- }),
572
- ...(worksheetIds && {
573
- worksheets: {
574
- connect: worksheetIds.map(id => ({ id }))
575
- }
576
- }),
577
- ...(uploadedFiles.length > 0 && {
578
- attachments: {
579
- create: uploadedFiles.map(file => ({
580
- name: file.name,
581
- type: file.type,
582
- size: file.size,
583
- path: file.path,
584
- ...(file.thumbnailId && {
585
- thumbnail: {
586
- connect: { id: file.thumbnailId }
587
- }
588
- })
589
- }))
590
- }
591
- }),
592
- ...(existingFileIds && existingFileIds.length > 0 && {
593
- attachments: {
594
- connect: existingFileIds.map(fileId => ({ id: fileId }))
595
- }
596
- }),
597
- ...(input.removedAttachments && input.removedAttachments.length > 0 && {
598
- attachments: {
599
- deleteMany: {
600
- id: { in: input.removedAttachments }
615
+ // Execute file deletions in parallel
616
+ await Promise.all(fileDeletionPromises);
617
+ // Prepare file upload operations
618
+ let uploadedFiles = [];
619
+ if (files && files.length > 0) {
620
+ uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
621
+ }
622
+ // Minimal transaction for database update
623
+ const updatedAssignment = await prisma.$transaction(async (tx) => {
624
+ return tx.assignment.update({
625
+ where: { id },
626
+ data: {
627
+ ...(title && { title }),
628
+ ...(instructions && { instructions }),
629
+ ...(dueDate && { dueDate: new Date(dueDate) }),
630
+ ...(maxGrade && { maxGrade }),
631
+ ...(graded !== undefined && { graded }),
632
+ ...(weight && { weight }),
633
+ ...(type && { type }),
634
+ ...(inProgress !== undefined && { inProgress }),
635
+ ...(acceptFiles !== undefined && { acceptFiles }),
636
+ ...(acceptExtendedResponse !== undefined && { acceptExtendedResponse }),
637
+ ...(acceptWorksheet !== undefined && { acceptWorksheet }),
638
+ ...(gradeWithAI !== undefined && { gradeWithAI }),
639
+ ...(studentIds && {
640
+ assignedTo: {
641
+ set: [], // Clear existing
642
+ connect: studentIds.map(id => ({ id }))
601
643
  }
602
- }
603
- }),
604
- },
605
- select: {
606
- id: true,
607
- title: true,
608
- instructions: true,
609
- dueDate: true,
610
- maxGrade: true,
611
- graded: true,
612
- weight: true,
613
- type: true,
614
- createdAt: true,
615
- submissions: {
616
- select: {
617
- student: {
618
- select: {
619
- id: true,
620
- username: true
644
+ }),
645
+ ...(submissionData && {
646
+ submissions: {
647
+ deleteMany: {}, // Clear existing submissions
648
+ create: submissionData
649
+ }
650
+ }),
651
+ ...(aiPolicyLevel !== undefined && { aiPolicyLevel }),
652
+ ...(sectionId !== undefined && {
653
+ section: sectionId ? {
654
+ connect: { id: sectionId }
655
+ } : {
656
+ disconnect: true
657
+ }
658
+ }),
659
+ ...(worksheetIds && {
660
+ worksheets: {
661
+ set: [], // Clear existing
662
+ connect: worksheetIds.map(id => ({ id }))
663
+ }
664
+ }),
665
+ ...(uploadedFiles.length > 0 && {
666
+ attachments: {
667
+ create: uploadedFiles.map(file => ({
668
+ name: file.name,
669
+ type: file.type,
670
+ size: file.size,
671
+ path: file.path,
672
+ ...(file.thumbnailId && {
673
+ thumbnail: {
674
+ connect: { id: file.thumbnailId }
675
+ }
676
+ })
677
+ }))
678
+ }
679
+ }),
680
+ ...(existingFileIds && existingFileIds.length > 0 && {
681
+ attachments: {
682
+ connect: existingFileIds.map(fileId => ({ id: fileId }))
683
+ }
684
+ }),
685
+ ...(input.removedAttachments && input.removedAttachments.length > 0 && {
686
+ attachments: {
687
+ deleteMany: {
688
+ id: { in: input.removedAttachments }
621
689
  }
622
690
  }
623
- }
624
- },
625
- attachments: {
626
- select: {
627
- id: true,
628
- name: true,
629
- type: true,
630
- thumbnail: true,
631
- size: true,
632
- path: true,
633
- uploadedAt: true,
634
- thumbnailId: true,
635
- }
691
+ }),
636
692
  },
637
- section: true,
638
- teacher: true,
639
- class: true
640
- }
693
+ select: {
694
+ id: true,
695
+ title: true,
696
+ instructions: true,
697
+ dueDate: true,
698
+ maxGrade: true,
699
+ graded: true,
700
+ weight: true,
701
+ type: true,
702
+ createdAt: true,
703
+ markSchemeId: true,
704
+ submissions: {
705
+ select: {
706
+ student: {
707
+ select: {
708
+ id: true,
709
+ username: true
710
+ }
711
+ }
712
+ }
713
+ },
714
+ attachments: {
715
+ select: {
716
+ id: true,
717
+ name: true,
718
+ type: true,
719
+ thumbnail: true,
720
+ size: true,
721
+ path: true,
722
+ uploadedAt: true,
723
+ thumbnailId: true,
724
+ }
725
+ },
726
+ section: true,
727
+ teacher: true,
728
+ class: true
729
+ }
730
+ });
731
+ }, {
732
+ maxWait: 5000, // 5 seconds max wait time
733
+ timeout: 10000, // 10 seconds timeout
641
734
  });
642
- if (assignment.markSchemeId) {
643
- const rubric = await prisma.markScheme.findUnique({
644
- where: { id: assignment.markSchemeId },
735
+ // Handle rubric max grade calculation outside transaction
736
+ if (updatedAssignment.markSchemeId) {
737
+ prisma.markScheme.findUnique({
738
+ where: { id: updatedAssignment.markSchemeId },
645
739
  select: {
646
740
  structured: true,
647
741
  }
648
- });
649
- const parsedRubric = JSON.parse(rubric?.structured || "{}");
650
- const computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
651
- const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
652
- return acc + maxPoints;
653
- }, 0);
654
- await prisma.assignment.update({
655
- where: { id },
656
- data: {
657
- maxGrade: computedMaxGrade,
742
+ }).then(rubric => {
743
+ if (rubric) {
744
+ const parsedRubric = JSON.parse(rubric.structured || "{}");
745
+ const computedMaxGrade = parsedRubric.criteria?.reduce((acc, criterion) => {
746
+ const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
747
+ return acc + maxPoints;
748
+ }, 0) || 0;
749
+ return prisma.assignment.update({
750
+ where: { id },
751
+ data: {
752
+ maxGrade: computedMaxGrade,
753
+ }
754
+ });
658
755
  }
756
+ }).catch(error => {
757
+ logger.error('Failed to update max grade from rubric:', error);
659
758
  });
660
759
  }
661
760
  return updatedAssignment;
@@ -783,6 +882,12 @@ export const assignmentRouter = createTRPCRouter({
783
882
  username: true
784
883
  }
785
884
  },
885
+ worksheets: {
886
+ select: {
887
+ id: true,
888
+ name: true,
889
+ }
890
+ },
786
891
  class: {
787
892
  select: {
788
893
  id: true,
@@ -866,6 +971,12 @@ export const assignmentRouter = createTRPCRouter({
866
971
  structured: true,
867
972
  }
868
973
  },
974
+ worksheets: {
975
+ select: {
976
+ id: true,
977
+ name: true,
978
+ }
979
+ },
869
980
  gradingBoundary: {
870
981
  select: {
871
982
  id: true,
@@ -973,6 +1084,12 @@ export const assignmentRouter = createTRPCRouter({
973
1084
  id: true,
974
1085
  structured: true,
975
1086
  }
1087
+ },
1088
+ worksheets: {
1089
+ select: {
1090
+ id: true,
1091
+ name: true,
1092
+ }
976
1093
  }
977
1094
  },
978
1095
  },
@@ -998,7 +1115,7 @@ export const assignmentRouter = createTRPCRouter({
998
1115
  message: "User must be authenticated",
999
1116
  });
1000
1117
  }
1001
- const { submissionId, submit, newAttachments, existingFileIds, removedAttachments } = input;
1118
+ const { submissionId, submit, newAttachments, existingFileIds, removedAttachments, extendedResponse } = input;
1002
1119
  const submission = await prisma.submission.findFirst({
1003
1120
  where: {
1004
1121
  id: submissionId,
@@ -1157,6 +1274,7 @@ export const assignmentRouter = createTRPCRouter({
1157
1274
  },
1158
1275
  },
1159
1276
  }),
1277
+ ...(extendedResponse !== undefined && { extendedResponse }),
1160
1278
  },
1161
1279
  include: {
1162
1280
  attachments: {