@studious-lms/server 1.3.0 → 1.4.1

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 (77) hide show
  1. package/dist/models/class.d.ts +24 -2
  2. package/dist/models/class.d.ts.map +1 -1
  3. package/dist/models/class.js +180 -81
  4. package/dist/models/class.js.map +1 -1
  5. package/dist/models/worksheet.d.ts +34 -34
  6. package/dist/pipelines/aiLabChat.d.ts +61 -2
  7. package/dist/pipelines/aiLabChat.d.ts.map +1 -1
  8. package/dist/pipelines/aiLabChat.js +204 -172
  9. package/dist/pipelines/aiLabChat.js.map +1 -1
  10. package/dist/pipelines/aiLabChatContract.d.ts +413 -0
  11. package/dist/pipelines/aiLabChatContract.d.ts.map +1 -0
  12. package/dist/pipelines/aiLabChatContract.js +74 -0
  13. package/dist/pipelines/aiLabChatContract.js.map +1 -0
  14. package/dist/pipelines/gradeWorksheet.d.ts +4 -4
  15. package/dist/pipelines/labChatPrompt.d.ts +2 -0
  16. package/dist/pipelines/labChatPrompt.d.ts.map +1 -0
  17. package/dist/pipelines/labChatPrompt.js +72 -0
  18. package/dist/pipelines/labChatPrompt.js.map +1 -0
  19. package/dist/routers/_app.d.ts +284 -56
  20. package/dist/routers/_app.d.ts.map +1 -1
  21. package/dist/routers/_app.js +4 -2
  22. package/dist/routers/_app.js.map +1 -1
  23. package/dist/routers/class.d.ts +24 -3
  24. package/dist/routers/class.d.ts.map +1 -1
  25. package/dist/routers/class.js +3 -3
  26. package/dist/routers/class.js.map +1 -1
  27. package/dist/routers/labChat.d.ts +10 -1
  28. package/dist/routers/labChat.d.ts.map +1 -1
  29. package/dist/routers/labChat.js +6 -3
  30. package/dist/routers/labChat.js.map +1 -1
  31. package/dist/routers/message.d.ts +11 -0
  32. package/dist/routers/message.d.ts.map +1 -1
  33. package/dist/routers/message.js +10 -3
  34. package/dist/routers/message.js.map +1 -1
  35. package/dist/routers/studentProgress.d.ts +75 -0
  36. package/dist/routers/studentProgress.d.ts.map +1 -0
  37. package/dist/routers/studentProgress.js +33 -0
  38. package/dist/routers/studentProgress.js.map +1 -0
  39. package/dist/routers/worksheet.d.ts +24 -24
  40. package/dist/services/class.d.ts +24 -2
  41. package/dist/services/class.d.ts.map +1 -1
  42. package/dist/services/class.js +18 -6
  43. package/dist/services/class.js.map +1 -1
  44. package/dist/services/labChat.d.ts +5 -1
  45. package/dist/services/labChat.d.ts.map +1 -1
  46. package/dist/services/labChat.js +112 -4
  47. package/dist/services/labChat.js.map +1 -1
  48. package/dist/services/message.d.ts +8 -0
  49. package/dist/services/message.d.ts.map +1 -1
  50. package/dist/services/message.js +116 -2
  51. package/dist/services/message.js.map +1 -1
  52. package/dist/services/studentProgress.d.ts +45 -0
  53. package/dist/services/studentProgress.d.ts.map +1 -0
  54. package/dist/services/studentProgress.js +291 -0
  55. package/dist/services/studentProgress.js.map +1 -0
  56. package/dist/services/worksheet.d.ts +18 -18
  57. package/package.json +2 -2
  58. package/prisma/schema.prisma +1 -1
  59. package/sentry.properties +3 -0
  60. package/src/models/class.ts +189 -84
  61. package/src/pipelines/aiLabChat.ts +246 -184
  62. package/src/pipelines/aiLabChatContract.ts +75 -0
  63. package/src/pipelines/labChatPrompt.ts +68 -0
  64. package/src/routers/_app.ts +4 -2
  65. package/src/routers/class.ts +1 -1
  66. package/src/routers/labChat.ts +7 -0
  67. package/src/routers/message.ts +13 -0
  68. package/src/routers/studentProgress.ts +47 -0
  69. package/src/services/class.ts +14 -7
  70. package/src/services/labChat.ts +120 -5
  71. package/src/services/message.ts +142 -0
  72. package/src/services/studentProgress.ts +390 -0
  73. package/tests/lib/aiLabChatContract.test.ts +32 -0
  74. package/tests/pipelines/aiLabChat.test.ts +95 -0
  75. package/tests/routers/studentProgress.test.ts +283 -0
  76. package/tests/utils/aiLabChatPrompt.test.ts +18 -0
  77. package/vitest.unit.config.ts +7 -1
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { Assignment, Class, Folder, GradingBoundary, MarkScheme, Section, Worksheet } from "@prisma/client";
8
- import type { PrismaClient } from "@prisma/client";
8
+ import type { PrismaClient, WorksheetQuestion } from "@prisma/client";
9
9
 
