@studious-lms/server 1.2.31 → 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.
@@ -22,10 +22,13 @@ const createAssignmentSchema = z.object({
22
22
  dueDate: z.string(),
23
23
  files: z.array(directFileSchema).optional(), // Use direct file schema
24
24
  existingFileIds: z.array(z.string()).optional(),
25
+ aiPolicyLevel: z.number().default(0),
25
26
  acceptFiles: z.boolean().optional(),
26
27
  acceptExtendedResponse: z.boolean().optional(),
27
28
  acceptWorksheet: z.boolean().optional(),
29
+ worksheetIds: z.array(z.string()).optional(),
28
30
  gradeWithAI: z.boolean().optional(),
31
+ studentIds: z.array(z.string()).optional(),
29
32
  maxGrade: z.number().optional(),
30
33
  graded: z.boolean().optional(),
31
34
  weight: z.number().optional(),
@@ -42,10 +45,13 @@ const updateAssignmentSchema = z.object({
42
45
  instructions: z.string().optional(),
43
46
  dueDate: z.string().optional(),
44
47
  files: z.array(directFileSchema).optional(), // Use direct file schema
48
+ aiPolicyLevel: z.number().default(0),
45
49
  acceptFiles: z.boolean().optional(),
46
50
  acceptExtendedResponse: z.boolean().optional(),
47
51
  acceptWorksheet: z.boolean().optional(),
52
+ worksheetIds: z.array(z.string()).optional(),
48
53
  gradeWithAI: z.boolean().optional(),
54
+ studentIds: z.array(z.string()).optional(),
49
55
  existingFileIds: z.array(z.string()).optional(),
50
56
  removedAttachments: z.array(z.string()).optional(),
51
57
  maxGrade: z.number().optional(),
@@ -69,6 +75,7 @@ const submissionSchema = z.object({
69
75
  submissionId: z.string(),
70
76
  submit: z.boolean().optional(),
71
77
  newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
78
+ extendedResponse: z.string().optional(),
72
79
  existingFileIds: z.array(z.string()).optional(),
73
80
  removedAttachments: z.array(z.string()).optional(),
74
81
  });
@@ -144,15 +151,38 @@ async function getUnifiedList(tx, classId) {
144
151
  return unified;
145
152
  }
146
153
  // Helper function to normalize unified list to 1..n
154
+ // Updated to batch updates to prevent timeouts with large lists
147
155
  async function normalizeUnifiedList(tx, classId, orderedItems) {
148
- 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 };
149
162
  if (item.type === 'section') {
150
- return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
163
+ sections.push(orderData);
151
164
  }
152
165
  else {
153
- 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
+ }));
154
181
  }
155
- }));
182
+ };
183
+ // Process sections and assignments sequentially to avoid transaction overload
184
+ await processBatch(sections, 'section');
185
+ await processBatch(assignments, 'assignment');
156
186
  }
157
187
  export const assignmentRouter = createTRPCRouter({
158
188
  // Reorder an assignment within the unified list (sections + assignments)
@@ -215,6 +245,9 @@ export const assignmentRouter = createTRPCRouter({
215
245
  // Normalize to 1..n
216
246
  await normalizeUnifiedList(tx, classId, next);
217
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
218
251
  });
219
252
  return result;
220
253
  }),
@@ -240,6 +273,9 @@ export const assignmentRouter = createTRPCRouter({
240
273
  const unified = await getUnifiedList(tx, current.classId);
241
274
  await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
242
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
243
279
  });
244
280
  return updated;
245
281
  }),
@@ -266,53 +302,67 @@ export const assignmentRouter = createTRPCRouter({
266
302
  // No need to reorder since we're keeping the same position in the unified list
267
303
  // If frontend wants to change position, they should call reorder after move
268
304
  return tx.assignment.findUnique({ where: { id } });
305
+ }, {
306
+ maxWait: 5000, // 5 seconds max wait time
307
+ timeout: 10000, // 10 seconds timeout
269
308
  });
270
309
  return updated;
271
310
  }),
272
311
  create: protectedProcedure
273
312
  .input(createAssignmentSchema)
274
313
  .mutation(async ({ ctx, input }) => {
275
- const { classId, title, instructions, dueDate, files, existingFileIds, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
314
+ const { classId, title, instructions, dueDate, files, existingFileIds, aiPolicyLevel, acceptFiles, acceptExtendedResponse, acceptWorksheet, worksheetIds, gradeWithAI, studentIds, maxGrade, graded, weight, sectionId, type, markSchemeId, gradingBoundaryId, inProgress } = input;
276
315
  if (!ctx.user) {
277
316
  throw new TRPCError({
278
317
  code: "UNAUTHORIZED",
279
318
  message: "User must be authenticated",
280
319
  });
281
320
  }
282
- // Get all students in the class
283
- const classData = await prisma.class.findUnique({
284
- where: { id: classId },
285
- include: {
286
- students: {
287
- 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
+ }
288
329
  }
289
- }
290
- });
330
+ }),
331
+ markSchemeId ? prisma.markScheme.findUnique({
332
+ where: { id: markSchemeId },
333
+ select: {
334
+ structured: true,
335
+ }
336
+ }) : null
337
+ ]);
291
338
  if (!classData) {
292
339
  throw new TRPCError({
293
340
  code: "NOT_FOUND",
294
341
  message: "Class not found",
295
342
  });
296
343
  }
