@studious-lms/server 1.1.24 → 1.1.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.map +1 -1
- package/dist/lib/fileUpload.js +68 -11
- 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 +156 -22
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/announcement.js +26 -2
- package/dist/routers/assignment.d.ts +54 -3
- 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.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/src/lib/fileUpload.ts +69 -11
- package/src/lib/googleCloudStorage.ts +19 -0
- package/src/lib/notificationHandler.ts +36 -0
- package/src/routers/announcement.ts +30 -2
- 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
package/dist/routers/auth.js
CHANGED
|
@@ -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
|
}>;
|
|
@@ -31,6 +31,22 @@ export declare const sectionRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
31
31
|
};
|
|
32
32
|
meta: object;
|
|
33
33
|
}>;
|
|
34
|
+
reorder: import("@trpc/server").TRPCMutationProcedure<{
|
|
35
|
+
input: {
|
|
36
|
+
classId: string;
|
|
37
|
+
movedId: string;
|
|
38
|
+
position: "start" | "end" | "before" | "after";
|
|
39
|
+
targetId?: string | undefined;
|
|
40
|
+
};
|
|
41
|
+
output: {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
color: string | null;
|
|
45
|
+
classId: string;
|
|
46
|
+
order: number | null;
|
|
47
|
+
} | null;
|
|
48
|
+
meta: object;
|
|
49
|
+
}>;
|
|
34
50
|
update: import("@trpc/server").TRPCMutationProcedure<{
|
|
35
51
|
input: {
|
|
36
52
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../src/routers/section.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuBxB,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"section.d.ts","sourceRoot":"","sources":["../../src/routers/section.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAuBxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkUxB,CAAC"}
|
package/dist/routers/section.js
CHANGED
|
@@ -56,34 +56,119 @@ export const sectionRouter = createTRPCRouter({
|
|
|
56
56
|
}),
|
|
57
57
|
},
|
|
58
58
|
});
|
|
59
|
-
//
|
|
60
|
-
const sections = await
|
|
61
|
-
|
|
62
|
-
classId: input.classId,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
const stack = [...sections, ...assignments].sort((a, b) => (a.order || 0) - (b.order || 0)).map((item, index) => ({
|
|
72
|
-
id: item.id,
|
|
73
|
-
order: index + 1,
|
|
74
|
-
})).map((item) => ({
|
|
75
|
-
where: { id: item.id },
|
|
76
|
-
data: { order: item.order },
|
|
77
|
-
}));
|
|
78
|
-
// Update sections and assignments with their new order
|
|
79
|
-
await Promise.all([
|
|
80
|
-
...stack.filter(item => sections.some(s => s.id === item.where.id))
|
|
81
|
-
.map(({ where, data }) => prisma.section.update({ where, data })),
|
|
82
|
-
...stack.filter(item => assignments.some(a => a.id === item.where.id))
|
|
83
|
-
.map(({ where, data }) => prisma.assignment.update({ where, data }))
|
|
59
|
+
// Insert new section at top of unified list (sections + assignments) and normalize
|
|
60
|
+
const [sections, assignments] = await Promise.all([
|
|
61
|
+
prisma.section.findMany({
|
|
62
|
+
where: { classId: input.classId },
|
|
63
|
+
select: { id: true, order: true },
|
|
64
|
+
}),
|
|
65
|
+
prisma.assignment.findMany({
|
|
66
|
+
where: { classId: input.classId },
|
|
67
|
+
select: { id: true, order: true },
|
|
68
|
+
}),
|
|
84
69
|
]);
|
|
70
|
+
const unified = [
|
|
71
|
+
...sections.map(s => ({ id: s.id, order: s.order, type: 'section' })),
|
|
72
|
+
...assignments.map(a => ({ id: a.id, order: a.order, type: 'assignment' })),
|
|
73
|
+
].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
|
|
74
|
+
const withoutNew = unified.filter(item => !(item.id === section.id && item.type === 'section'));
|
|
75
|
+
const reindexed = [{ id: section.id, type: 'section' }, ...withoutNew.map(item => ({ id: item.id, type: item.type }))];
|
|
76
|
+
await Promise.all(reindexed.map((item, index) => {
|
|
77
|
+
if (item.type === 'section') {
|
|
78
|
+
return prisma.section.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
return prisma.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
82
|
+
}
|
|
83
|
+
}));
|
|
85
84
|
return section;
|
|
86
85
|
}),
|
|
86
|
+
reorder: protectedProcedure
|
|
87
|
+
.input(z.object({
|
|
88
|
+
classId: z.string(),
|
|
89
|
+
movedId: z.string(), // Section ID
|
|
90
|
+
// One of: place at start/end of unified list, or relative to targetId (can be section or assignment)
|
|
91
|
+
position: z.enum(['start', 'end', 'before', 'after']),
|
|
92
|
+
targetId: z.string().optional(), // Can be a section ID or assignment ID
|
|
93
|
+
}))
|
|
94
|
+
.mutation(async ({ ctx, input }) => {
|
|
95
|
+
if (!ctx.user) {
|
|
96
|
+
throw new TRPCError({
|
|
97
|
+
code: "UNAUTHORIZED",
|
|
98
|
+
message: "User must be authenticated",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const { classId, movedId, position, targetId } = input;
|
|
102
|
+
const moved = await prisma.section.findFirst({
|
|
103
|
+
where: { id: movedId, classId },
|
|
104
|
+
select: { id: true, classId: true },
|
|
105
|
+
});
|
|
106
|
+
if (!moved) {
|
|
107
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Section not found' });
|
|
108
|
+
}
|
|
109
|
+
if ((position === 'before' || position === 'after') && !targetId) {
|
|
110
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId required for before/after' });
|
|
111
|
+
}
|
|
112
|
+
const result = await prisma.$transaction(async (tx) => {
|
|
113
|
+
const [sections, assignments] = await Promise.all([
|
|
114
|
+
tx.section.findMany({
|
|
115
|
+
where: { classId },
|
|
116
|
+
select: { id: true, order: true },
|
|
117
|
+
}),
|
|
118
|
+
tx.assignment.findMany({
|
|
119
|
+
where: { classId },
|
|
120
|
+
select: { id: true, order: true },
|
|
121
|
+
}),
|
|
122
|
+
]);
|
|
123
|
+
const unified = [
|
|
124
|
+
...sections.map(s => ({ id: s.id, order: s.order, type: 'section' })),
|
|
125
|
+
...assignments.map(a => ({ id: a.id, order: a.order, type: 'assignment' })),
|
|
126
|
+
].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
|
|
127
|
+
const movedIdx = unified.findIndex(item => item.id === movedId && item.type === 'section');
|
|
128
|
+
if (movedIdx === -1) {
|
|
129
|
+
throw new TRPCError({ code: 'NOT_FOUND', message: 'Section not found in unified list' });
|
|
130
|
+
}
|
|
131
|
+
const withoutMoved = unified.filter(item => !(item.id === movedId && item.type === 'section'));
|
|
132
|
+
let next = [];
|
|
133
|
+
if (position === 'start') {
|
|
134
|
+
next = [{ id: movedId, type: 'section' }, ...withoutMoved.map(item => ({ id: item.id, type: item.type }))];
|
|
135
|
+
}
|
|
136
|
+
else if (position === 'end') {
|
|
137
|
+
next = [...withoutMoved.map(item => ({ id: item.id, type: item.type })), { id: movedId, type: 'section' }];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
const targetIdx = withoutMoved.findIndex(item => item.id === targetId);
|
|
141
|
+
if (targetIdx === -1) {
|
|
142
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'targetId not found in unified list' });
|
|
143
|
+
}
|
|
144
|
+
if (position === 'before') {
|
|
145
|
+
next = [
|
|
146
|
+
...withoutMoved.slice(0, targetIdx).map(item => ({ id: item.id, type: item.type })),
|
|
147
|
+
{ id: movedId, type: 'section' },
|
|
148
|
+
...withoutMoved.slice(targetIdx).map(item => ({ id: item.id, type: item.type })),
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
next = [
|
|
153
|
+
...withoutMoved.slice(0, targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
|
|
154
|
+
{ id: movedId, type: 'section' },
|
|
155
|
+
...withoutMoved.slice(targetIdx + 1).map(item => ({ id: item.id, type: item.type })),
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Normalize to 1..n
|
|
160
|
+
await Promise.all(next.map((item, index) => {
|
|
161
|
+
if (item.type === 'section') {
|
|
162
|
+
return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
166
|
+
}
|
|
167
|
+
}));
|
|
168
|
+
return tx.section.findUnique({ where: { id: movedId } });
|
|
169
|
+
});
|
|
170
|
+
return result;
|
|
171
|
+
}),
|
|
87
172
|
update: protectedProcedure
|
|
88
173
|
.input(updateSectionSchema)
|
|
89
174
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -151,11 +236,35 @@ export const sectionRouter = createTRPCRouter({
|
|
|
151
236
|
message: "Class not found or you are not a teacher",
|
|
152
237
|
});
|
|
153
238
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
239
|
+
// Update order and normalize unified list
|
|
240
|
+
await prisma.$transaction(async (tx) => {
|
|
241
|
+
await tx.section.update({
|
|
242
|
+
where: { id: input.id },
|
|
243
|
+
data: { order: input.order },
|
|
244
|
+
});
|
|
245
|
+
// Normalize entire unified list
|
|
246
|
+
const [sections, assignments] = await Promise.all([
|
|
247
|
+
tx.section.findMany({
|
|
248
|
+
where: { classId: input.classId },
|
|
249
|
+
select: { id: true, order: true },
|
|
250
|
+
}),
|
|
251
|
+
tx.assignment.findMany({
|
|
252
|
+
where: { classId: input.classId },
|
|
253
|
+
select: { id: true, order: true },
|
|
254
|
+
}),
|
|
255
|
+
]);
|
|
256
|
+
const unified = [
|
|
257
|
+
...sections.map(s => ({ id: s.id, order: s.order, type: 'section' })),
|
|
258
|
+
...assignments.map(a => ({ id: a.id, order: a.order, type: 'assignment' })),
|
|
259
|
+
].sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER));
|
|
260
|
+
await Promise.all(unified.map((item, index) => {
|
|
261
|
+
if (item.type === 'section') {
|
|
262
|
+
return tx.section.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
return tx.assignment.update({ where: { id: item.id }, data: { order: index + 1 } });
|
|
266
|
+
}
|
|
267
|
+
}));
|
|
159
268
|
});
|
|
160
269
|
return { id: input.id };
|
|
161
270
|
}),
|
package/dist/seedDatabase.d.ts
CHANGED
|
@@ -14,9 +14,9 @@ export declare function addNotification(userId: string, title: string, content:
|
|
|
14
14
|
title: string;
|
|
15
15
|
content: string;
|
|
16
16
|
createdAt: Date;
|
|
17
|
-
read: boolean;
|
|
18
|
-
receiverId: string;
|
|
19
17
|
senderId: string | null;
|
|
18
|
+
receiverId: string;
|
|
19
|
+
read: boolean;
|
|
20
20
|
}>;
|
|
21
21
|
export declare const seedDatabase: () => Promise<void>;
|
|
22
22
|
//# sourceMappingURL=seedDatabase.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seedDatabase.d.ts","sourceRoot":"","sources":["../src/seedDatabase.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,
|
|
1
|
+
{"version":3,"file":"seedDatabase.d.ts","sourceRoot":"","sources":["../src/seedDatabase.ts"],"names":[],"mappings":"AAIA,wBAAsB,aAAa,kBAuClC;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;;;;;;;;;GAOjF;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;;;;;;;;GAQnF;AAED,eAAO,MAAM,YAAY,qBAq+CxB,CAAC"}
|
package/dist/seedDatabase.js
CHANGED
|
@@ -4,6 +4,7 @@ import { logger } from "./utils/logger.js";
|
|
|
4
4
|
export async function clearDatabase() {
|
|
5
5
|
// Delete in order to respect foreign key constraints
|
|
6
6
|
// Delete notifications first (they reference users)
|
|
7
|
+
logger.info('Clearing database');
|
|
7
8
|
await prisma.notification.deleteMany();
|
|
8
9
|
// Delete chat-related records
|
|
9
10
|
await prisma.mention.deleteMany();
|
|
@@ -77,7 +78,7 @@ export const seedDatabase = async () => {
|
|
|
77
78
|
]);
|
|
78
79
|
// 3. Create Students (realistic names)
|
|
79
80
|
const students = await Promise.all([
|
|
80
|
-
createUser('alex.martinez@student.
|
|
81
|
+
createUser('alex.martinez@student.rverside.eidu', 'student123', 'alex.martinez'),
|
|
81
82
|
createUser('sophia.williams@student.riverside.edu', 'student123', 'sophia.williams'),
|
|
82
83
|
createUser('james.brown@student.riverside.edu', 'student123', 'james.brown'),
|
|
83
84
|
createUser('olivia.taylor@student.riverside.edu', 'student123', 'olivia.taylor'),
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,oBAAY,QAAQ;IAClB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;CAChB;AAED,KAAK,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA,oBAAY,QAAQ;IAClB,IAAI,SAAS;IACb,IAAI,SAAS;IACb,KAAK,UAAU;IACf,KAAK,UAAU;CAChB;AAED,KAAK,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAyC3D,cAAM,MAAM;IACV,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAS;IAChC,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,IAAI,CAAU;IACtB,OAAO,CAAC,WAAW,CAA2B;IAC9C,OAAO,CAAC,aAAa,CAA2B;IAChD,OAAO,CAAC,WAAW,CAA2B;IAE9C,OAAO;WA8BO,WAAW,IAAI,MAAM;IAO5B,OAAO,CAAC,IAAI,EAAE,OAAO;IAI5B,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,GAAG;IAqCJ,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAInD,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAInD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAIpD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;CAG5D;AAED,eAAO,MAAM,MAAM,QAAuB,CAAC"}
|
package/dist/utils/logger.js
CHANGED
|
@@ -17,7 +17,24 @@ const colors = {
|
|
|
17
17
|
magenta: '\x1b[35m',
|
|
18
18
|
cyan: '\x1b[36m',
|
|
19
19
|
white: '\x1b[37m',
|
|
20
|
-
gray: '\x1b[90m'
|
|
20
|
+
gray: '\x1b[90m',
|
|
21
|
+
// Background colors
|
|
22
|
+
bgRed: '\x1b[41m',
|
|
23
|
+
bgGreen: '\x1b[42m',
|
|
24
|
+
bgYellow: '\x1b[43m',
|
|
25
|
+
bgBlue: '\x1b[44m',
|
|
26
|
+
bgMagenta: '\x1b[45m',
|
|
27
|
+
bgCyan: '\x1b[46m',
|
|
28
|
+
bgWhite: '\x1b[47m',
|
|
29
|
+
bgGray: '\x1b[100m',
|
|
30
|
+
// Bright background colors
|
|
31
|
+
bgBrightRed: '\x1b[101m',
|
|
32
|
+
bgBrightGreen: '\x1b[102m',
|
|
33
|
+
bgBrightYellow: '\x1b[103m',
|
|
34
|
+
bgBrightBlue: '\x1b[104m',
|
|
35
|
+
bgBrightMagenta: '\x1b[105m',
|
|
36
|
+
bgBrightCyan: '\x1b[106m',
|
|
37
|
+
bgBrightWhite: '\x1b[107m'
|
|
21
38
|
};
|
|
22
39
|
class Logger {
|
|
23
40
|
constructor() {
|
|
@@ -30,6 +47,12 @@ class Logger {
|
|
|
30
47
|
[LogLevel.ERROR]: colors.red,
|
|
31
48
|
[LogLevel.DEBUG]: colors.magenta
|
|
32
49
|
};
|
|
50
|
+
this.levelBgColors = {
|
|
51
|
+
[LogLevel.INFO]: colors.bgBlue,
|
|
52
|
+
[LogLevel.WARN]: colors.bgYellow,
|
|
53
|
+
[LogLevel.ERROR]: colors.bgRed,
|
|
54
|
+
[LogLevel.DEBUG]: colors.bgMagenta
|
|
55
|
+
};
|
|
33
56
|
this.levelEmojis = {
|
|
34
57
|
[LogLevel.INFO]: 'ℹ️',
|
|
35
58
|
[LogLevel.WARN]: '⚠️',
|
|
@@ -70,9 +93,11 @@ class Logger {
|
|
|
70
93
|
formatMessage(logMessage) {
|
|
71
94
|
const { level, message, timestamp, context } = logMessage;
|
|
72
95
|
const color = this.levelColors[level];
|
|
96
|
+
const bgColor = this.levelBgColors[level];
|
|
73
97
|
const emoji = this.levelEmojis[level];
|
|
74
98
|
const timestampStr = colors.gray + `[${timestamp}]` + colors.reset;
|
|
75
|
-
|
|
99
|
+
// Use background color for level badge like Vitest
|
|
100
|
+
const levelStr = colors.white + bgColor + ` ${level.toUpperCase()} ` + colors.reset;
|
|
76
101
|
const emojiStr = emoji + ' ';
|
|
77
102
|
const messageStr = colors.bright + message + colors.reset;
|
|
78
103
|
const contextStr = context
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@studious-lms/server",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.26",
|
|
4
4
|
"description": "Backend server for Studious application",
|
|
5
5
|
"main": "dist/exportType.js",
|
|
6
6
|
"types": "dist/exportType.d.ts",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"generate": "npx prisma generate",
|
|
19
19
|
"prepublishOnly": "npm run generate && npm run build",
|
|
20
20
|
"test": "vitest",
|
|
21
|
-
"seed": "
|
|
21
|
+
"seed": "tsx src/seedDatabase.ts"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@google-cloud/storage": "^7.16.0",
|
package/src/lib/fileUpload.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TRPCError } from "@trpc/server";
|
|
2
2
|
import { v4 as uuidv4 } from "uuid";
|
|
3
|
-
import { getSignedUrl } from "./googleCloudStorage.js";
|
|
3
|
+
import { getSignedUrl, objectExists } from "./googleCloudStorage.js";
|
|
4
4
|
import { generateMediaThumbnail } from "./thumbnailGenerator.js";
|
|
5
5
|
import { prisma } from "./prisma.js";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
@@ -182,7 +182,11 @@ export async function createDirectUploadFile(
|
|
|
182
182
|
uploadSessionId
|
|
183
183
|
};
|
|
184
184
|
} catch (error) {
|
|
185
|
-
|
|
185
|
+
logger.error('Error creating direct upload file:', {error: error instanceof Error ? {
|
|
186
|
+
name: error.name,
|
|
187
|
+
message: error.message,
|
|
188
|
+
stack: error.stack,
|
|
189
|
+
} : error});
|
|
186
190
|
throw new TRPCError({
|
|
187
191
|
code: 'INTERNAL_SERVER_ERROR',
|
|
188
192
|
message: 'Failed to create direct upload file',
|
|
@@ -202,17 +206,53 @@ export async function confirmDirectUpload(
|
|
|
202
206
|
errorMessage?: string
|
|
203
207
|
): Promise<void> {
|
|
204
208
|
try {
|
|
209
|
+
// First fetch the file record to get the object path
|
|
210
|
+
const fileRecord = await prisma.file.findUnique({
|
|
211
|
+
where: { id: fileId },
|
|
212
|
+
select: { path: true }
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (!fileRecord) {
|
|
216
|
+
throw new TRPCError({
|
|
217
|
+
code: 'NOT_FOUND',
|
|
218
|
+
message: 'File record not found',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let actualUploadSuccess = uploadSuccess;
|
|
223
|
+
let actualErrorMessage = errorMessage;
|
|
224
|
+
|
|
225
|
+
// If uploadSuccess is true, verify the object actually exists in GCS
|
|
226
|
+
if (uploadSuccess) {
|
|
227
|
+
try {
|
|
228
|
+
const exists = await objectExists(process.env.GOOGLE_CLOUD_BUCKET_NAME!, fileRecord.path);
|
|
229
|
+
if (!exists) {
|
|
230
|
+
actualUploadSuccess = false;
|
|
231
|
+
actualErrorMessage = 'File upload reported as successful but object not found in Google Cloud Storage';
|
|
232
|
+
logger.error(`File upload verification failed for ${fileId}: object ${fileRecord.path} not found in GCS`);
|
|
233
|
+
}
|
|
234
|
+
} catch (error) {
|
|
235
|
+
logger.error(`Error verifying file existence in GCS for ${fileId}:`, {error: error instanceof Error ? {
|
|
236
|
+
name: error.name,
|
|
237
|
+
message: error.message,
|
|
238
|
+
stack: error.stack,
|
|
239
|
+
} : error});
|
|
240
|
+
actualUploadSuccess = false;
|
|
241
|
+
actualErrorMessage = 'Failed to verify file existence in Google Cloud Storage';
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
205
245
|
const updateData: any = {
|
|
206
|
-
uploadStatus:
|
|
207
|
-
uploadProgress:
|
|
246
|
+
uploadStatus: actualUploadSuccess ? 'COMPLETED' : 'FAILED',
|
|
247
|
+
uploadProgress: actualUploadSuccess ? 100 : 0,
|
|
208
248
|
};
|
|
209
249
|
|
|
210
|
-
if (!
|
|
211
|
-
updateData.uploadError =
|
|
250
|
+
if (!actualUploadSuccess && actualErrorMessage) {
|
|
251
|
+
updateData.uploadError = actualErrorMessage;
|
|
212
252
|
updateData.uploadRetryCount = { increment: 1 };
|
|
213
253
|
}
|
|
214
254
|
|
|
215
|
-
if (
|
|
255
|
+
if (actualUploadSuccess) {
|
|
216
256
|
updateData.uploadedAt = new Date();
|
|
217
257
|
}
|
|
218
258
|
|
|
@@ -221,7 +261,7 @@ export async function confirmDirectUpload(
|
|
|
221
261
|
data: updateData
|
|
222
262
|
});
|
|
223
263
|
} catch (error) {
|
|
224
|
-
|
|
264
|
+
logger.error('Error confirming direct upload:', {error});
|
|
225
265
|
throw new TRPCError({
|
|
226
266
|
code: 'INTERNAL_SERVER_ERROR',
|
|
227
267
|
message: 'Failed to confirm upload',
|
|
@@ -239,15 +279,29 @@ export async function updateUploadProgress(
|
|
|
239
279
|
progress: number
|
|
240
280
|
): Promise<void> {
|
|
241
281
|
try {
|
|
282
|
+
// await prisma.file.update({
|
|
283
|
+
// where: { id: fileId },
|
|
284
|
+
// data: {
|
|
285
|
+
// uploadStatus: 'UPLOADING',
|
|
286
|
+
// uploadProgress: Math.min(100, Math.max(0, progress))
|
|
287
|
+
// }
|
|
288
|
+
// });
|
|
289
|
+
const current = await prisma.file.findUnique({ where: { id: fileId }, select: { uploadStatus: true } });
|
|
290
|
+
if (!current || ['COMPLETED','FAILED','CANCELLED'].includes(current.uploadStatus as string)) return;
|
|
291
|
+
const clamped = Math.min(100, Math.max(0, progress));
|
|
242
292
|
await prisma.file.update({
|
|
243
293
|
where: { id: fileId },
|
|
244
294
|
data: {
|
|
245
295
|
uploadStatus: 'UPLOADING',
|
|
246
|
-
uploadProgress:
|
|
296
|
+
uploadProgress: clamped
|
|
247
297
|
}
|
|
248
298
|
});
|
|
249
299
|
} catch (error) {
|
|
250
|
-
|
|
300
|
+
logger.error('Error updating upload progress:', {error: error instanceof Error ? {
|
|
301
|
+
name: error.name,
|
|
302
|
+
message: error.message,
|
|
303
|
+
stack: error.stack,
|
|
304
|
+
} : error});
|
|
251
305
|
throw new TRPCError({
|
|
252
306
|
code: 'INTERNAL_SERVER_ERROR',
|
|
253
307
|
message: 'Failed to update upload progress',
|
|
@@ -277,7 +331,11 @@ export async function createDirectUploadFiles(
|
|
|
277
331
|
);
|
|
278
332
|
return await Promise.all(uploadPromises);
|
|
279
333
|
} catch (error) {
|
|
280
|
-
|
|
334
|
+
logger.error('Error creating direct upload files:', {error: error instanceof Error ? {
|
|
335
|
+
name: error.name,
|
|
336
|
+
message: error.message,
|
|
337
|
+
stack: error.stack,
|
|
338
|
+
} : error});
|
|
281
339
|
throw new TRPCError({
|
|
282
340
|
code: 'INTERNAL_SERVER_ERROR',
|
|
283
341
|
message: 'Failed to create direct upload files',
|
|
@@ -62,4 +62,23 @@ export async function deleteFile(filePath: string): Promise<void> {
|
|
|
62
62
|
message: 'Failed to delete file from storage',
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Checks if an object exists in Google Cloud Storage
|
|
69
|
+
* @param bucketName The name of the bucket (unused, uses default bucket)
|
|
70
|
+
* @param objectPath The path of the object to check
|
|
71
|
+
* @returns Promise<boolean> True if the object exists, false otherwise
|
|
72
|
+
*/
|
|
73
|
+
export async function objectExists(bucketName: string, objectPath: string): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
const [exists] = await bucket.file(objectPath).exists();
|
|
76
|
+
return exists;
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Error checking if object exists in Google Cloud Storage:', error);
|
|
79
|
+
throw new TRPCError({
|
|
80
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
81
|
+
message: 'Failed to check object existence',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
65
84
|
}
|