10
10
  import { prisma } from "../lib/prisma.js";
11
11
  import { v4 as uuidv4 } from 'uuid';
@@ -417,35 +417,53 @@ async function recursivelyIncludeFiles(folderId: string) {
417
417
  };
418
418
  }
419
419
 
420
+ /** Copy all files in folder tree to GCS (outside transaction). Returns map of sourcePath -> newPath. */
421
+ async function recursivelyCopyFilesToGcs(folderId: string): Promise<Map<string, string>> {
422
+ const pathMap = new Map<string, string>();
423
+ if (!folderId.length) return pathMap;
424
+
425
+ const folder = await prisma.folder.findUnique({
426
+ where: { id: folderId },
427
+ include: {
428
+ files: { select: { path: true, name: true } },
429
+ childFolders: { select: { id: true } },
430
+ },
431
+ });
432
+ if (!folder) return pathMap;
433
+
434
+ for (const file of folder.files) {
435
+ const ext = file.name.split('.').pop() || '';
436
+ const newPath = `imported/${uuidv4()}${ext ? `.${ext}` : ''}`;
437
+ await copyFile(file.path, newPath);
438
+ pathMap.set(file.path, newPath);
439
+ }
440
+ for (const child of folder.childFolders) {
441
+ const childMap = await recursivelyCopyFilesToGcs(child.id);
442
+ childMap.forEach((v, k) => pathMap.set(k, v));
443
+ }
444
+ return pathMap;
445
+ }
446
+
420
447
  async function recursivelyCreateFiles(
421
448
  tx: PrismaClient | Omit<PrismaClient, "$connect" | "$disconnect" | "$on" | "$transaction" | "$use" | "$extends">,
422
449
  folderId: string,
423
- targetFolderId?: string
450
+ targetFolderId: string | undefined,
451
+ pathMap: Map<string, string>
424
452
  ) {
425
- if (!folderId.length) {
426
- return null;
427
- }
453
+ if (!folderId.length) return;
428
454
 
429
455
  const parentFolder = await tx.folder.findUnique({
430
456
  where: { id: folderId },
431
457
  include: {
432
- files: {
433
- select: { id: true, name: true, type: true, size: true, path: true },
434
- },
435
- childFolders: {
436
- select: { id: true, name: true, color: true, parentFolderId: true },
437
- },
458
+ files: { select: { id: true, name: true, type: true, size: true, path: true } },
459
+ childFolders: { select: { id: true, name: true, color: true, parentFolderId: true } },
438
460
  },
439
461
  });
440
- if (!parentFolder) {
441
- return null;
442
- }
462
+ if (!parentFolder) return;
443
463
 
444
- const createdFiles: { count: number } = { count: 0 };
445
464
  for (const file of parentFolder.files) {
446
- const ext = file.name.split('.').pop() || '';
447
- const newPath = `imported/${uuidv4()}${ext ? `.${ext}` : ''}`;
448
- await copyFile(file.path, newPath);
465
+ const newPath = pathMap.get(file.path);
466
+ if (!newPath) continue;
449
467
  await tx.file.create({
450
468
  data: {
451
469
  name: file.name,
@@ -456,10 +474,8 @@ async function recursivelyCreateFiles(
456
474
  ...(targetFolderId && { folder: { connect: { id: targetFolderId } } }),
457
475
  },
458
476
  });
459
- createdFiles.count += 1;
460
477
  }
461
478
 
462
- const childFolders: { files: { count: number }; childFolders: unknown[] }[] = [];
463
479
  for (const childFolder of parentFolder.childFolders) {
464
480
  const newFolder = await tx.folder.create({
465
481
  data: {
@@ -468,14 +484,8 @@ async function recursivelyCreateFiles(
468
484
  parentFolderId: targetFolderId ?? parentFolder.id,
469
485
  },
470
486
  });
471
- const result = await recursivelyCreateFiles(tx, childFolder.id, newFolder.id);
472
- if (result) childFolders.push(result);
487
+ await recursivelyCreateFiles(tx, childFolder.id, newFolder.id, pathMap);
473
488
  }
474
-
475
- return {
476
- files: createdFiles,
477
- childFolders,
478
- };
479
489
  }
480
490
 
481
491
 
@@ -487,6 +497,18 @@ export async function findFullExportableClass(classId: string) {
487
497
  select: {
488
498
  ...assignmentSelect,
489
499
  submissions: false,
500
+ worksheets: { select: { id: true } },
501
+ teacherId: true,
502
+ classId: true,
503
+ sectionId: true,
504
+ markSchemeId: true,
505
+ gradingBoundaryId: true,
506
+ eventId: true,
507
+ acceptFiles: true,
508
+ acceptExtendedResponse: true,
509
+ acceptWorksheet: true,
510
+ gradeWithAI: true,
511
+ aiPolicyLevel: true,
490
512
  },
491
513
  },
492
514
  classFiles: {
@@ -494,10 +516,48 @@ export async function findFullExportableClass(classId: string) {
494
516
  id: true,
495
517
  },
496
518
  },
497
- worksheets: true,
498
- markSchemes: true,
499
- gradingBoundaries: true,
500
- sections: true,
519
+ worksheets: {
520
+ select: {
521
+ id: true,
522
+ name: true,
523
+ classId: true,
524
+ questions: {
525
+ select: {
526
+ type: true,
527
+ question: true,
528
+ answer: true,
529
+ points: true,
530
+ options: true,
531
+ markScheme: true,
532
+ order: true,
533
+ worksheetId: true,
534
+ },
535
+ },
536
+ },
537
+ },
538
+ markSchemes: {
539
+ select: {
540
+ id: true,
541
+ classId: true,
542
+ structured: true,
543
+ },
544
+ },
545
+ gradingBoundaries: {
546
+ select: {
547
+ id: true,
548
+ classId: true,
549
+ structured: true,
550
+ },
551
+ },
552
+ sections: {
553
+ select: {
554
+ id: true,
555
+ name: true,
556
+ classId: true,
557
+ color: true,
558
+ order: true,
559
+ },
560
+ },
501
561
  },
502
562
  });
