@studious-lms/server 1.1.22 → 1.1.24

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.
@@ -113,6 +113,18 @@ const confirmSubmissionUploadSchema = z.object({
113
113
  errorMessage: z.string().optional(),
114
114
  });
115
115
 
116
+ const getAnnotationUploadUrlsSchema = z.object({
117
+ submissionId: z.string(),
118
+ classId: z.string(),
119
+ files: z.array(directFileSchema),
120
+ });
121
+
122
+ const confirmAnnotationUploadSchema = z.object({
123
+ fileId: z.string(),
124
+ uploadSuccess: z.boolean(),
125
+ errorMessage: z.string().optional(),
126
+ });
127
+
116
128
  const updateUploadProgressSchema = z.object({
117
129
  fileId: z.string(),
118
130
  progress: z.number().min(0).max(100),
@@ -741,7 +753,6 @@ export const assignmentRouter = createTRPCRouter({
741
753
  select: {
742
754
  id: true,
743
755
  username: true,
744
- profile: true,
745
756
  },
746
757
  },
747
758
  assignment: {
@@ -848,7 +859,12 @@ export const assignmentRouter = createTRPCRouter({
848
859
  select: {
849
860
  id: true,
850
861
  username: true,
851
- profile: true,
862
+ profile: {
863
+ select: {
864
+ displayName: true,
865
+ profilePicture: true,
866
+ }
867
+ }
852
868
  },
853
869
  },
854
870
  assignment: {
@@ -1270,31 +1286,13 @@ export const assignmentRouter = createTRPCRouter({
1270
1286
  });
1271
1287
  }
1272
1288
 
1273
- let uploadedFiles: UploadedFile[] = [];
1289
+ // NOTE: Teacher annotation files are now handled via direct upload endpoints
1290
+ // Use getAnnotationUploadUrls and confirmAnnotationUpload endpoints instead
1291
+ // The newAttachments field is deprecated for annotations
1274
1292
  if (newAttachments && newAttachments.length > 0) {
1275
- // Store files in a class and assignment specific directory
1276
- uploadedFiles = await createDirectUploadFiles(newAttachments, ctx.user.id, undefined, undefined, submission.id);
1277
- }
1278
-
1279
- // Update submission with new file attachments
1280
- if (uploadedFiles.length > 0) {
1281
- await prisma.submission.update({
1282
- where: { id: submission.id },
1283
- data: {
1284
- annotations: {
1285
- create: uploadedFiles.map(file => ({
1286
- name: file.name,
1287
- type: file.type,
1288
- size: file.size,
1289
- path: file.path,
1290
- ...(file.thumbnailId && {
1291
- thumbnail: {
1292
- connect: { id: file.thumbnailId }
1293
- }
1294
- })
1295
- }))
1296
- }
1297
- }
1293
+ throw new TRPCError({
1294
+ code: "BAD_REQUEST",
1295
+ message: "Direct file upload is deprecated. Use getAnnotationUploadUrls endpoint instead.",
1298
1296
  });
1299
1297
  }
1300
1298
 
@@ -2054,6 +2052,109 @@ export const assignmentRouter = createTRPCRouter({
2054
2052
  };
2055
2053
  }),
2056
2054
 
2055
+ getAnnotationUploadUrls: protectedTeacherProcedure
2056
+ .input(getAnnotationUploadUrlsSchema)
2057
+ .mutation(async ({ ctx, input }) => {
2058
+ const { submissionId, classId, files } = input;
2059
+
2060
+ if (!ctx.user) {
2061
+ throw new TRPCError({
2062
+ code: "UNAUTHORIZED",
2063
+ message: "You must be logged in to upload files",
2064
+ });
2065
+ }
2066
+
2067
+ // Verify submission exists and user is a teacher of the class
2068
+ const submission = await prisma.submission.findFirst({
2069
+ where: {
2070
+ id: submissionId,
2071
+ assignment: {
2072
+ classId: classId,
2073
+ class: {
2074
+ teachers: {
2075
+ some: {
2076
+ id: ctx.user.id,
2077
+ },
2078
+ },
2079
+ },
2080
+ },
2081
+ },
2082
+ });
2083
+
2084
+ if (!submission) {
2085
+ throw new TRPCError({
2086
+ code: "NOT_FOUND",
2087
+ message: "Submission not found or you are not a teacher of this class",
2088
+ });
2089
+ }
2090
+
2091
+ // Create direct upload files for annotations
2092
+ // Note: We pass submissionId as the 5th parameter, but these are annotations not submission files
2093
+ // We need to store them separately, so we'll use a different approach
2094
+ const directUploadFiles = await createDirectUploadFiles(
2095
+ files,
2096
+ ctx.user.id,
2097
+ undefined, // No specific directory
2098
+ undefined, // No assignment ID
2099
+ undefined // Don't link to submission yet (will be linked in confirmAnnotationUpload)
2100
+ );
2101
+
2102
+ // Store the submissionId in the file record so we can link it to annotations later
2103
+ await Promise.all(
2104
+ directUploadFiles.map(file =>
2105
+ prisma.file.update({
2106
+ where: { id: file.id },
2107
+ data: {
2108
+ annotationId: submissionId, // Store as annotation
2109
+ }
2110
+ })
2111
+ )
2112
+ );
2113
+
2114
+ return {
2115
+ success: true,
2116
+ uploadFiles: directUploadFiles,
2117
+ };
2118
+ }),
2119
+
2120
+ confirmAnnotationUpload: protectedTeacherProcedure
2121
+ .input(confirmAnnotationUploadSchema)
2122
+ .mutation(async ({ ctx, input }) => {
2123
+ const { fileId, uploadSuccess, errorMessage } = input;
2124
+
2125
+ if (!ctx.user) {
2126
+ throw new TRPCError({
2127
+ code: "UNAUTHORIZED",
2128
+ message: "You must be logged in",
2129
+ });
2130
+ }
2131
+
2132
+ // Verify file belongs to user and is an annotation file
2133
+ const file = await prisma.file.findFirst({
2134
+ where: {
2135
+ id: fileId,
2136
+ userId: ctx.user.id,
2137
+ annotationId: {
2138
+ not: null,
2139
+ },
2140
+ },
2141
+ });
2142
+
2143
+ if (!file) {
2144
+ throw new TRPCError({
2145
+ code: "NOT_FOUND",
2146
+ message: "File not found or you don't have permission",
2147
+ });
2148
+ }
2149
+
2150
+ await confirmDirectUpload(fileId, uploadSuccess, errorMessage);
2151
+
2152
+ return {
2153
+ success: true,
2154
+ message: uploadSuccess ? "Annotation upload confirmed successfully" : "Annotation upload failed",
2155
+ };
2156
+ }),
2157
+
2057
2158
  updateUploadProgress: protectedProcedure
2058
2159
  .input(updateUploadProgressSchema)
2059
2160
  .mutation(async ({ ctx, input }) => {
@@ -793,4 +793,113 @@ export const folderRouter = createTRPCRouter({
793
793
 
794
794
  return parents;
795
795
  }),
796
+
797
+ getFolderUploadUrls: protectedTeacherProcedure
798
+ .input(z.object({
799
+ classId: z.string(),
800
+ folderId: z.string(),
801
+ files: z.array(directFileSchema),
802
+ }))
803
+ .mutation(async ({ ctx, input }) => {
804
+ const { classId, folderId, files } = input;
805
+
806
+ if (!ctx.user) {
807
+ throw new TRPCError({
808
+ code: "UNAUTHORIZED",
809
+ message: "You must be logged in to upload files",
810
+ });
811
+ }
812
+
813
+ // Verify user is a teacher of the class
814
+ const classData = await prisma.class.findFirst({
815
+ where: {
816
+ id: classId,
817
+ teachers: {
818
+ some: {
819
+ id: ctx.user.id,
820
+ },
821
+ },
822
+ },
823
+ });
824
+
825
+ if (!classData) {
826
+ throw new TRPCError({
827
+ code: "NOT_FOUND",
828
+ message: "Class not found or you are not a teacher",
829
+ });
830
+ }
831
+
832
+ // Verify folder exists
833
+ const folder = await prisma.folder.findUnique({
834
+ where: {
835
+ id: folderId,
836
+ },
837
+ });
838
+
839
+ if (!folder) {
840
+ throw new TRPCError({
841
+ code: "NOT_FOUND",
842
+ message: "Folder not found",
843
+ });
844
+ }
845
+
846
+ // Verify folder belongs to the class by traversing parent hierarchy
847
+ // Only root folders have classId, child folders use parentFolderId
848
+ let currentFolder: typeof folder | null = folder;
849
+ let belongsToClass = false;
850
+
851
+ while (currentFolder) {
852
+ // Check if we've reached a root folder with the matching classId
853
+ if (currentFolder.classId === classId) {
854
+ belongsToClass = true;
855
+ break;
856
+ }
857
+
858
+ // If this folder has a parent, traverse up
859
+ if (currentFolder.parentFolderId) {
860
+ currentFolder = await prisma.folder.findUnique({
861
+ where: { id: currentFolder.parentFolderId },
862
+ });
863
+ } else {
864
+ // Reached a root folder without matching classId
865
+ break;
866
+ }
867
+ }
868
+
869
+ if (!belongsToClass) {
870
+ throw new TRPCError({
871
+ code: "FORBIDDEN",
872
+ message: "Folder does not belong to this class",
873
+ });
874
+ }
875
+
876
+ // Create direct upload files
877
+ const uploadFiles = await createDirectUploadFiles(files, ctx.user.id, folder.id);
878
+
879
+ return uploadFiles;
880
+ }),
881
+
882
+ confirmFolderUpload: protectedTeacherProcedure
883
+ .input(z.object({
884
+ fileId: z.string(),
885
+ uploadSuccess: z.boolean(),
886
+ }))
887
+ .mutation(async ({ ctx, input }) => {
888
+ const { fileId, uploadSuccess } = input;
889
+
890
+ if (!ctx.user) {
891
+ throw new TRPCError({
892
+ code: "UNAUTHORIZED",
893
+ message: "You must be logged in to confirm uploads",
894
+ });
895
+ }
896
+
897
+ // Import the confirmDirectUpload function
898
+ const { confirmDirectUpload } = await import("../lib/fileUpload.js");
899
+
900
+ // Confirm the upload
901
+ await confirmDirectUpload(fileId, uploadSuccess);
902
+
903
+ return { success: true };
904
+ }),
796
905
  });
@@ -5,7 +5,6 @@ import { logger } from "./utils/logger.js";
5
5
  export async function clearDatabase() {
6
6
  // Delete in order to respect foreign key constraints
7
7
  // Delete notifications first (they reference users)
8
- logger.info('Clearing database');
9
8
  await prisma.notification.deleteMany();
10
9
 
11
10
  // Delete chat-related records
@@ -95,7 +94,7 @@ export const seedDatabase = async () => {
95
94
 
96
95
  // 3. Create Students (realistic names)
97
96
  const students = await Promise.all([
98
- createUser('alex.martinez@student.rverside.eidu', 'student123', 'alex.martinez'),
97
+ createUser('alex.martinez@student.riverside.edu', 'student123', 'alex.martinez'),
99
98
  createUser('sophia.williams@student.riverside.edu', 'student123', 'sophia.williams'),
100
99
  createUser('james.brown@student.riverside.edu', 'student123', 'james.brown'),
101
100
  createUser('olivia.taylor@student.riverside.edu', 'student123', 'olivia.taylor'),
@@ -26,24 +26,7 @@ const colors = {
26
26
  magenta: '\x1b[35m',
27
27
  cyan: '\x1b[36m',
28
28
  white: '\x1b[37m',
29
- gray: '\x1b[90m',
30
- // Background colors
31
- bgRed: '\x1b[41m',
32
- bgGreen: '\x1b[42m',
33
- bgYellow: '\x1b[43m',
34
- bgBlue: '\x1b[44m',
35
- bgMagenta: '\x1b[45m',
36
- bgCyan: '\x1b[46m',
37
- bgWhite: '\x1b[47m',
38
- bgGray: '\x1b[100m',
39
- // Bright background colors
40
- bgBrightRed: '\x1b[101m',
41
- bgBrightGreen: '\x1b[102m',
42
- bgBrightYellow: '\x1b[103m',
43
- bgBrightBlue: '\x1b[104m',
44
- bgBrightMagenta: '\x1b[105m',
45
- bgBrightCyan: '\x1b[106m',
46
- bgBrightWhite: '\x1b[107m'
29
+ gray: '\x1b[90m'
47
30
  };
48
31
 
49
32
  class Logger {
@@ -51,7 +34,6 @@ class Logger {
51
34
  private isDevelopment: boolean;
52
35
  private mode: LogMode;
53
36
  private levelColors: Record<LogLevel, string>;
54
- private levelBgColors: Record<LogLevel, string>;
55
37
  private levelEmojis: Record<LogLevel, string>;
56
38
 
57
39
  private constructor() {
@@ -69,13 +51,6 @@ class Logger {
69
51
  [LogLevel.DEBUG]: colors.magenta
70
52
  };
71
53
 
72
- this.levelBgColors = {
73
- [LogLevel.INFO]: colors.bgBlue,
74
- [LogLevel.WARN]: colors.bgYellow,
75
- [LogLevel.ERROR]: colors.bgRed,
76
- [LogLevel.DEBUG]: colors.bgMagenta
77
- };
78
-
79
54
  this.levelEmojis = {
80
55
  [LogLevel.INFO]: 'ℹ️',
81
56
  [LogLevel.WARN]: '⚠️',
@@ -120,12 +95,10 @@ class Logger {
120
95
  private formatMessage(logMessage: LogMessage): string {
121
96
  const { level, message, timestamp, context } = logMessage;
122
97
  const color = this.levelColors[level];
123
- const bgColor = this.levelBgColors[level];
124
98
  const emoji = this.levelEmojis[level];
125
99
 
126
100
  const timestampStr = colors.gray + `[${timestamp}]` + colors.reset;
127
- // Use background color for level badge like Vitest
128
- const levelStr = colors.white + bgColor + ` ${level.toUpperCase()} ` + colors.reset;
101
+ const levelStr = color + `[${level.toUpperCase()}]` + colors.reset;
129
102
  const emojiStr = emoji + ' ';
130
103
  const messageStr = colors.bright + message + colors.reset;
131
104
 
package/tests/setup.ts CHANGED
@@ -5,7 +5,6 @@ import { logger } from '../src/utils/logger';
5
5
  import { appRouter } from '../src/routers/_app';
6
6
  import { createTRPCContext } from '../src/trpc';
7
7
  import { Session } from '@prisma/client';
8
- import { clearDatabase } from '../src/seedDatabase';
9
8
 
10
9
  const getCaller = async (token: string) => {
11
10
  const ctx = await createTRPCContext({
@@ -20,8 +19,15 @@ const getCaller = async (token: string) => {
20
19
 
21
20
  // Before the entire test suite runs
22
21
  beforeAll(async () => {
23
-
24
- await clearDatabase();
22
+ // // Run migrations so the test DB has the latest schema
23
+ // try {
24
+ // logger.info('Setting up test database');
25
+ // execSync('rm -f prisma/test.db');
26
+ // execSync('npx prisma db push --force-reset --schema=prisma/schema.prisma');
27
+
28
+ // } catch (error) {
29
+ // logger.error('Error initializing test database');
30
+ // }
25
31
 
26
32
  logger.info('Getting caller');
27
33