@studious-lms/server 1.1.24 → 1.2.26

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 (49) hide show
  1. package/dist/lib/fileUpload.d.ts +2 -2
  2. package/dist/lib/fileUpload.d.ts.map +1 -1
  3. package/dist/lib/fileUpload.js +76 -14
  4. package/dist/lib/googleCloudStorage.d.ts +7 -0
  5. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  6. package/dist/lib/googleCloudStorage.js +19 -0
  7. package/dist/lib/notificationHandler.d.ts +25 -0
  8. package/dist/lib/notificationHandler.d.ts.map +1 -0
  9. package/dist/lib/notificationHandler.js +28 -0
  10. package/dist/routers/_app.d.ts +818 -78
  11. package/dist/routers/_app.d.ts.map +1 -1
  12. package/dist/routers/announcement.d.ts +290 -3
  13. package/dist/routers/announcement.d.ts.map +1 -1
  14. package/dist/routers/announcement.js +896 -10
  15. package/dist/routers/assignment.d.ts +70 -4
  16. package/dist/routers/assignment.d.ts.map +1 -1
  17. package/dist/routers/assignment.js +265 -131
  18. package/dist/routers/auth.js +1 -1
  19. package/dist/routers/file.d.ts +2 -0
  20. package/dist/routers/file.d.ts.map +1 -1
  21. package/dist/routers/file.js +9 -6
  22. package/dist/routers/labChat.d.ts.map +1 -1
  23. package/dist/routers/labChat.js +13 -5
  24. package/dist/routers/notifications.d.ts +8 -8
  25. package/dist/routers/section.d.ts +16 -0
  26. package/dist/routers/section.d.ts.map +1 -1
  27. package/dist/routers/section.js +139 -30
  28. package/dist/seedDatabase.d.ts +2 -2
  29. package/dist/seedDatabase.d.ts.map +1 -1
  30. package/dist/seedDatabase.js +2 -1
  31. package/dist/utils/logger.d.ts +1 -0
  32. package/dist/utils/logger.d.ts.map +1 -1
  33. package/dist/utils/logger.js +27 -2
  34. package/package.json +2 -2
  35. package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +30 -0
  36. package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +35 -0
  37. package/prisma/schema.prisma +50 -0
  38. package/src/lib/fileUpload.ts +79 -14
  39. package/src/lib/googleCloudStorage.ts +19 -0
  40. package/src/lib/notificationHandler.ts +36 -0
  41. package/src/routers/announcement.ts +1007 -10
  42. package/src/routers/assignment.ts +230 -82
  43. package/src/routers/auth.ts +1 -1
  44. package/src/routers/file.ts +10 -7
  45. package/src/routers/labChat.ts +15 -6
  46. package/src/routers/section.ts +158 -36
  47. package/src/seedDatabase.ts +2 -1
  48. package/src/utils/logger.ts +29 -2
  49. package/tests/setup.ts +3 -9
@@ -4,6 +4,8 @@ import { TRPCError } from "@trpc/server";
4
4
  import { prisma } from "../lib/prisma.js";
5
5
  import { createDirectUploadFiles, confirmDirectUpload, updateUploadProgress } from "../lib/fileUpload.js";
6
6
  import { deleteFile } from "../lib/googleCloudStorage.js";
7
+ import { sendNotifications } from "../lib/notificationHandler.js";
8
+ import { logger } from "../utils/logger.js";
7
9
  // DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
8
10
  // Use directFileSchema instead
9
11
  // New schema for direct file uploads (no base64 data)
@@ -114,7 +116,100 @@ const updateUploadProgressSchema = z.object({
114
116
  fileId: z.string(),
115
117
  progress: z.number().min(0).max(100),
116
118
  });