344
+ // Calculate max grade outside transaction
297
345
  let computedMaxGrade = maxGrade;
298
- if (markSchemeId) {
299
- const rubric = await prisma.markScheme.findUnique({
300
- where: { id: markSchemeId },
301
- select: {
302
- structured: true,
303
- }
304
- });
305
- const parsedRubric = JSON.parse(rubric?.structured || "{}");
306
- // Calculate max grade from rubric criteria levels
346
+ if (markSchemeId && rubricData) {
347
+ const parsedRubric = JSON.parse(rubricData.structured || "{}");
307
348
  computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
308
349
  const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
309
350
  return acc + maxPoints;
310
351
  }, 0);
311
352
  }
312
- console.log(markSchemeId, gradingBoundaryId);
313
- // 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
+ }));
314
362
  const teacherId = ctx.user.id;
363
+ // Minimal transaction - only for critical operations
315
364
  const assignment = await prisma.$transaction(async (tx) => {
365
+ // Create assignment with order 0 (will be at top)
316
366
  const created = await tx.assignment.create({
317
367
  data: {
318
368
  title,
@@ -322,11 +372,22 @@ export const assignmentRouter = createTRPCRouter({
322
372
  graded,
323
373
  weight,
324
374
  type,
375
+ ...(aiPolicyLevel && { aiPolicyLevel }),
325
376
  acceptFiles,
326
377
  acceptExtendedResponse,
327
378
  acceptWorksheet,
379
+ ...(worksheetIds && {
380
+ worksheets: {
381
+ connect: worksheetIds.map(id => ({ id }))
382
+ }
383
+ }),
328
384
  gradeWithAI,
329
- order: 1,
385
+ ...(studentIds && {
386
+ assignedTo: {
387
+ connect: studentIds.map(id => ({ id }))
388
+ }
389
+ }),
390
+ order: 0, // Set to 0 to place at top
330
391
  inProgress: inProgress || false,
331
392
  class: {
332
393
  connect: { id: classId }
@@ -347,11 +408,7 @@ export const assignmentRouter = createTRPCRouter({
347
408
  }
348
409
  }),
349
410
  submissions: {
350
- create: classData.students.map((student) => ({
351
- student: {
352
- connect: { id: student.id }
353
- }
354
- }))
411
+ create: submissionData
355
412
  },
356
413
  teacher: {
357
414
  connect: { id: teacherId }
@@ -393,241 +450,311 @@ export const assignmentRouter = createTRPCRouter({
393
450
  }
394
451
  }
395
452
  });
396
- // Insert new assignment at top of unified list and normalize
397
- const unified = await getUnifiedList(tx, classId);
398
- const withoutNew = unified.filter(item => !(item.id === created.id && item.type === 'assignment'));
399
- const reindexed = [{ id: created.id, type: 'assignment' }, ...withoutNew.map(item => ({ id: item.id, type: item.type }))];
400
- 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
+ });
401
477
  return created;
478
+ }, {
479
+ maxWait: 10000, // Increased to 10 seconds max wait time
480
+ timeout: 20000, // Increased to 20 seconds timeout for safety
402
481
  });
403
- // NOTE: Files are now handled via direct upload endpoints
404
- // The files field in the schema is for metadata only
405
- // Actual file uploads should use getAssignmentUploadUrls endpoint
406
- let uploadedFiles = [];
482
+ // Handle file operations outside transaction
483
+ const fileOperations = [];
484
+ // Process direct upload files
407
485
  if (files && files.length > 0) {
408
- // Create direct upload files instead of processing base64
409
- uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id);
410
- }
411
- // Update assignment with new file attachments
412
- if (uploadedFiles.length > 0) {
413
- await prisma.assignment.update({
414
- where: { id: assignment.id },
415
- data: {
416
- attachments: {
417
- create: uploadedFiles.map(file => ({
418
- name: file.name,
419
- type: file.type,
420
- size: file.size,
421
- path: file.path,
422
- ...(file.thumbnailId && {
423
- thumbnail: {
424
- connect: { id: file.thumbnailId }
425
- }
426
- })
427
- }))
428
- }
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
+ });
429
502
  }
430
- });
503
+ }));
431
504
  }
