@studious-lms/server 1.1.20 → 1.1.21

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.
@@ -2,20 +2,23 @@ import { z } from "zod";
2
2
  import { createTRPCRouter, protectedProcedure, protectedClassMemberProcedure, protectedTeacherProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
- import { uploadFiles } from "../lib/fileUpload.js";
5
+ import { createDirectUploadFiles, confirmDirectUpload, updateUploadProgress } from "../lib/fileUpload.js";
6
6
  import { deleteFile } from "../lib/googleCloudStorage.js";
7
- const fileSchema = z.object({
7
+ // DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
8
+ // Use directFileSchema instead
9
+ // New schema for direct file uploads (no base64 data)
10
+ const directFileSchema = z.object({
8
11
  name: z.string(),
9
12
  type: z.string(),
10
13
  size: z.number(),
11
- data: z.string(), // base64 encoded file data
14
+ // No data field - for direct file uploads
12
15
  });
13
16
  const createAssignmentSchema = z.object({
14
17
  classId: z.string(),
15
18
  title: z.string(),
16
19
  instructions: z.string(),
17
20
  dueDate: z.string(),
18
- files: z.array(fileSchema).optional(),
21
+ files: z.array(directFileSchema).optional(), // Use direct file schema
19
22
  existingFileIds: z.array(z.string()).optional(),
20
23
  maxGrade: z.number().optional(),
21
24
  graded: z.boolean().optional(),
@@ -32,7 +35,7 @@ const updateAssignmentSchema = z.object({
32
35
  title: z.string().optional(),
33
36
  instructions: z.string().optional(),
34
37
  dueDate: z.string().optional(),
35
- files: z.array(fileSchema).optional(),
38
+ files: z.array(directFileSchema).optional(), // Use direct file schema
36
39
  existingFileIds: z.array(z.string()).optional(),
37
40
  removedAttachments: z.array(z.string()).optional(),
38
41
  maxGrade: z.number().optional(),
@@ -55,7 +58,7 @@ const submissionSchema = z.object({
55
58
  classId: z.string(),
56
59
  submissionId: z.string(),
57
60
  submit: z.boolean().optional(),
58
- newAttachments: z.array(fileSchema).optional(),
61
+ newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
59
62
  existingFileIds: z.array(z.string()).optional(),
60
63
  removedAttachments: z.array(z.string()).optional(),
61
64
  });
@@ -65,7 +68,7 @@ const updateSubmissionSchema = z.object({
65
68
  submissionId: z.string(),
66
69
  return: z.boolean().optional(),
67
70
  gradeReceived: z.number().nullable().optional(),
68
- newAttachments: z.array(fileSchema).optional(),
71
+ newAttachments: z.array(directFileSchema).optional(), // Use direct file schema
69
72
  existingFileIds: z.array(z.string()).optional(),
70
73
  removedAttachments: z.array(z.string()).optional(),
71
74
  feedback: z.string().optional(),
@@ -76,6 +79,31 @@ const updateSubmissionSchema = z.object({
76
79
  comments: z.string(),
77
80
  })).optional(),
78
81
  });
82
+ // New schemas for direct upload functionality
83
+ const getAssignmentUploadUrlsSchema = z.object({
84
+ assignmentId: z.string(),
85
+ classId: z.string(),
86
+ files: z.array(directFileSchema),
87
+ });
88
+ const getSubmissionUploadUrlsSchema = z.object({
89
+ submissionId: z.string(),
90
+ classId: z.string(),
91
+ files: z.array(directFileSchema),
92
+ });
93
+ const confirmAssignmentUploadSchema = z.object({
94
+ fileId: z.string(),
95
+ uploadSuccess: z.boolean(),
96
+ errorMessage: z.string().optional(),
97
+ });
98
+ const confirmSubmissionUploadSchema = z.object({
99
+ fileId: z.string(),
100
+ uploadSuccess: z.boolean(),
101
+ errorMessage: z.string().optional(),
102
+ });
103
+ const updateUploadProgressSchema = z.object({
104
+ fileId: z.string(),
105
+ progress: z.number().min(0).max(100),
106
+ });
79
107
  export const assignmentRouter = createTRPCRouter({
80
108
  order: protectedTeacherProcedure
81
109
  .input(z.object({
@@ -98,7 +126,7 @@ export const assignmentRouter = createTRPCRouter({
98
126
  targetSectionId: z.string(),
99
127
  }))
100
128
  .mutation(async ({ ctx, input }) => {
101
- const { id, targetSectionId, } = input;
129
+ const { id, targetSectionId } = input;
102
130
  const assignments = await prisma.assignment.findMany({
103
131
  where: { sectionId: targetSectionId },
104
132
  });
@@ -251,11 +279,13 @@ export const assignmentRouter = createTRPCRouter({
251
279
  }
252
280
  });
253
281
  await Promise.all(stack.map(({ where, data }) => prisma.assignment.update({ where, data })));
254
- // Upload files if provided
282
+ // NOTE: Files are now handled via direct upload endpoints
283
+ // The files field in the schema is for metadata only
284
+ // Actual file uploads should use getAssignmentUploadUrls endpoint
255
285
  let uploadedFiles = [];
256
286
  if (files && files.length > 0) {
257
- // Store files in a class and assignment specific directory
258
- uploadedFiles = await uploadFiles(files, ctx.user.id);
287
+ // Create direct upload files instead of processing base64
288
+ uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, assignment.id);
259
289
  }
260
290
  // Update assignment with new file attachments
261
291
  if (uploadedFiles.length > 0) {
@@ -336,11 +366,11 @@ export const assignmentRouter = createTRPCRouter({
336
366
  message: "Assignment not found",
337
367
  });
338
368
  }
339
- // Upload new files if provided
369
+ // NOTE: Files are now handled via direct upload endpoints
340
370
  let uploadedFiles = [];
341
371
  if (files && files.length > 0) {
342
- // Store files in a class and assignment specific directory
343
- uploadedFiles = await uploadFiles(files, ctx.user.id);
372
+ // Create direct upload files instead of processing base64
373
+ uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
344
374
  }
345
375
  // Update assignment
346
376
  const updatedAssignment = await prisma.assignment.update({
@@ -875,7 +905,7 @@ export const assignmentRouter = createTRPCRouter({
875
905
  let uploadedFiles = [];
876
906
  if (newAttachments && newAttachments.length > 0) {
877
907
  // Store files in a class and assignment specific directory
878
- uploadedFiles = await uploadFiles(newAttachments, ctx.user.id);
908
+ uploadedFiles = await createDirectUploadFiles(newAttachments, ctx.user.id, undefined, undefined, submission.id);
879
909
  }
880
910
  // Update submission with new file attachments
881
911
  if (uploadedFiles.length > 0) {
@@ -1140,7 +1170,7 @@ export const assignmentRouter = createTRPCRouter({
1140
1170
  let uploadedFiles = [];
1141
1171
  if (newAttachments && newAttachments.length > 0) {
1142
1172
  // Store files in a class and assignment specific directory
1143
- uploadedFiles = await uploadFiles(newAttachments, ctx.user.id);
1173
+ uploadedFiles = await createDirectUploadFiles(newAttachments, ctx.user.id, undefined, undefined, submission.id);
1144
1174
  }
1145
1175
  // Update submission with new file attachments
1146
1176
  if (uploadedFiles.length > 0) {
@@ -1666,4 +1696,201 @@ export const assignmentRouter = createTRPCRouter({
1666
1696
  });
1667
1697
  return updatedAssignment;
1668
1698
  }),
1699
+ // New direct upload endpoints
1700
+ getAssignmentUploadUrls: protectedTeacherProcedure
1701
+ .input(getAssignmentUploadUrlsSchema)
1702
+ .mutation(async ({ ctx, input }) => {
1703
+ const { assignmentId, classId, files } = input;
1704
+ if (!ctx.user) {
1705
+ throw new TRPCError({
1706
+ code: "UNAUTHORIZED",
1707
+ message: "You must be logged in to upload files",
1708
+ });
1709
+ }
1710
+ // Verify user is a teacher of the class
1711
+ const classData = await prisma.class.findFirst({
1712
+ where: {
1713
+ id: classId,
1714
+ teachers: {
1715
+ some: {
1716
+ id: ctx.user.id,
1717
+ },
1718
+ },
1719
+ },
1720
+ });
1721
+ if (!classData) {
1722
+ throw new TRPCError({
1723
+ code: "NOT_FOUND",
1724
+ message: "Class not found or you are not a teacher",
1725
+ });
1726
+ }
1727
+ // Verify assignment exists and belongs to the class
1728
+ const assignment = await prisma.assignment.findFirst({
1729
+ where: {
1730
+ id: assignmentId,
1731
+ classId: classId,
1732
+ },
1733
+ });
1734
+ if (!assignment) {
1735
+ throw new TRPCError({
1736
+ code: "NOT_FOUND",
1737
+ message: "Assignment not found",
1738
+ });
1739
+ }
1740
+ // Create direct upload files
1741
+ const directUploadFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, // No specific directory
1742
+ assignmentId);
1743
+ return {
1744
+ success: true,
1745
+ uploadFiles: directUploadFiles,
1746
+ };
1747
+ }),
1748
+ getSubmissionUploadUrls: protectedClassMemberProcedure
1749
+ .input(getSubmissionUploadUrlsSchema)
1750
+ .mutation(async ({ ctx, input }) => {
1751
+ const { submissionId, classId, files } = input;
1752
+ if (!ctx.user) {
1753
+ throw new TRPCError({
1754
+ code: "UNAUTHORIZED",
1755
+ message: "You must be logged in to upload files",
1756
+ });
1757
+ }
1758
+ // Verify submission exists and user has access
1759
+ const submission = await prisma.submission.findFirst({
1760
+ where: {
1761
+ id: submissionId,
1762
+ assignment: {
1763
+ classId: classId,
1764
+ },
1765
+ },
1766
+ include: {
1767
+ assignment: {
1768
+ include: {
1769
+ class: {
1770
+ include: {
1771
+ students: true,
1772
+ teachers: true,
1773
+ },
1774
+ },
1775
+ },
1776
+ },
1777
+ },
1778
+ });
1779
+ if (!submission) {
1780
+ throw new TRPCError({
1781
+ code: "NOT_FOUND",
1782
+ message: "Submission not found",
1783
+ });
1784
+ }
1785
+ // Check if user is the student who owns the submission or a teacher of the class
1786
+ const isStudent = submission.studentId === ctx.user.id;
1787
+ const isTeacher = submission.assignment.class.teachers.some(teacher => teacher.id === ctx.user?.id);
1788
+ if (!isStudent && !isTeacher) {
1789
+ throw new TRPCError({
1790
+ code: "FORBIDDEN",
1791
+ message: "You don't have permission to upload files to this submission",
1792
+ });
1793
+ }
1794
+ // Create direct upload files
1795
+ const directUploadFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, // No specific directory
1796
+ undefined, // No assignment ID
1797
+ submissionId);
1798
+ return {
1799
+ success: true,
1800
+ uploadFiles: directUploadFiles,
1801
+ };
1802
+ }),
1803
+ confirmAssignmentUpload: protectedTeacherProcedure
1804
+ .input(confirmAssignmentUploadSchema)
1805
+ .mutation(async ({ ctx, input }) => {
1806
+ const { fileId, uploadSuccess, errorMessage } = input;
1807
+ if (!ctx.user) {
1808
+ throw new TRPCError({
1809
+ code: "UNAUTHORIZED",
1810
+ message: "You must be logged in",
1811
+ });
1812
+ }
1813
+ // Verify file belongs to user and is an assignment file
1814
+ const file = await prisma.file.findFirst({
1815
+ where: {
1816
+ id: fileId,
1817
+ userId: ctx.user.id,
1818
+ assignment: {
1819
+ isNot: null,
1820
+ },
1821
+ },
1822
+ });
1823
+ if (!file) {
1824
+ throw new TRPCError({
1825
+ code: "NOT_FOUND",
1826
+ message: "File not found or you don't have permission",
1827
+ });
1828
+ }
1829
+ await confirmDirectUpload(fileId, uploadSuccess, errorMessage);
1830
+ return {
1831
+ success: true,
1832
+ message: uploadSuccess ? "Upload confirmed successfully" : "Upload failed",
1833
+ };
1834
+ }),
1835
+ confirmSubmissionUpload: protectedClassMemberProcedure
1836
+ .input(confirmSubmissionUploadSchema)
1837
+ .mutation(async ({ ctx, input }) => {
1838
+ const { fileId, uploadSuccess, errorMessage } = input;
1839
+ if (!ctx.user) {
1840
+ throw new TRPCError({
1841
+ code: "UNAUTHORIZED",
1842
+ message: "You must be logged in",
1843
+ });
1844
+ }
1845
+ // Verify file belongs to user and is a submission file
1846
+ const file = await prisma.file.findFirst({
1847
+ where: {
1848
+ id: fileId,
1849
+ userId: ctx.user.id,
1850
+ submission: {
1851
+ isNot: null,
1852
+ },
1853
+ },
1854
+ });
1855
+ if (!file) {
1856
+ throw new TRPCError({
1857
+ code: "NOT_FOUND",
1858
+ message: "File not found or you don't have permission",
1859
+ });
1860
+ }
1861
+ await confirmDirectUpload(fileId, uploadSuccess, errorMessage);
1862
+ return {
1863
+ success: true,
1864
+ message: uploadSuccess ? "Upload confirmed successfully" : "Upload failed",
1865
+ };
1866
+ }),
1867
+ updateUploadProgress: protectedProcedure
1868
+ .input(updateUploadProgressSchema)
1869
+ .mutation(async ({ ctx, input }) => {
1870
+ const { fileId, progress } = input;
1871
+ if (!ctx.user) {
1872
+ throw new TRPCError({
1873
+ code: "UNAUTHORIZED",
1874
+ message: "You must be logged in",
1875
+ });
1876
+ }
1877
+ // Verify file belongs to user
1878
+ const file = await prisma.file.findFirst({
1879
+ where: {
1880
+ id: fileId,
1881
+ userId: ctx.user.id,
1882
+ },
1883
+ });
1884
+ if (!file) {
1885
+ throw new TRPCError({
1886
+ code: "NOT_FOUND",
1887
+ message: "File not found or you don't have permission",
1888
+ });
1889
+ }
1890
+ await updateUploadProgress(fileId, progress);
1891
+ return {
1892
+ success: true,
1893
+ progress,
1894
+ };
1895
+ }),
1669
1896
  });
@@ -113,7 +113,7 @@ export const authRouter = createTRPCRouter({
113
113
  if (!user) {
114
114
  throw new TRPCError({
115
115
  code: "UNAUTHORIZED",
116
- message: "Invalid username or password",
116
+ message: "user doesn't exists.",
117
117
  });
118
118
  }
119
119
  if (!(await compare(password, user.password))) {
@@ -49,6 +49,15 @@ export declare const fileRouter: import("@trpc/server").TRPCBuiltRouter<{
49
49
  userId: string | null;
50
50
  thumbnailId: string | null;
51
51
  annotationId: string | null;
52
+ uploadStatus: import(".prisma/client").$Enums.UploadStatus;
53
+ uploadUrl: string | null;
54
+ uploadExpiresAt: Date | null;
55
+ uploadSessionId: string | null;
56
+ uploadProgress: number | null;
57
+ uploadError: string | null;
58
+ uploadRetryCount: number;
59
+ isOrphaned: boolean;
60
+ cleanupAt: Date | null;
52
61
  classDraftId: string | null;
53
62
  folderId: string | null;
54
63
  conversationId: string | null;
@@ -81,6 +90,15 @@ export declare const fileRouter: import("@trpc/server").TRPCBuiltRouter<{
81
90
  userId: string | null;
82
91
  thumbnailId: string | null;
83
92
  annotationId: string | null;
93
+ uploadStatus: import(".prisma/client").$Enums.UploadStatus;
94
+ uploadUrl: string | null;
95
+ uploadExpiresAt: Date | null;
96
+ uploadSessionId: string | null;
97
+ uploadProgress: number | null;
98
+ uploadError: string | null;
99
+ uploadRetryCount: number;
100
+ isOrphaned: boolean;
101
+ cleanupAt: Date | null;
84
102
  classDraftId: string | null;
85
103
  folderId: string | null;
86
104
  conversationId: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/routers/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsVrB,CAAC"}
1
+ {"version":3,"file":"file.d.ts","sourceRoot":"","sources":["../../src/routers/file.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAQxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsVrB,CAAC"}
@@ -208,7 +208,6 @@ export declare const folderRouter: import("@trpc/server").TRPCBuiltRouter<{
208
208
  type: string;
209
209
  name: string;
210
210
  size: number;
211
- data: string;
212
211
  }[];
213
212
  folderId: string;
214
213
  };
@@ -1 +1 @@
1
- {"version":3,"file":"folder.d.ts","sourceRoot":"","sources":["../../src/routers/folder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA6BxB,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0vBvB,CAAC"}
1
+ {"version":3,"file":"folder.d.ts","sourceRoot":"","sources":["../../src/routers/folder.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiCxB,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0vBvB,CAAC"}
@@ -2,21 +2,24 @@ import { z } from "zod";
2
2
  import { createTRPCRouter, protectedProcedure, protectedClassMemberProcedure, protectedTeacherProcedure } from "../trpc.js";
3
3
  import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
- import { uploadFiles } from "../lib/fileUpload.js";
6
- const fileSchema = z.object({
7
- name: z.string(),
8
- type: z.string(),
9
- size: z.number(),
10
- data: z.string(), // base64 encoded file data
11
- });
5
+ import { createDirectUploadFiles } from "../lib/fileUpload.js";
6
+ // DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
7
+ // Use directFileSchema instead
12
8
  const createFolderSchema = z.object({
13
9
  name: z.string(),
14
10
  parentFolderId: z.string().optional(),
15
11
  color: z.string().optional(),
16
12
  });
13
+ // New schema for direct file uploads (no base64 data)
14
+ const directFileSchema = z.object({
15
+ name: z.string(),
16
+ type: z.string(),
17
+ size: z.number(),
18
+ // No data field - for direct file uploads
19
+ });
17
20
  const uploadFilesToFolderSchema = z.object({
18
21
  folderId: z.string(),
19
- files: z.array(fileSchema),
22
+ files: z.array(directFileSchema), // Use direct file schema
20
23
  });
21
24
  const getRootFolderSchema = z.object({
22
25
  classId: z.string(),
@@ -431,7 +434,7 @@ export const folderRouter = createTRPCRouter({
431
434
  });
432
435
  }
433
436
  // Upload files
434
- const uploadedFiles = await uploadFiles(files, ctx.user.id, folder.id);
437
+ const uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, folder.id);
435
438
  // Create file records in database
436
439
  // const fileRecords = await prisma.file.createMany({
437
440
  // data: uploadedFiles.map(file => ({
@@ -1 +1 @@
1
- {"version":3,"file":"labChat.d.ts","sourceRoot":"","sources":["../../src/routers/labChat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAonBxB,CAAC"}
1
+ {"version":3,"file":"labChat.d.ts","sourceRoot":"","sources":["../../src/routers/labChat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAkBxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAonBxB,CAAC"}
@@ -6,7 +6,8 @@ import { TRPCError } from '@trpc/server';
6
6
  import { inferenceClient, sendAIMessage } from '../utils/inference.js';
7
7
  import { logger } from '../utils/logger.js';
8
8
  import { isAIUser } from '../utils/aiUser.js';
9
- import { uploadFile } from '../lib/googleCloudStorage.js';
9
+ // DEPRECATED: uploadFile removed - use direct upload instead
10
+ // import { uploadFile } from '../lib/googleCloudStorage.js';
10
11
  import { createPdf } from "../lib/jsonConversion.js";
11
12
  import { v4 as uuidv4 } from "uuid";
12
13
  export const labChatRouter = createTRPCRouter({
@@ -833,7 +834,8 @@ WHEN CREATING COURSE MATERIALS (docs field):
833
834
  .substring(0, 50);
834
835
  const filename = `${sanitizedTitle}_${uuidv4().substring(0, 8)}.pdf`;
835
836
  logger.info(`PDF ${i + 1} generated successfully`, { labChatId, title: doc.title });
836
- const gcpResult = await uploadFile(Buffer.from(pdfBytes).toString('base64'), `class/generated/${fullLabChat.classId}/${filename}`, 'application/pdf');
837
+ // DEPRECATED: Base64 upload removed - use direct upload instead
838
+ // const gcpResult = await uploadFile(Buffer.from(pdfBytes).toString('base64'), `class/generated/${fullLabChat.classId}/${filename}`, 'application/pdf');
837
839
  logger.info(`PDF ${i + 1} uploaded successfully`, { labChatId, filename });
838
840
  const file = await prisma.file.create({
839
841
  data: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studious-lms/server",
3
- "version": "1.1.20",
3
+ "version": "1.1.21",
4
4
  "description": "Backend server for Studious application",
5
5
  "main": "dist/exportType.js",
6
6
  "types": "dist/exportType.d.ts",
@@ -0,0 +1,57 @@
1
+ -- CreateEnum
2
+ CREATE TYPE "public"."UploadStatus" AS ENUM ('PENDING', 'UPLOADING', 'COMPLETED', 'FAILED', 'CANCELLED');
3
+
4
+ -- AlterTable
5
+ ALTER TABLE "public"."File" ADD COLUMN "cleanupAt" TIMESTAMP(3),
6
+ ADD COLUMN "conversationId" TEXT,
7
+ ADD COLUMN "isOrphaned" BOOLEAN NOT NULL DEFAULT false,
8
+ ADD COLUMN "messageId" TEXT,
9
+ ADD COLUMN "schoolDevelopementProgramId" TEXT,
10
+ ADD COLUMN "uploadError" TEXT,
11
+ ADD COLUMN "uploadExpiresAt" TIMESTAMP(3),
12
+ ADD COLUMN "uploadProgress" INTEGER,
13
+ ADD COLUMN "uploadRetryCount" INTEGER NOT NULL DEFAULT 0,
14
+ ADD COLUMN "uploadSessionId" TEXT,
15
+ ADD COLUMN "uploadStatus" "public"."UploadStatus" NOT NULL DEFAULT 'PENDING',
16
+ ADD COLUMN "uploadUrl" TEXT;
17
+
18
+ -- CreateTable
19
+ CREATE TABLE "public"."SchoolDevelopementProgram" (
20
+ "id" TEXT NOT NULL,
21
+ "name" TEXT NOT NULL,
22
+ "type" TEXT NOT NULL,
23
+ "address" TEXT NOT NULL,
24
+ "city" TEXT NOT NULL,
25
+ "country" TEXT NOT NULL,
26
+ "numberOfStudents" INTEGER NOT NULL,
27
+ "numberOfTeachers" INTEGER NOT NULL,
28
+ "website" TEXT,
29
+ "contactName" TEXT,
30
+ "contactRole" TEXT,
31
+ "contactEmail" TEXT,
32
+ "contactPhone" TEXT DEFAULT '',
33
+ "eligibilityInformation" TEXT,
34
+ "whyHelp" TEXT,
35
+ "additionalInformation" TEXT,
36
+ "submittedAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
37
+ "reviewedAt" TIMESTAMP(3),
38
+ "status" TEXT NOT NULL DEFAULT 'PENDING',
39
+
40
+ CONSTRAINT "SchoolDevelopementProgram_pkey" PRIMARY KEY ("id")
41
+ );
42
+
43
+ -- CreateTable
44
+ CREATE TABLE "public"."EarlyAccessRequest" (
45
+ "id" TEXT NOT NULL,
46
+ "email" TEXT NOT NULL,
47
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
48
+ "institutionSize" TEXT NOT NULL,
49
+
50
+ CONSTRAINT "EarlyAccessRequest_pkey" PRIMARY KEY ("id")
51
+ );
52
+
53
+ -- AddForeignKey
54
+ ALTER TABLE "public"."File" ADD CONSTRAINT "File_messageId_fkey" FOREIGN KEY ("messageId") REFERENCES "public"."Message"("id") ON DELETE CASCADE ON UPDATE CASCADE;
55
+
56
+ -- AddForeignKey
57
+ ALTER TABLE "public"."File" ADD CONSTRAINT "File_schoolDevelopementProgramId_fkey" FOREIGN KEY ("schoolDevelopementProgramId") REFERENCES "public"."SchoolDevelopementProgram"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -34,6 +34,14 @@ enum UserRole {
34
34
  NONE
35
35
  }
36
36
 
37
+ enum UploadStatus {
38
+ PENDING
39
+ UPLOADING
40
+ COMPLETED
41
+ FAILED
42
+ CANCELLED
43
+ }
44
+
37
45
  model School {
38
46
  id String @id @default(uuid())
39
47
  name String
@@ -163,6 +171,17 @@ model File {
163
171
  userId String?
164
172
  uploadedAt DateTime? @default(now())
165
173
 
174
+ // Upload tracking fields
175
+ uploadStatus UploadStatus @default(PENDING)
176
+ uploadUrl String?
177
+ uploadExpiresAt DateTime?
178
+ uploadSessionId String?
179
+ uploadProgress Int?
180
+ uploadError String?
181
+ uploadRetryCount Int @default(0)
182
+ isOrphaned Boolean @default(false)
183
+ cleanupAt DateTime?
184
+
166
185
  // Thumbnail relationship
167
186
  thumbnail File? @relation("Thumbnail", fields: [thumbnailId], references: [id])
168
187
  thumbnailId String? @unique