@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.
- package/dist/lib/fileUpload.d.ts +2 -2
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +76 -14
- package/dist/lib/googleCloudStorage.d.ts +7 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +19 -0
- package/dist/lib/notificationHandler.d.ts +25 -0
- package/dist/lib/notificationHandler.d.ts.map +1 -0
- package/dist/lib/notificationHandler.js +28 -0
- package/dist/routers/_app.d.ts +818 -78
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +290 -3
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +896 -10
- package/dist/routers/assignment.d.ts +70 -4
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +265 -131
- package/dist/routers/auth.js +1 -1
- package/dist/routers/file.d.ts +2 -0
- package/dist/routers/file.d.ts.map +1 -1
- package/dist/routers/file.js +9 -6
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/labChat.js +13 -5
- package/dist/routers/notifications.d.ts +8 -8
- package/dist/routers/section.d.ts +16 -0
- package/dist/routers/section.d.ts.map +1 -1
- package/dist/routers/section.js +139 -30
- package/dist/seedDatabase.d.ts +2 -2
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +2 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +27 -2
- package/package.json +2 -2
- package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +30 -0
- package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +35 -0
- package/prisma/schema.prisma +50 -0
- package/src/lib/fileUpload.ts +79 -14
- package/src/lib/googleCloudStorage.ts +19 -0
- package/src/lib/notificationHandler.ts +36 -0
- package/src/routers/announcement.ts +1007 -10
- package/src/routers/assignment.ts +230 -82
- package/src/routers/auth.ts +1 -1
- package/src/routers/file.ts +10 -7
- package/src/routers/labChat.ts +15 -6
- package/src/routers/section.ts +158 -36
- package/src/seedDatabase.ts +2 -1
- package/src/utils/logger.ts +29 -2
- 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, type DirectUploadFile, confirmDirectUpload, updateUploadProgress, type UploadedFile } 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
|
|
|
8
10
|
// DEPRECATED: This schema is no longer used - files are uploaded directly to GCS
|
|
9
11
|
// Use directFileSchema instead
|
|
@@ -130,7 +132,113 @@ const updateUploadProgressSchema = z.object({
|
|
|
130
132
|
progress: z.number().min(0).max(100),
|
|
131
133
|
});
|
|
132
134
|
|
|
135
|
+
// Helper function to get unified list of sections and assignments for a class
|
|
136
|
+
async function getUnifiedList(tx: any, classId: string) {
|
|
137
|
+
const [sections, assignments] = await Promise.all([
|
|
138
|
+
tx.section.findMany({
|
|
139
|
+
where: { classId },
|
|
140
|
+
select: { id: true, order: true },
|
|
141
|
+
}),
|
|
142
|
+
tx.assignment.findMany({
|
|
143
|
+
where: { classId },
|
|
144
|
+
select: { id: true, order: true },
|
|
145
|
+
}),
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
// Combine and sort by order
|
|
149
|
+
const unified = [
|
|
150
|
+
...sections.map((s: any) => ({ id: s.id, order: s.order, type: 'section' as const })),
|
|
151
|
+
...assignments.map((a: any) => ({ id: a.id, order: a.order, type: 'assignment' as const })),
|
|
152
|
+
].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
|
|
153
|
+
|
|
154
|
+
return unified;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Helper function to normalize unified list to 1..n
|
|
158
|
+
async function normalizeUnifiedList(tx: any, classId: string, orderedItems: Array<{ id: string; type: 'section' | 'assignment' }>) {
|
|
159
|
+
await Promise.all(
|
|
160
|
+
orderedItems.map((item, index) => {
|
|
161
|
+
if (item.type === 'section') {
|
|
162
|
+
return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
163
|
+
} else {
|
|
164
|
+
return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
165
|
+
}
|
|
166
|
+
})
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
133
170
|
export const assignmentRouter = createTRPCRouter({
|
|
171
|
+
// Reorder an assignment within the unified list (sections + assignments)
|
|
172
|
+
reorder: protectedTeacherProcedure
|
|
173
|
+
.input(z.object({
|
|
174
|
+
classId: z.string(),
|
|
175
|
+
movedId: z.string(),
|
|
176
|
+
// One of: place at start/end of unified list, or relative to targetId (can be section or assignment)
|
|
177
|
+
position: z.enum(['start', 'end', 'before', 'after']),
|
|
178
|
+
targetId: z.string().optional(), // Can be a section ID or assignment ID
|
|
179
|
+
}))
|
|
180
|
+
.mutation(async ({ ctx, input }) => {
|
|
181
|
+
const { classId, movedId, position, targetId } = input;
|
|
182
|
+
|
|
183
|
+
const moved = await prisma.assignment.findFirst({
|
|
184
|
+
where: { id: movedId, classId },
|
|
185
|
+
select: { id: true, classId: true },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (!moved) {
|
|
189
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if ((position === 'before' || position === 'after') && !targetId) {
|
|
193
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId required for before/after' });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
197
|
+
const unified = await getUnifiedList(tx, classId);
|
|
198
|
+
|
|
199
|
+
// Find moved item and target in unified list
|
|
200
|
+
const movedIdx = unified.findIndex(item => item.id === movedId && item.type === 'assignment');
|
|
201
|
+
if (movedIdx === -1) {
|
|
202
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found in unified list' });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Build list without moved item
|
|
206
|
+
const withoutMoved = unified.filter(item => !(item.id === movedId && item.type === 'assignment'));
|
|
207
|
+
|
|
208
|
+
let next: Array<{ id: string; type: 'section' | 'assignment' }> = [];
|
|
209
|
+
|
|
210
|
+
if (position === 'start') {
|
|
211
|
+
next = [{ id: movedId, type: 'assignment' }, ...withoutMoved.map(item => ({ id: item.id, type: item.type }))];
|
|
212
|
+
} else if (position === 'end') {
|
|
213
|
+
next = [...withoutMoved.map(item => ({ id: item.id, type: item.type })), { id: movedId, type: 'assignment' }];
|
|
214
|
+
} else {
|
|
215
|
+
const targetIdx = withoutMoved.findIndex(item => item.id === targetId);
|
|
216
|
+
if (targetIdx === -1) {
|
|
217
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId not found in unified list' });
|
|
218
|
+
}
|
|
219
|
+
if (position === 'before') {
|
|
220
|
+
next = [
|
|
221
|
+
...withoutMoved.slice(0, targetIdx).map(item => ({ id: item.id, type: item.type })),
|
|
222
|
+
{ id: movedId, type: 'assignment' },
|
|
223
|
+
...withoutMoved.slice(targetIdx).map(item => ({ id: item.id, type: item.type })),
|
|
224
|
+
];
|
|
225
|
+
} else {
|
|
226
|
+
next = [
|
|
227
|
+
...withoutMoved.slice(0, targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
|
|
228
|
+
{ id: movedId, type: 'assignment' },
|
|
229
|
+
...withoutMoved.slice(targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Normalize to 1..n
|
|
235
|
+
await normalizeUnifiedList(tx, classId, next);
|
|
236
|
+
|
|
237
|
+
return tx.assignment.findUnique({ where: { id: movedId } });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}),
|
|
134
242
|
order: protectedTeacherProcedure
|
|
135
243
|
.input(z.object({
|
|
136
244
|
id: z.string(),
|
|
@@ -138,50 +246,62 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
138
246
|
order: z.number(),
|
|
139
247
|
}))
|
|
140
248
|
.mutation(async ({ ctx, input }) => {
|
|
249
|
+
// Deprecated: prefer `reorder`. For backward-compatibility, set the order then normalize unified list.
|
|
141
250
|
const { id, order } = input;
|
|
142
251
|
|
|
143
|
-
const
|
|
252
|
+
const current = await prisma.assignment.findUnique({
|
|
144
253
|
where: { id },
|
|
145
|
-
|
|
254
|
+
select: { id: true, classId: true },
|
|
146
255
|
});
|
|
147
256
|
|
|
148
|
-
|
|
257
|
+
if (!current) {
|
|
258
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const updated = await prisma.$transaction(async (tx) => {
|
|
262
|
+
await tx.assignment.update({ where: { id }, data: { order } });
|
|
263
|
+
|
|
264
|
+
// Normalize entire unified list
|
|
265
|
+
const unified = await getUnifiedList(tx, current.classId);
|
|
266
|
+
await normalizeUnifiedList(tx, current.classId, unified.map(item => ({ id: item.id, type: item.type })));
|
|
267
|
+
|
|
268
|
+
return tx.assignment.findUnique({ where: { id } });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return updated;
|
|
149
272
|
}),
|
|
150
273
|
|
|
151
274
|
move: protectedTeacherProcedure
|
|
152
275
|
.input(z.object({
|
|
153
276
|
id: z.string(),
|
|
154
277
|
classId: z.string(),
|
|
155
|
-
targetSectionId: z.string(),
|
|
278
|
+
targetSectionId: z.string().nullable().optional(),
|
|
156
279
|
}))
|
|
157
280
|
.mutation(async ({ ctx, input }) => {
|
|
158
|
-
const { id
|
|
159
|
-
|
|
281
|
+
const { id } = input;
|
|
282
|
+
const targetSectionId = (input.targetSectionId ?? null) || null; // normalize empty string to null
|
|
160
283
|
|
|
161
|
-
const
|
|
162
|
-
where: {
|
|
284
|
+
const moved = await prisma.assignment.findUnique({
|
|
285
|
+
where: { id },
|
|
286
|
+
select: { id: true, classId: true, sectionId: true },
|
|
163
287
|
});
|
|
164
288
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
})).map((assignment) => ({
|
|
169
|
-
where: { id: assignment.id },
|
|
170
|
-
data: { order: assignment.order },
|
|
171
|
-
}));
|
|
289
|
+
if (!moved) {
|
|
290
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Assignment not found' });
|
|
291
|
+
}
|
|
172
292
|
|
|
173
|
-
await
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
)
|
|
177
|
-
);
|
|
293
|
+
const updated = await prisma.$transaction(async (tx) => {
|
|
294
|
+
// Update sectionId first
|
|
295
|
+
await tx.assignment.update({ where: { id }, data: { sectionId: targetSectionId } });
|
|
178
296
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
297
|
+
// The unified list ordering remains the same, just the assignment's sectionId changed
|
|
298
|
+
// No need to reorder since we're keeping the same position in the unified list
|
|
299
|
+
// If frontend wants to change position, they should call reorder after move
|
|
300
|
+
|
|
301
|
+
return tx.assignment.findUnique({ where: { id } });
|
|
182
302
|
});
|
|
183
303
|
|
|
184
|
-
return
|
|
304
|
+
return updated;
|
|
185
305
|
}),
|
|
186
306
|
|
|
187
307
|
create: protectedProcedure
|
|
@@ -232,26 +352,10 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
232
352
|
}
|
|
233
353
|
console.log(markSchemeId, gradingBoundaryId);
|
|
234
354
|
|
|
235
|
-
//
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
...(sectionId && {
|
|
240
|
-
sectionId: sectionId,
|
|
241
|
-
}),
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
const stack = assignments.sort((a, b) => (a.order || 0) - (b.order || 0)).map((assignment, index) => ({
|
|
246
|
-
id: assignment.id,
|
|
247
|
-
order: index + 1,
|
|
248
|
-
})).map((assignment) => ({
|
|
249
|
-
where: { id: assignment.id },
|
|
250
|
-
data: { order: assignment.order },
|
|
251
|
-
}));
|
|
252
|
-
|
|
253
|
-
// Create assignment with submissions for all students
|
|
254
|
-
const assignment = await prisma.assignment.create({
|
|
355
|
+
// Create assignment and place at top of its scope within a single transaction
|
|
356
|
+
const teacherId = ctx.user!.id;
|
|
357
|
+
const assignment = await prisma.$transaction(async (tx) => {
|
|
358
|
+
const created = await tx.assignment.create({
|
|
255
359
|
data: {
|
|
256
360
|
title,
|
|
257
361
|
instructions,
|
|
@@ -260,7 +364,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
260
364
|
graded,
|
|
261
365
|
weight,
|
|
262
366
|
type,
|
|
263
|
-
|
|
367
|
+
order: 1,
|
|
264
368
|
inProgress: inProgress || false,
|
|
265
369
|
class: {
|
|
266
370
|
connect: { id: classId }
|
|
@@ -287,11 +391,11 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
287
391
|
}
|
|
288
392
|
}))
|
|
289
393
|
},
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
394
|
+
teacher: {
|
|
395
|
+
connect: { id: teacherId }
|
|
396
|
+
}
|
|
293
397
|
},
|
|
294
|
-
|
|
398
|
+
select: {
|
|
295
399
|
id: true,
|
|
296
400
|
title: true,
|
|
297
401
|
instructions: true,
|
|
@@ -325,14 +429,17 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
325
429
|
name: true
|
|
326
430
|
}
|
|
327
431
|
}
|
|
328
|
-
|
|
329
|
-
|
|
432
|
+
}
|
|
433
|
+
});
|
|
330
434
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
)
|
|
335
|
-
|
|
435
|
+
// Insert new assignment at top of unified list and normalize
|
|
436
|
+
const unified = await getUnifiedList(tx, classId);
|
|
437
|
+
const withoutNew = unified.filter(item => !(item.id === created.id && item.type === 'assignment'));
|
|
438
|
+
const reindexed = [{ id: created.id, type: 'assignment' as const }, ...withoutNew.map(item => ({ id: item.id, type: item.type }))];
|
|
439
|
+
await normalizeUnifiedList(tx, classId, reindexed);
|
|
440
|
+
|
|
441
|
+
return created;
|
|
442
|
+
});
|
|
336
443
|
|
|
337
444
|
// NOTE: Files are now handled via direct upload endpoints
|
|
338
445
|
// The files field in the schema is for metadata only
|
|
@@ -376,6 +483,16 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
376
483
|
}
|
|
377
484
|
});
|
|
378
485
|
}
|
|
486
|
+
|
|
487
|
+
sendNotifications(classData.students.map(student => student.id), {
|
|
488
|
+
title: `🔔 New assignment for ${classData.name}`,
|
|
489
|
+
content:
|
|
490
|
+
`The assignment "${title}" has been created in ${classData.name}.\n
|
|
491
|
+
Due date: ${new Date(dueDate).toLocaleDateString()}.
|
|
492
|
+
[Link to assignment](/class/${classId}/assignments/${assignment.id})`
|
|
493
|
+
}).catch(error => {
|
|
494
|
+
logger.error('Failed to send assignment notifications:');
|
|
495
|
+
});
|
|
379
496
|
|
|
380
497
|
return assignment;
|
|
381
498
|
}),
|
|
@@ -405,6 +522,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
405
522
|
type: true,
|
|
406
523
|
path: true,
|
|
407
524
|
size: true,
|
|
525
|
+
uploadStatus: true,
|
|
408
526
|
thumbnail: {
|
|
409
527
|
select: {
|
|
410
528
|
path: true
|
|
@@ -435,6 +553,31 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
435
553
|
uploadedFiles = await createDirectUploadFiles(files, ctx.user.id, undefined, input.id);
|
|
436
554
|
}
|
|
437
555
|
|
|
556
|
+
// Delete removed attachments from storage before updating database
|
|
557
|
+
if (input.removedAttachments && input.removedAttachments.length > 0) {
|
|
558
|
+
const filesToDelete = assignment.attachments.filter((file) =>
|
|
559
|
+
input.removedAttachments!.includes(file.id)
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
// Delete files from storage (only if they were actually uploaded)
|
|
563
|
+
await Promise.all(filesToDelete.map(async (file) => {
|
|
564
|
+
try {
|
|
565
|
+
// Only delete from GCS if the file was successfully uploaded
|
|
566
|
+
if (file.uploadStatus === 'COMPLETED') {
|
|
567
|
+
// Delete the main file
|
|
568
|
+
await deleteFile(file.path);
|
|
569
|
+
|
|
570
|
+
// Delete thumbnail if it exists
|
|
571
|
+
if (file.thumbnail?.path) {
|
|
572
|
+
await deleteFile(file.thumbnail.path);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
} catch (error) {
|
|
576
|
+
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
577
|
+
}
|
|
578
|
+
}));
|
|
579
|
+
}
|
|
580
|
+
|
|
438
581
|
// Update assignment
|
|
439
582
|
const updatedAssignment = await prisma.assignment.update({
|
|
440
583
|
where: { id },
|
|
@@ -600,15 +743,18 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
600
743
|
...assignment.submissions.flatMap(sub => [...sub.attachments, ...sub.annotations])
|
|
601
744
|
];
|
|
602
745
|
|
|
603
|
-
// Delete files from storage
|
|
746
|
+
// Delete files from storage (only if they were actually uploaded)
|
|
604
747
|
await Promise.all(filesToDelete.map(async (file) => {
|
|
605
748
|
try {
|
|
606
|
-
//
|
|
607
|
-
|
|
749
|
+
// Only delete from GCS if the file was successfully uploaded
|
|
750
|
+
if (file.uploadStatus === 'COMPLETED') {
|
|
751
|
+
// Delete the main file
|
|
752
|
+
await deleteFile(file.path);
|
|
608
753
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
754
|
+
// Delete thumbnail if it exists
|
|
755
|
+
if (file.thumbnail) {
|
|
756
|
+
await deleteFile(file.thumbnail.path);
|
|
757
|
+
}
|
|
612
758
|
}
|
|
613
759
|
} catch (error) {
|
|
614
760
|
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
@@ -753,6 +899,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
753
899
|
select: {
|
|
754
900
|
id: true,
|
|
755
901
|
username: true,
|
|
902
|
+
profile: true,
|
|
756
903
|
},
|
|
757
904
|
},
|
|
758
905
|
assignment: {
|
|
@@ -859,12 +1006,7 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
859
1006
|
select: {
|
|
860
1007
|
id: true,
|
|
861
1008
|
username: true,
|
|
862
|
-
profile:
|
|
863
|
-
select: {
|
|
864
|
-
displayName: true,
|
|
865
|
-
profilePicture: true,
|
|
866
|
-
}
|
|
867
|
-
}
|
|
1009
|
+
profile: true,
|
|
868
1010
|
},
|
|
869
1011
|
},
|
|
870
1012
|
assignment: {
|
|
@@ -1050,15 +1192,18 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1050
1192
|
removedAttachments.includes(file.id)
|
|
1051
1193
|
);
|
|
1052
1194
|
|
|
1053
|
-
// Delete files from storage
|
|
1195
|
+
// Delete files from storage (only if they were actually uploaded)
|
|
1054
1196
|
await Promise.all(filesToDelete.map(async (file) => {
|
|
1055
1197
|
try {
|
|
1056
|
-
//
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1198
|
+
// Only delete from GCS if the file was successfully uploaded
|
|
1199
|
+
if (file.uploadStatus === 'COMPLETED') {
|
|
1200
|
+
// Delete the main file
|
|
1201
|
+
await deleteFile(file.path);
|
|
1202
|
+
|
|
1203
|
+
// Delete thumbnail if it exists
|
|
1204
|
+
if (file.thumbnail?.path) {
|
|
1205
|
+
await deleteFile(file.thumbnail.path);
|
|
1206
|
+
}
|
|
1062
1207
|
}
|
|
1063
1208
|
} catch (error) {
|
|
1064
1209
|
console.warn(`Failed to delete file ${file.path}:`, error);
|
|
@@ -1314,15 +1459,18 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1314
1459
|
removedAttachments.includes(file.id)
|
|
1315
1460
|
);
|
|
1316
1461
|
|
|
1317
|
-
// Delete files from storage
|
|
1462
|
+
// Delete files from storage (only if they were actually uploaded)
|
|
1318
1463
|
await Promise.all(filesToDelete.map(async (file) => {
|
|
1319
1464
|
try {
|
|
1320
|
-
//
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1465
|
+
// Only delete from GCS if the file was successfully uploaded
|
|
1466
|
+
if (file.uploadStatus === 'COMPLETED') {
|
|
1467
|
+
// Delete the main file
|
|
1468
|
+
await deleteFile(file.path);
|
|
1469
|
+
|
|
1470
|
+
// Delete thumbnail if it exists
|
|
1471
|
+
if (file.thumbnail?.path) {
|
|
1472
|
+
await deleteFile(file.thumbnail.path);
|
|
1473
|
+
}
|
|
1326
1474
|
}
|
|
1327
1475
|
} catch (error) {
|
|
1328
1476
|
console.warn(`Failed to delete file ${file.path}:`, error);
|
package/src/routers/auth.ts
CHANGED
package/src/routers/file.ts
CHANGED
|
@@ -328,14 +328,17 @@ export const fileRouter = createTRPCRouter({
|
|
|
328
328
|
});
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
// Delete files from storage
|
|
331
|
+
// Delete files from storage (only if they were actually uploaded)
|
|
332
332
|
try {
|
|
333
|
-
//
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
// Only delete from GCS if the file was successfully uploaded
|
|
334
|
+
if (file.uploadStatus === 'COMPLETED') {
|
|
335
|
+
// Delete the main file
|
|
336
|
+
await deleteFile(file.path);
|
|
337
|
+
|
|
338
|
+
// Delete thumbnail if it exists
|
|
339
|
+
if (file.thumbnail) {
|
|
340
|
+
await deleteFile(file.thumbnail.path);
|
|
341
|
+
}
|
|
339
342
|
}
|
|
340
343
|
} catch (error) {
|
|
341
344
|
logger.warn(`Failed to delete file ${file.path}:`, error as Record<string, any>);
|
package/src/routers/labChat.ts
CHANGED
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
} from '../utils/inference.js';
|
|
11
11
|
import { logger } from '../utils/logger.js';
|
|
12
12
|
import { isAIUser } from '../utils/aiUser.js';
|
|
13
|
-
|
|
14
|
-
// import { uploadFile } from '../lib/googleCloudStorage.js';
|
|
13
|
+
import { bucket } from '../lib/googleCloudStorage.js';
|
|
15
14
|
import { createPdf } from "../lib/jsonConversion.js"
|
|
16
15
|
import OpenAI from 'openai';
|
|
17
16
|
import { v4 as uuidv4 } from "uuid";
|
|
@@ -922,19 +921,29 @@ WHEN CREATING COURSE MATERIALS (docs field):
|
|
|
922
921
|
.substring(0, 50);
|
|
923
922
|
|
|
924
923
|
const filename = `${sanitizedTitle}_${uuidv4().substring(0, 8)}.pdf`;
|
|
925
|
-
|
|
924
|
+
const filePath = `class/generated/${fullLabChat.classId}/${filename}`;
|
|
926
925
|
|
|
927
926
|
logger.info(`PDF ${i + 1} generated successfully`, { labChatId, title: doc.title });
|
|
928
|
-
|
|
929
|
-
//
|
|
927
|
+
|
|
928
|
+
// Upload directly to Google Cloud Storage
|
|
929
|
+
const gcsFile = bucket.file(filePath);
|
|
930
|
+
await gcsFile.save(Buffer.from(pdfBytes), {
|
|
931
|
+
metadata: {
|
|
932
|
+
contentType: 'application/pdf',
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
|
|
930
936
|
logger.info(`PDF ${i + 1} uploaded successfully`, { labChatId, filename });
|
|
931
937
|
|
|
932
938
|
const file = await prisma.file.create({
|
|
933
939
|
data: {
|
|
934
940
|
name: filename,
|
|
935
|
-
path:
|
|
941
|
+
path: filePath,
|
|
936
942
|
type: 'application/pdf',
|
|
943
|
+
size: pdfBytes.length,
|
|
937
944
|
userId: fullLabChat.createdById,
|
|
945
|
+
uploadStatus: 'COMPLETED',
|
|
946
|
+
uploadedAt: new Date(),
|
|
938
947
|
},
|
|
939
948
|
});
|
|
940
949
|
attachmentIds.push(file.id);
|