@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.
Files changed (43) hide show
  1. package/dist/lib/fileUpload.d.ts.map +1 -1
  2. package/dist/lib/fileUpload.js +68 -11
  3. package/dist/lib/googleCloudStorage.d.ts +7 -0
  4. package/dist/lib/googleCloudStorage.d.ts.map +1 -1
  5. package/dist/lib/googleCloudStorage.js +19 -0
  6. package/dist/lib/notificationHandler.d.ts +25 -0
  7. package/dist/lib/notificationHandler.d.ts.map +1 -0
  8. package/dist/lib/notificationHandler.js +28 -0
  9. package/dist/routers/_app.d.ts +156 -22
  10. package/dist/routers/_app.d.ts.map +1 -1
  11. package/dist/routers/announcement.d.ts.map +1 -1
  12. package/dist/routers/announcement.js +26 -2
  13. package/dist/routers/assignment.d.ts +54 -3
  14. package/dist/routers/assignment.d.ts.map +1 -1
  15. package/dist/routers/assignment.js +265 -131
  16. package/dist/routers/auth.js +1 -1
  17. package/dist/routers/file.d.ts.map +1 -1
  18. package/dist/routers/file.js +9 -6
  19. package/dist/routers/labChat.d.ts.map +1 -1
  20. package/dist/routers/labChat.js +13 -5
  21. package/dist/routers/notifications.d.ts +8 -8
  22. package/dist/routers/section.d.ts +16 -0
  23. package/dist/routers/section.d.ts.map +1 -1
  24. package/dist/routers/section.js +139 -30
  25. package/dist/seedDatabase.d.ts +2 -2
  26. package/dist/seedDatabase.d.ts.map +1 -1
  27. package/dist/seedDatabase.js +2 -1
  28. package/dist/utils/logger.d.ts +1 -0
  29. package/dist/utils/logger.d.ts.map +1 -1
  30. package/dist/utils/logger.js +27 -2
  31. package/package.json +2 -2
  32. package/src/lib/fileUpload.ts +69 -11
  33. package/src/lib/googleCloudStorage.ts +19 -0
  34. package/src/lib/notificationHandler.ts +36 -0
  35. package/src/routers/announcement.ts +30 -2
  36. package/src/routers/assignment.ts +230 -82
  37. package/src/routers/auth.ts +1 -1
  38. package/src/routers/file.ts +10 -7
  39. package/src/routers/labChat.ts +15 -6
  40. package/src/routers/section.ts +158 -36
  41. package/src/seedDatabase.ts +2 -1
  42. package/src/utils/logger.ts +29 -2
  43. package/tests/setup.ts +3 -9
@@ -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({
@@ -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
  }>;
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwMxB,CAAC"}
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"}
@@ -56,34 +56,119 @@ export const sectionRouter = createTRPCRouter({
56
56
  }),
57
57
  },
58
58
  });
59
- // find all root items in the class and reorder them
60
- const sections = await prisma.section.findMany({
61
- where: {
62
- classId: input.classId,
63
- },
64
- });
65
- const assignments = await prisma.assignment.findMany({
66
- where: {
67
- classId: input.classId,
68
- sectionId: null,
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
- await prisma.section.update({
155
- where: { id: input.id },
156
- data: {
157
- order: input.order,
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
  }),
@@ -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,kBAsClC;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"}
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"}
@@ -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.riverside.edu', 'student123', 'alex.martinez'),
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'),
@@ -10,6 +10,7 @@ declare class Logger {
10
10
  private isDevelopment;
11
11
  private mode;
12
12
  private levelColors;
13
+ private levelBgColors;
13
14
  private levelEmojis;
14
15
  private constructor();
15
16
  static getInstance(): Logger;
@@ -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;AAwB3D,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,WAAW,CAA2B;IAE9C,OAAO;WAuBO,WAAW,IAAI,MAAM;IAO5B,OAAO,CAAC,IAAI,EAAE,OAAO;IAI5B,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,aAAa;IAiBrB,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"}
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"}
@@ -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
- const levelStr = color + `[${level.toUpperCase()}]` + colors.reset;
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.24",
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": "node dist/seedDatabase.js"
21
+ "seed": "tsx src/seedDatabase.ts"
22
22
  },
23
23
  "dependencies": {
24
24
  "@google-cloud/storage": "^7.16.0",
@@ -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
- console.error('Error creating direct upload file:', error);
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: uploadSuccess ? 'COMPLETED' : 'FAILED',
207
- uploadProgress: uploadSuccess ? 100 : 0,
246
+ uploadStatus: actualUploadSuccess ? 'COMPLETED' : 'FAILED',
247
+ uploadProgress: actualUploadSuccess ? 100 : 0,
208
248
  };
209
249
 
210
- if (!uploadSuccess && errorMessage) {
211
- updateData.uploadError = errorMessage;
250
+ if (!actualUploadSuccess && actualErrorMessage) {
251
+ updateData.uploadError = actualErrorMessage;
212
252
  updateData.uploadRetryCount = { increment: 1 };
213
253
  }
214
254
 
215
- if (uploadSuccess) {
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
- console.error('Error confirming direct upload:', error);
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: Math.min(100, Math.max(0, progress))
296
+ uploadProgress: clamped
247
297
  }
248
298
  });
249
299
  } catch (error) {
250
- console.error('Error updating upload progress:', error);
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
- console.error('Error creating direct upload files:', error);
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
  }