@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, 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
|
|
222
|
+
const current = await prisma.assignment.findUnique({
|
|
127
223
|
where: { id },
|
|
128
|
-
|
|
224
|
+
select: { id: true, classId: true },
|
|
129
225
|
});
|
|
130
|
-
|
|
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
|
|
140
|
-
const
|
|
141
|
-
|
|
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
|
-
|
|
249
|
+
select: { id: true, classId: true, sectionId: true },
|
|
154
250
|
});
|
|
155
|
-
|
|
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
|
-
//
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
//
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
-
//
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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
|
-
//
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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) {
|
package/dist/routers/auth.js
CHANGED
package/dist/routers/file.d.ts
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/routers/file.js
CHANGED
|
@@ -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
|
-
//
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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;
|
|
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"}
|
package/dist/routers/labChat.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
838
|
-
|
|
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:
|
|
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
|
}>;
|