119
+ // Helper function to get unified list of sections and assignments for a class
120
+ async function getUnifiedList(tx, classId) {
121
+ const [sections, assignments] = await Promise.all([
122
+ tx.section.findMany({
123
+ where: { classId },
124
+ select: { id: true, order: true },
125
+ }),
126
+ tx.assignment.findMany({
127
+ where: { classId },
128
+ select: { id: true, order: true },
129
+ }),
130
+ ]);
131
+ // Combine and sort by order
132
+ const unified = [
133
+ ...sections.map((s) => ({ id: s.id, order: s.order, type: 'section' })),
134
+ ...assignments.map((a) => ({ id: a.id, order: a.order, type: 'assignment' })),
135
+ ].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
136
+ return unified;
137
+ }
138
+ // Helper function to normalize unified list to 1..n
139
+ async function normalizeUnifiedList(tx, classId, orderedItems) {
140
+ await Promise.all(orderedItems.map((item, index) => {
141
+ if (item.type === 'section') {
142
+ return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
143
+ }
144
+ else {
145
+ return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
146
+ }
147
+ }));
148
+ }
117
149
  export const assignmentRouter = createTRPCRouter({
150
+ // Reorder an assignment within the unified list (sections + assignments)
151
+ reorder: protectedTeacherProcedure
152
+ .input(z.object({
153
+ classId: z.string(),
154
+ movedId: z.string(),
155
+ // One of: place at start/end of unified list, or relative to targetId (can be section or assignment)
156
+ position: z.enum(['start', 'end', 'before', 'after']),
157
+ targetId: z.string().optional(), // Can be a section ID or assignment ID
158
+ }))
159
+ .mutation(async ({ ctx, input }) => {
160
+ const { classId, movedId, position, targetId } = input;
161
+ const moved = await prisma.assignment.findFirst({
162
+ where: { id: movedId, classId },
163
+ select: { id: true, classId: true },
164
+ });
165
+ if (!moved) {
166
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
167
+ }
168
+ if ((position === 'before' || position === 'after') && !targetId) {
169
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId required for before/after' });
170
+ }
171
+ const result = await prisma.$transaction(async (tx) => {
172
+ const unified = await getUnifiedList(tx, classId);
173
+ // Find moved item and target in unified list
174
+ const movedIdx = unified.findIndex(item => item.id === movedId && item.type === 'assignment');
175
+ if (movedIdx === -1) {
176
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found in unified list' });
177
+ }
178
+ // Build list without moved item
179
+ const withoutMoved = unified.filter(item => !(item.id === movedId && item.type === 'assignment'));
180
+ let next = [];
181
+ if (position === 'start') {
182
+ next = [{ id: movedId, type: 'assignment' }, ...withoutMoved.map(item => ({ id: item.id, type: item.type }))];
183
+ }
184
+ else if (position === 'end') {
185
+ next = [...withoutMoved.map(item => ({ id: item.id, type: item.type })), { id: movedId, type: 'assignment' }];
186
+ }
187
+ else {
188
+ const targetIdx = withoutMoved.findIndex(item => item.id === targetId);
189
+ if (targetIdx === -1) {
190
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId not found in unified list' });
191
+ }
192
+ if (position === 'before') {
193
+ next = [
194
+ ...withoutMoved.slice(0, targetIdx).map(item => ({ id: item.id, type: item.type })),
195
+ { id: movedId, type: 'assignment' },
196
+ ...withoutMoved.slice(targetIdx).map(item => ({ id: item.id, type: item.type })),
197
+ ];
198
+ }
199
+ else {
200
+ next = [
201
+ ...withoutMoved.slice(0, targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
202
+ { id: movedId, type: 'assignment' },
203
+ ...withoutMoved.slice(targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
204
+ ];
205
+ }
206
+ }
207
+ // Normalize to 1..n
208
+ await normalizeUnifiedList(tx, classId, next);
209
+ return tx.assignment.findUnique({ where: { id: movedId } });
210
+ });
211
+ return result;
212
+ }),
118
213
  order: protectedTeacherProcedure
119
214
  .input(z.object({
120
215
  id: z.string(),
@@ -122,37 +217,49 @@ export const assignmentRouter = createTRPCRouter({
122
217
  order: z.number(),
123
218
  }))
124
219
  .mutation(async ({ ctx, input }) => {
220
+ // Deprecated: prefer `reorder`. For backward-compatibility, set the order then normalize unified list.
125
221
  const { id, order } = input;
126
- const assignment = await prisma.assignment.update({
222
+ const current = await prisma.assignment.findUnique({
127
223
  where: { id },
128
- data: { order },
224
+ select: { id: true, classId: true },
129
225
  });
130
- return assignment;
226
+ if (!current) {
227
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
228
+ }
229
+ const updated = await prisma.$transaction(async (tx) => {
230
+ await tx.assignment.update({ where: { id }, data: { order } });
231
+ // Normalize entire unified list
232
+ const unified = await getUnifiedList(tx, current.classId);
233
+ await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
234
+ return tx.assignment.findUnique({ where: { id } });
235
+ });
236
+ return updated;
131
237
  }),
132
238
  move: protectedTeacherProcedure
133
239
  .input(z.object({
134
240
  id: z.string(),
135
241
  classId: z.string(),
136
- targetSectionId: z.string(),
242
+ targetSectionId: z.string().nullable().optional(),
137
243
  }))
138
244
  .mutation(async ({ ctx, input }) => {
139
- const { id, targetSectionId } = input;
140
- const assignments = await prisma.assignment.findMany({
141
- where: { sectionId: targetSectionId },
142
- });
143
- const stack = assignments.sort((a, b) => (a.order || 0) - (b.order || 0)).map((assignment, index) => ({
144
- id: assignment.id,
145
- order: index + 1,
146
- })).map((assignment) => ({
147
- where: { id: assignment.id },
148
- data: { order: assignment.order },
149
- }));
150
- await Promise.all(stack.map(({ where, data }) => prisma.assignment.update({ where, data })));
151
- const assignment = await prisma.assignment.update({
245
+ const { id } = input;
246
+ const targetSectionId = (input.targetSectionId ?? null) || null; // normalize empty string to null
247
+ const moved = await prisma.assignment.findUnique({
152
248
  where: { id },
153
- data: { sectionId: targetSectionId, order: 0 },
249
+ select: { id: true, classId: true, sectionId: true },
154
250
  });
155
- return assignment;
251
+ if (!moved) {
252
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
253
+ }
254
+ const updated = await prisma.$transaction(async (tx) => {
255
+ // Update sectionId first
256
+ await tx.assignment.update({ where: { id }, data: { sectionId: targetSectionId } });
257
+ // The unified list ordering remains the same, just the assignment's sectionId changed
258
+ // No need to reorder since we're keeping the same position in the unified list
259
+ // If frontend wants to change position, they should call reorder after move
260
+ return tx.assignment.findUnique({ where: { id } });
261
+ });
262
+ return updated;
156
263
  }),
157
264
  create: protectedProcedure
158
265
  .input(createAssignmentSchema)
@@ -195,100 +302,92 @@ export const assignmentRouter = createTRPCRouter({
195
302
  }, 0);
196
303
  }
197
304
  console.log(markSchemeId, gradingBoundaryId);
198
- // find all assignments in the section it is in (or none) and reorder them
199
- const assignments = await prisma.assignment.findMany({
200
- where: {
201
- classId: classId,
202
- ...(sectionId && {
203
- sectionId: sectionId,
204
- }),
205
- },
206
- });
207
- const stack = assignments.sort((a, b) => (a.order || 0) - (b.order || 0)).map((assignment, index) => ({
208
- id: assignment.id,
209
- order: index + 1,
210
- })).map((assignment) => ({
211
- where: { id: assignment.id },
212
- data: { order: assignment.order },
213
- }));
214
- // Create assignment with submissions for all students
215
- const assignment = await prisma.assignment.create({
216
- data: {
217
- title,
218
- instructions,
219
- dueDate: new Date(dueDate),
220
- maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
221
- graded,
222
- weight,
223
- type,
224
- order: 0,
225
- inProgress: inProgress || false,
226
- class: {
227
- connect: { id: classId }
228
- },
229
- ...(sectionId && {
230
- section: {
231
- connect: { id: sectionId }
232
- }
233
- }),
234
- ...(markSchemeId && {
235
- markScheme: {
236
- connect: { id: markSchemeId }
237
- }
238
- }),
239
- ...(gradingBoundaryId && {
240
- gradingBoundary: {
241
- connect: { id: gradingBoundaryId }
242
- }
243
- }),
244
- submissions: {
245
- create: classData.students.map((student) => ({
246
- student: {
247
- connect: { id: student.id }
305
+ // Create assignment and place at top of its scope within a single transaction
306
+ const teacherId = ctx.user.id;
307
+ const assignment = await prisma.$transaction(async (tx) => {
308
+ const created = await tx.assignment.create({
309
+ data: {
310
+ title,
311
+ instructions,
312
+ dueDate: new Date(dueDate),
313
+ maxGrade: markSchemeId ? computedMaxGrade : maxGrade,
314
+ graded,
315
+ weight,
316
+ type,
317
+ order: 1,
318
+ inProgress: inProgress || false,
319
+ class: {
320
+ connect: { id: classId }
321
+ },
322
+ ...(sectionId && {
323
+ section: {
324
+ connect: { id: sectionId }
248
325
  }
249
- }))
250
- },
251
- teacher: {
252
- connect: { id: ctx.user.id }
253
- }
254
- },
255
- select: {
256
- id: true,
257
- title: true,
258
- instructions: true,
259
- dueDate: true,
260
- maxGrade: true,
261
- graded: true,
262
- weight: true,
263
- type: true,
264
- attachments: {
265
- select: {
266
- id: true,
267
- name: true,
268
- type: true,
269
- }
270
- },
271
- section: {
272
- select: {
273
- id: true,
274
- name: true
275
- }
276
- },
277
- teacher: {
278
- select: {
279
- id: true,
280
- username: true
326
+ }),
327
+ ...(markSchemeId && {
328
+ markScheme: {
329
+ connect: { id: markSchemeId }
330
+ }
331
+ }),
332
+ ...(gradingBoundaryId && {
333
+ gradingBoundary: {
334
+ connect: { id: gradingBoundaryId }
335
+ }
336
+ }),
337
+ submissions: {
338
+ create: classData.students.map((student) => ({
339
+ student: {
340
+ connect: { id: student.id }
341
+ }
342
+ }))
343
+ },
344
+ teacher: {
345
+ connect: { id: teacherId }
281
346
  }
282
347
  },
283
- class: {
284
- select: {
285
- id: true,
286
- name: true
348
+ select: {
349
+ id: true,
350
+ title: true,
351
+ instructions: true,
352
+ dueDate: true,
353
+ maxGrade: true,
354
+ graded: true,
355
+ weight: true,
356
+ type: true,
357
+ attachments: {
358
+ select: {
359
+ id: true,
360
+ name: true,
361
+ type: true,
362
+ }
363
+ },
364
+ section: {
365
+ select: {
366
+ id: true,
367
+ name: true
368
+ }
369
+ },
370
+ teacher: {
371
+ select: {
372
+ id: true,
373
+ username: true
374
+ }
375
+ },
376
+ class: {
377
+ select: {
378
+ id: true,
379
+ name: true
380
+ }
287
381
  }
288
382
  }
289
- }
383
+ });
384
+ // Insert new assignment at top of unified list and normalize
385
+ const unified = await getUnifiedList(tx, classId);
386
+ const withoutNew = unified.filter(item => !(item.id === created.id && item.type === 'assignment'));
387
+ const reindexed = [{ id: created.id, type: 'assignment' }, ...withoutNew.map(item => ({ id: item.id, type: item.type }))];
388
+ await normalizeUnifiedList(tx, classId, reindexed);
389
+ return created;
290
390
  });
291
- await Promise.all(stack.map(({ where, data }) => prisma.assignment.update({ where, data })));
292
391
  // NOTE: Files are now handled via direct upload endpoints
293
392
  // The files field in the schema is for metadata only
294
393
  // Actual file uploads should use getAssignmentUploadUrls endpoint
@@ -329,6 +428,14 @@ export const assignmentRouter = createTRPCRouter({
329
428
  }
330
429
  });
331
430
  }
431
+ sendNotifications(classData.students.map(student => student.id), {
432
+ title: `🔔 New assignment for ${classData.name}`,
433
+ content: `The assignment "${title}" has been created in ${classData.name}.\n
434
+ Due date: ${new Date(dueDate).toLocaleDateString()}.
435
+ [Link to assignment](/class/${classId}/assignments/${assignment.id})`
436
+ }).catch(error => {
437
+ logger.error('Failed to send assignment notifications:');
438
+ });
332
439
  return assignment;
333
440
  }),
334
441
  update: protectedProcedure
@@ -355,6 +462,7 @@ export const assignmentRouter = createTRPCRouter({
355
462
  type: true,
356
463
  path: true,
357
464
  size: true,
465
+ uploadStatus: true,
358
466
  thumbnail: {
359
467
  select: {
360
468
  path: true
@@ -382,6 +490,27 @@ export const assignmentRouter = createTRPCRouter({
382
490
  // Create direct upload files instead of processing base64
383
491
  uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
384
492
  }
493
+ // Delete removed attachments from storage before updating database
494
+ if (input.removedAttachments && input.removedAttachments.length > 0) {
495
+ const filesToDelete = assignment.attachments.filter((file) => input.removedAttachments.includes(file.id));
496
+ // Delete files from storage (only if they were actually uploaded)
497
+ await Promise.all(filesToDelete.map(async (file) => {
498
+ try {
499
+ // Only delete from GCS if the file was successfully uploaded
500
+ if (file.uploadStatus === 'COMPLETED') {
501
+ // Delete the main file
502
+ await deleteFile(file.path);
503
+ // Delete thumbnail if it exists
504
+ if (file.thumbnail?.path) {
505
+ await deleteFile(file.thumbnail.path);
506
+ }
507
+ }
508
+ }
509
+ catch (error) {
510
+ console.warn(`Failed to delete file ${file.path}:`, error);
511
+ }
512
+ }));
513
+ }
385
514
  // Update assignment
386
515
  const updatedAssignment = await prisma.assignment.update({
387
516
  where: { id },
@@ -536,14 +665,17 @@ export const assignmentRouter = createTRPCRouter({
536
665
  ...assignment.attachments,
537
666
  ...assignment.submissions.flatMap(sub => [...sub.attachments, ...sub.annotations])
538
667
  ];
539
- // Delete files from storage
668
+ // Delete files from storage (only if they were actually uploaded)
540
669
  await Promise.all(filesToDelete.map(async (file) => {
541
670
  try {
542
- // Delete the main file
543
- await deleteFile(file.path);
544
- // Delete thumbnail if it exists
545
- if (file.thumbnail) {
546
- await deleteFile(file.thumbnail.path);
671
+ // Only delete from GCS if the file was successfully uploaded
672
+ if (file.uploadStatus === 'COMPLETED') {
673
+ // Delete the main file
674
+ await deleteFile(file.path);
675
+ // Delete thumbnail if it exists
676
+ if (file.thumbnail) {
677
+ await deleteFile(file.thumbnail.path);
678
+ }
547
679
  }
548
680
  }
549
681
  catch (error) {
@@ -678,6 +810,7 @@ export const assignmentRouter = createTRPCRouter({
678
810
  select: {
679
811
  id: true,
680
812
  username: true,
813
+ profile: true,
681
814
  },
682
815
  },
683
816
  assignment: {
@@ -779,12 +912,7 @@ export const assignmentRouter = createTRPCRouter({
779
912
  select: {
780
913
  id: true,
781
914
  username: true,
782
- profile: {
783
- select: {
784
- displayName: true,
785
- profilePicture: true,
786
- }
787
- }
915
+ profile: true,
788
916
  },
789
917
  },
790
918
  assignment: {
@@ -956,14 +1084,17 @@ export const assignmentRouter = createTRPCRouter({
956
1084
  // Delete removed attachments if any
957
1085
  if (removedAttachments && removedAttachments.length > 0) {
958
1086
  const filesToDelete = submission.attachments.filter((file) => removedAttachments.includes(file.id));
959
- // Delete files from storage
1087
+ // Delete files from storage (only if they were actually uploaded)
960
1088
  await Promise.all(filesToDelete.map(async (file) => {
961
1089
  try {
962
- // Delete the main file
963
- await deleteFile(file.path);
964
- // Delete thumbnail if it exists
965
- if (file.thumbnail?.path) {
966
- await deleteFile(file.thumbnail.path);
1090
+ // Only delete from GCS if the file was successfully uploaded
1091
+ if (file.uploadStatus === 'COMPLETED') {
1092
+ // Delete the main file
1093
+ await deleteFile(file.path);
1094
+ // Delete thumbnail if it exists
1095
+ if (file.thumbnail?.path) {
1096
+ await deleteFile(file.thumbnail.path);
1097
+ }
967
1098
  }
968
1099
  }
969
1100
  catch (error) {
@@ -1204,14 +1335,17 @@ export const assignmentRouter = createTRPCRouter({
1204
1335
  // Delete removed attachments if any
1205
1336
  if (removedAttachments && removedAttachments.length > 0) {
1206
1337
  const filesToDelete = submission.annotations.filter((file) => removedAttachments.includes(file.id));
1207
- // Delete files from storage
1338
+ // Delete files from storage (only if they were actually uploaded)
1208
1339
  await Promise.all(filesToDelete.map(async (file) => {
1209
1340
  try {
1210
- // Delete the main file
1211
- await deleteFile(file.path);
1212
- // Delete thumbnail if it exists
1213
- if (file.thumbnail?.path) {
1214
- await deleteFile(file.thumbnail.path);
1341
+ // Only delete from GCS if the file was successfully uploaded
1342
+ if (file.uploadStatus === 'COMPLETED') {
1343
+ // Delete the main file
1344
+ await deleteFile(file.path);
1345
+ // Delete thumbnail if it exists
1346
+ if (file.thumbnail?.path) {
1347
+ await deleteFile(file.thumbnail.path);
1348
+ }
1215
1349
  }
1216
1350
  }
1217
1351
  catch (error) {
@@ -146,7 +146,7 @@ export const authRouter = createTRPCRouter({
146
146
  },
147
147
  };
148
148
  }),
149
- logout: publicProcedure
149
+ logout: protectedProcedure
150
150
  .mutation(async ({ ctx }) => {
151
151
  if (!ctx.user) {
152
152
  throw new TRPCError({
@@ -62,6 +62,7 @@ export declare const fileRouter: import("@trpc/server").TRPCBuiltRouter<{
62
62
  folderId: string | null;
63
63
  conversationId: string | null;
64
64
  messageId: string | null;
65
+ announcementId: string | null;
65
66
  schoolDevelopementProgramId: string | null;
66
67
  };
67
68
  meta: object;
@@ -103,6 +104,7 @@ export declare const fileRouter: import("@trpc/server").TRPCBuiltRouter<{
103
104
  folderId: string | null;
104
105
  conversationId: string | null;
105
106
  messageId: string | null;
107
+ announcementId: string | null;
106
108
  schoolDevelopementProgramId: string | null;
107
109
  };
108
110
  meta: object;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyVrB,CAAC"}
@@ -295,13 +295,16 @@ export const fileRouter = createTRPCRouter({
295
295
  message: "File does not belong to this class",
296
296
  });
297
297
  }
298
- // Delete files from storage
298
+ // Delete files from storage (only if they were actually uploaded)
299
299
  try {
300
- // Delete the main file
301
- await deleteFile(file.path);
302
- // Delete thumbnail if it exists
303
- if (file.thumbnail) {
304
- await deleteFile(file.thumbnail.path);
300
+ // Only delete from GCS if the file was successfully uploaded
301
+ if (file.uploadStatus === 'COMPLETED') {
302
+ // Delete the main file
303
+ await deleteFile(file.path);
304
+ // Delete thumbnail if it exists
305
+ if (file.thumbnail) {
306
+ await deleteFile(file.thumbnail.path);
307
+ }
305
308
  }
306
309
  }
307
310
  catch (error) {
@@ -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;AAkBxB,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;AAiBxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAonBxB,CAAC"}
@@ -6,8 +6,7 @@ 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
- // DEPRECATED: uploadFile removed - use direct upload instead
10
- // import { uploadFile } from '../lib/googleCloudStorage.js';
9
+ import { bucket } from '../lib/googleCloudStorage.js';
11
10
  import { createPdf } from "../lib/jsonConversion.js";
12
11
  import { v4 as uuidv4 } from "uuid";
13
12
  export const labChatRouter = createTRPCRouter({
@@ -833,16 +832,25 @@ WHEN CREATING COURSE MATERIALS (docs field):
833
832
  .replace(/\s+/g, '_')
834
833
  .substring(0, 50);
835
834
  const filename = `${sanitizedTitle}_${uuidv4().substring(0, 8)}.pdf`;
835
+ const filePath = `class/generated/${fullLabChat.classId}/${filename}`;
836
836
  logger.info(`PDF ${i + 1} generated successfully`, { labChatId, title: doc.title });
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
+ // Upload directly to Google Cloud Storage
838
+ const gcsFile = bucket.file(filePath);
839
+ await gcsFile.save(Buffer.from(pdfBytes), {
840
+ metadata: {
841
+ contentType: 'application/pdf',
842
+ }
843
+ });
839
844
  logger.info(`PDF ${i + 1} uploaded successfully`, { labChatId, filename });
840
845
  const file = await prisma.file.create({
841
846
  data: {
842
847
  name: filename,
843
- path: `class/generated/${fullLabChat.classId}/${filename}`,
848
+ path: filePath,
844
849
  type: 'application/pdf',
850
+ size: pdfBytes.length,
845
851
  userId: fullLabChat.createdById,
852
+ uploadStatus: 'COMPLETED',
853
+ uploadedAt: new Date(),
846
854
  },
847
855
  });
848
856
  attachmentIds.push(file.id);
@@ -30,9 +30,9 @@ export declare const notificationRouter: import("@trpc/server").TRPCBuiltRouter<
30
30
  title: string;
31
31
  content: string;
32
32
  createdAt: Date;
33
- read: boolean;
34
- receiverId: string;
35
33
  senderId: string | null;
34
+ receiverId: string;
35
+ read: boolean;
36
36
  })[];
37
37
  meta: object;
38
38
  }>;
@@ -52,9 +52,9 @@ export declare const notificationRouter: import("@trpc/server").TRPCBuiltRouter<
52
52
  title: string;
53
53
  content: string;
54
54
  createdAt: Date;
55
- read: boolean;
56
- receiverId: string;
57
55
  senderId: string | null;
56
+ receiverId: string;
57
+ read: boolean;
58
58
  }) | null;
59
59
  meta: object;
60
60
  }>;
@@ -69,9 +69,9 @@ export declare const notificationRouter: import("@trpc/server").TRPCBuiltRouter<
69
69
  title: string;
70
70
  content: string;
71
71
  createdAt: Date;
72
- read: boolean;
73
- receiverId: string;
74
72
  senderId: string | null;
73
+ receiverId: string;
74
+ read: boolean;
75
75
  };
76
76
  meta: object;
77
77
  }>;
@@ -93,9 +93,9 @@ export declare const notificationRouter: import("@trpc/server").TRPCBuiltRouter<
93
93
  title: string;
94
94
  content: string;
95
95
  createdAt: Date;
96
- read: boolean;
97
- receiverId: string;
98
96
  senderId: string | null;
97
+ receiverId: string;
98
+ read: boolean;
99
99
  };
100
100
  meta: object;
101
101
  }>;