503
563
 
@@ -516,6 +576,10 @@ export async function findFullExportableClass(classId: string) {
516
576
  }
517
577
 
518
578
  export async function createClassByImport(classId: string, userId: string, year: number, classData: Class & { classFiles: Folder | null }): Promise<string | null> {
579
+ const pathMap = classData.classFiles?.id
580
+ ? await recursivelyCopyFilesToGcs(classData.classFiles.id)
581
+ : new Map<string, string>();
582
+
519
583
  const newClassId = await prisma.$transaction(async (tx) => {
520
584
  const createdClass = await tx.class.create({
521
585
  data: {
@@ -532,67 +596,108 @@ export async function createClassByImport(classId: string, userId: string, year:
532
596
  },
533
597
  });
534
598
 
535
- const assignments = await prisma.assignment.createMany({
536
- data: (classData as unknown as Class & { assignments: Assignment[] }).assignments.map((assignment) => {
537
- const newDate = new Date(assignment.dueDate).setFullYear(year);
538
- return {
539
- ...assignment,
540
- id: assignment.id,
541
- title: assignment.title,
542
- type: assignment.type,
543
- dueDate: newDate as unknown as Date,
544
- };
599
+ const oldSectionIdToNew = new Map<string, string>();
600
+ const oldMarkSchemeIdToNew = new Map<string, string>();
601
+ const oldGradingBoundaryIdToNew = new Map<string, string>();
602
+ const oldWorksheetIdToNew = new Map<string, string>();
603
+
604
+ const sections = (classData as unknown as Class & { sections: Section[] }).sections ?? [];
605
+ await Promise.all(
606
+ sections.map(async (section, index) => {
607
+ const newSection = await tx.section.create({
608
+ data: {
609
+ name: section.name,
610
+ classId: createdClass.id,
611
+ color: section.color,
612
+ order: section.order ?? index,
613
+ },
614
+ });
615
+ oldSectionIdToNew.set(section.id, newSection.id);
616
+ return newSection;
545
617
  })
546
- });
547
-
548
- const worksheets = await prisma.worksheet.createMany({
549
- data: (classData as unknown as Class & { worksheets: Worksheet[] }).worksheets.map((worksheet) => {
550
- return {
551
- ...worksheet,
552
- id: worksheet.id,
553
- name: worksheet.name,
554
- };
618
+ );
619
+
620
+ const markSchemes = (classData as unknown as Class & { markSchemes: MarkScheme[] }).markSchemes ?? [];
621
+ await Promise.all(
622
+ markSchemes.map(async (ms) => {
623
+ const newMs = await tx.markScheme.create({
624
+ data: { classId: createdClass.id, structured: ms.structured },
625
+ });
626
+ oldMarkSchemeIdToNew.set(ms.id, newMs.id);
627
+ return newMs;
555
628
  })
556
- });
557
-
558
- const markSchemes = await prisma.markScheme.createMany({
559
- data: (classData as unknown as Class & { markSchemes: MarkScheme[] }).markSchemes.map((markScheme) => {
560
- return {
561
- ...markScheme,
562
- id: markScheme.id,
563
- structured: markScheme.structured,
564
- };
629
+ );
630
+
631
+ const gradingBoundaries = (classData as unknown as Class & { gradingBoundaries: GradingBoundary[] }).gradingBoundaries ?? [];
632
+ await Promise.all(
633
+ gradingBoundaries.map(async (gb) => {
634
+ const newGb = await tx.gradingBoundary.create({
635
+ data: { classId: createdClass.id, structured: gb.structured },
636
+ });
637
+ oldGradingBoundaryIdToNew.set(gb.id, newGb.id);
638
+ return newGb;
565
639
  })
566
- });
640
+ );
567
641
 
568
- const gradingBoundaries = await prisma.gradingBoundary.createMany({
569
- data: (classData as unknown as Class & { gradingBoundaries: GradingBoundary[] }).gradingBoundaries.map((gradingBoundary) => {
570
- return {
571
- ...gradingBoundary,
572
- id: gradingBoundary.id,
573
- structured: gradingBoundary.structured,
574
- };
575
- })
576
- });
642
+ const worksheets = (classData as unknown as Class & { worksheets: Array<Worksheet & { questions: WorksheetQuestion[] }> }).worksheets ?? [];
643
+ for (const worksheet of worksheets) {
644
+ const newWorksheet = await tx.worksheet.create({
645
+ data: {
646
+ name: worksheet.name,
647
+ classId: createdClass.id,
648
+ questions: {
649
+ create: (worksheet.questions ?? []).map((q, i) => ({
650
+ type: q.type,
651
+ question: q.question,
652
+ answer: q.answer,
653
+ points: q.points ?? 0,
654
+ options: q.options ?? {},
655
+ markScheme: q.markScheme ?? {},
656
+ order: q.order ?? i,
657
+ })),
658
+ },
659
+ },
660
+ });
661
+ oldWorksheetIdToNew.set(worksheet.id, newWorksheet.id);
662
+ }
577
663
 
578
- const sections = await prisma.section.createMany({
579
- data: (classData as unknown as Class & { sections: Section[] }).sections.map((section) => {
580
- return {
581
- ...section,
582
- id: section.id,
583
- name: section.name,
584
- };
585
- })
586
- });
664
+ const assignments = (classData as unknown as Class & { assignments: Array<Assignment & { worksheets?: { id: string }[] }> }).assignments ?? [];
665
+ for (const assignment of assignments) {
666
+ const date = new Date(assignment.dueDate);
667
+ date.setFullYear(year);
668
+ const worksheetIds = (assignment as { worksheets?: { id: string }[] }).worksheets?.map((w) => oldWorksheetIdToNew.get(w.id)).filter(Boolean) as string[] | undefined;
669
+ await tx.assignment.create({
670
+ data: {
671
+ title: assignment.title,
672
+ type: assignment.type,
673
+ dueDate: date,
674
+ instructions: assignment.instructions,
675
+ teacherId: userId,
676
+ classId: createdClass.id,
677
+ sectionId: assignment.sectionId ? oldSectionIdToNew.get(assignment.sectionId) ?? undefined : undefined,
678
+ markSchemeId: assignment.markSchemeId ? oldMarkSchemeIdToNew.get(assignment.markSchemeId) ?? undefined : undefined,
679
+ gradingBoundaryId: assignment.gradingBoundaryId ? oldGradingBoundaryIdToNew.get(assignment.gradingBoundaryId) ?? undefined : undefined,
680
+ weight: assignment.weight ?? 1,
681
+ maxGrade: assignment.maxGrade ?? 0,
682
+ graded: assignment.graded ?? false,
683
+ inProgress: assignment.inProgress ?? false,
684
+ template: assignment.template ?? false,
685
+ acceptFiles: assignment.acceptFiles ?? false,
686
+ acceptExtendedResponse: assignment.acceptExtendedResponse ?? false,
687
+ acceptWorksheet: assignment.acceptWorksheet ?? false,
688
+ gradeWithAI: assignment.gradeWithAI ?? false,
689
+ aiPolicyLevel: assignment.aiPolicyLevel ?? 0,
690
+ ...(worksheetIds?.length && { worksheets: { connect: worksheetIds.map((id) => ({ id })) } }),
691
+ },
692
+ });
693
+ }
587
694
 
588
- const classFiles = await recursivelyCreateFiles(tx, classData.classFiles?.id ?? uuidv4(), createdClass.classFiles?.id);
589
- if (!classFiles) {
590
- return null;
695
+ if (classData.classFiles?.id) {
696
+ await recursivelyCreateFiles(tx, classData.classFiles.id, createdClass.classFiles?.id, pathMap);
591
697
  }
592
698
 
593
699
  return createdClass.id;
594
-
595
- });
700
+ }, { timeout: 30000 });
596
701
 
597
702
  return newClassId;
598
- }
703
+ }