432
- // Connect existing files if provided
505
+ // Connect existing files
433
506
  if (existingFileIds && existingFileIds.length > 0) {
434
- await prisma.assignment.update({
507
+ fileOperations.push(prisma.assignment.update({
435
508
  where: { id: assignment.id },
436
509
  data: {
437
510
  attachments: {
438
511
  connect: existingFileIds.map(fileId => ({ id: fileId }))
439
512
  }
440
513
  }
441
- });
514
+ }));
442
515
  }
516
+ // Execute file operations in parallel
517
+ await Promise.all(fileOperations);
518
+ // Send notifications asynchronously (non-blocking)
443
519
  sendNotifications(classData.students.map(student => student.id), {
444
520
  title: `🔔 New assignment for ${classData.name}`,
445
521
  content: `The assignment "${title}" has been created in ${classData.name}.\n
446
522
  Due date: ${new Date(dueDate).toLocaleDateString()}.
447
523
  [Link to assignment](/class/${classId}/assignments/${assignment.id})`
448
524
  }).catch(error => {
449
- logger.error('Failed to send assignment notifications:');
525
+ logger.error('Failed to send assignment notifications:', error);
450
526
  });
451
527
  return assignment;
452
528
  }),
453
529
  update: protectedProcedure
454
530
  .input(updateAssignmentSchema)
455
531
  .mutation(async ({ ctx, input }) => {
456
- const { id, title, instructions, dueDate, files, existingFileIds, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI } = input;
532
+ const { id, title, instructions, dueDate, files, existingFileIds, worksheetIds, aiPolicyLevel, maxGrade, graded, weight, sectionId, type, inProgress, acceptFiles, acceptExtendedResponse, acceptWorksheet, gradeWithAI, studentIds } = input;
457
533
  if (!ctx.user) {
458
534
  throw new TRPCError({
459
535
  code: "UNAUTHORIZED",
460
536
  message: "User must be authenticated",
461
537
  });
462
538
  }
463
- // Get the assignment with current attachments
464
- const assignment = await prisma.assignment.findFirst({
465
- where: {
466
- id,
467
- teacherId: ctx.user.id,
468
- },
469
- include: {
470
- attachments: {
471
- select: {
472
- id: true,
473
- name: true,
474
- type: true,
475
- path: true,
476
- size: true,
477
- uploadStatus: true,
478
- thumbnail: {
479
- select: {
480
- 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
+ }
481
559
  }
560
+ },
561
+ },
562
+ class: {
563
+ select: {
564
+ id: true,
565
+ name: true
482
566
  }
483
567
  },
568
+ markScheme: true,
484
569
  },
485
- class: {
486
- select: {
487
- id: true,
488
- name: true
570
+ }),
571
+ prisma.class.findFirst({
572
+ where: {
573
+ assignments: {
574
+ some: { id }
489
575
  }
490
576
  },
491
- },
492
- });
577
+ include: {
578
+ students: {
579
+ select: { id: true }
580
+ }
581
+ }
582
+ })
583
+ ]);
493
584
  if (!assignment) {
494
585
  throw new TRPCError({
495
586
  code: "NOT_FOUND",
496
587
  message: "Assignment not found",
497
588
  });
498
589
  }
499
- // NOTE: Files are now handled via direct upload endpoints
500
- let uploadedFiles = [];
501
- if (files && files.length > 0) {
502
- // Create direct upload files instead of processing base64
503
- uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
504
- }
505
- // 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 = [];
506
600
  if (input.removedAttachments && input.removedAttachments.length > 0) {
507
601
  const filesToDelete = assignment.attachments.filter((file) => input.removedAttachments.includes(file.id));
508
- // Delete files from storage (only if they were actually uploaded)
509
- await Promise.all(filesToDelete.map(async (file) => {
510
- try {
511
- // Only delete from GCS if the file was successfully uploaded
512
- if (file.uploadStatus === 'COMPLETED') {
513
- // Delete the main file
514
- await deleteFile(file.path);
515
- // Delete thumbnail if it exists
516
- if (file.thumbnail?.path) {
517
- await deleteFile(file.thumbnail.path);
518
- }
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
+ }));
519
611
  }
520
612
  }
521
- catch (error) {
522
- console.warn(`Failed to delete file ${file.path}:`, error);
523
- }
524
- }));
613
+ });
525
614
  }
526
- // Update assignment
527
- const updatedAssignment = await prisma.assignment.update({
528
- where: { id },
529
- data: {
530
- ...(title && { title }),
531
- ...(instructions && { instructions }),
532
- ...(dueDate && { dueDate: new Date(dueDate) }),
533
- ...(maxGrade && { maxGrade }),
534
- ...(graded !== undefined && { graded }),
535
- ...(weight && { weight }),
536
- ...(type && { type }),
537
- ...(inProgress !== undefined && { inProgress }),
538
- ...(acceptFiles !== undefined && { acceptFiles }),
539
- ...(acceptExtendedResponse !== undefined && { acceptExtendedResponse }),
540
- ...(acceptWorksheet !== undefined && { acceptWorksheet }),
541
- ...(gradeWithAI !== undefined && { gradeWithAI }),
542
- ...(sectionId !== undefined && {
543
- section: sectionId ? {
544
- connect: { id: sectionId }
545
- } : {
546
- disconnect: true
547
- }
548
- }),
549
- ...(uploadedFiles.length > 0 && {
550
- attachments: {
551
- create: uploadedFiles.map(file => ({
552
- name: file.name,
553
- type: file.type,
554
- size: file.size,
555
- path: file.path,
556
- ...(file.thumbnailId && {
557
- thumbnail: {
558
- connect: { id: file.thumbnailId }
559
- }
560
- })
561
- }))
562
- }
563
- }),
564
- ...(existingFileIds && existingFileIds.length > 0 && {
565
- attachments: {
566
- connect: existingFileIds.map(fileId => ({ id: fileId }))
567
- }
568
- }),
569
- ...(input.removedAttachments && input.removedAttachments.length > 0 && {
570
- attachments: {
571
- deleteMany: {
572
- 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 }))
573
643
  }
574
- }
575
- }),
576
- },
577
- select: {
578
- id: true,
579
- title: true,
580
- instructions: true,
581
- dueDate: true,
582
- maxGrade: true,
583
- graded: true,
584
- weight: true,
585
- type: true,
586
- createdAt: true,
587
- submissions: {
588
- select: {
589
- student: {
590
- select: {
591
- id: true,
592
- 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 }
593
689
  }
594
690
  }
595
- }
596
- },
597
- attachments: {
598
- select: {
599
- id: true,
600
- name: true,
601
- type: true,
602
- thumbnail: true,
603
- size: true,
604
- path: true,
605
- uploadedAt: true,
606
- thumbnailId: true,
607
- }
691
+ }),
608
692
  },
609
- section: true,
610
- teacher: true,
611
- class: true
612
- }
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
613
734
  });
614
- if (assignment.markSchemeId) {
615
- const rubric = await prisma.markScheme.findUnique({
616
- 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 },
617
739
  select: {
618
740
  structured: true,
619
741
  }
620
- });
621
- const parsedRubric = JSON.parse(rubric?.structured || "{}");
622
- const computedMaxGrade = parsedRubric.criteria.reduce((acc, criterion) => {
623
- const maxPoints = Math.max(...criterion.levels.map((level) => level.points));
624
- return acc + maxPoints;
625
- }, 0);
626
- await prisma.assignment.update({
627
- where: { id },
628
- data: {
629
- 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
+ });
630
755
  }
756
+ }).catch(error => {
757
+ logger.error('Failed to update max grade from rubric:', error);
631
758
  });
632
759
  }
633
760
  return updatedAssignment;
@@ -755,6 +882,12 @@ export const assignmentRouter = createTRPCRouter({
755
882
  username: true
756
883
  }
757
884
  },
885
+ worksheets: {
886
+ select: {
887
+ id: true,
888
+ name: true,
889
+ }
890
+ },
758
891
  class: {
759
892
  select: {
760
893
  id: true,
@@ -838,6 +971,12 @@ export const assignmentRouter = createTRPCRouter({
838
971
  structured: true,
839
972
  }
840
973
  },
974
+ worksheets: {
975
+ select: {
976
+ id: true,
977
+ name: true,
978
+ }
979
+ },
841
980
  gradingBoundary: {
842
981
  select: {
843
982
  id: true,
@@ -945,6 +1084,12 @@ export const assignmentRouter = createTRPCRouter({
945
1084
  id: true,
946
1085
  structured: true,
947
1086
  }
1087
+ },
1088
+ worksheets: {
1089
+ select: {
1090
+ id: true,
1091
+ name: true,
1092
+ }
948
1093
  }
949
1094
  },
950
1095
  },
@@ -970,7 +1115,7 @@ export const assignmentRouter = createTRPCRouter({
970
1115
  message: "User must be authenticated",
971
1116
  });
972
1117
  }
973
- const { submissionId, submit, newAttachments, existingFileIds, removedAttachments } = input;
1118
+ const { submissionId, submit, newAttachments, existingFileIds, removedAttachments, extendedResponse } = input;
974
1119
  const submission = await prisma.submission.findFirst({
975
1120
  where: {
976
1121
  id: submissionId,
@@ -1129,6 +1274,7 @@ export const assignmentRouter = createTRPCRouter({
1129
1274
  },
1130
1275
  },
1131
1276
  }),
1277
+ ...(extendedResponse !== undefined && { extendedResponse }),
1132
1278
  },
1133
1279
  include: {
1134
1280
  attachments: {