@studious-lms/server 1.1.22 → 1.1.23

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,6 @@ export const assignmentRouter = createTRPCRouter({
848
859
  select: {
849
860
  id: true,
850
861
  username: true,
851
- profile: true,
852
862
  },
853
863
  },
854
864
  assignment: {
@@ -1270,31 +1280,13 @@ export const assignmentRouter = createTRPCRouter({
1270
1280
  });
1271
1281
  }
1272
1282
 
1273
- let uploadedFiles: UploadedFile[] = [];
1283
+ // NOTE: Teacher annotation files are now handled via direct upload endpoints
1284
+ // Use getAnnotationUploadUrls and confirmAnnotationUpload endpoints instead
1285
+ // The newAttachments field is deprecated for annotations
1274
1286
  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
- }
1287
+ throw new TRPCError({
1288
+ code: "BAD_REQUEST",
1289
+ message: "Direct file upload is deprecated. Use getAnnotationUploadUrls endpoint instead.",
1298
1290
  });
1299
1291
  }
1300
1292
 
@@ -2054,6 +2046,109 @@ export const assignmentRouter = createTRPCRouter({
2054
2046
  };
2055
2047
  }),
2056
2048
 
2049
+ getAnnotationUploadUrls: protectedTeacherProcedure
2050
+ .input(getAnnotationUploadUrlsSchema)
2051
+ .mutation(async ({ ctx, input }) => {
2052
+ const { submissionId, classId, files } = input;
2053
+
2054
+ if (!ctx.user) {
2055
+ throw new TRPCError({
2056
+ code: "UNAUTHORIZED",
2057
+ message: "You must be logged in to upload files",
2058
+ });
2059
+ }
2060
+
2061
+ // Verify submission exists and user is a teacher of the class
2062
+ const submission = await prisma.submission.findFirst({
2063
+ where: {
2064
+ id: submissionId,
2065
+ assignment: {
2066
+ classId: classId,
2067
+ class: {
2068
+ teachers: {
2069
+ some: {
2070
+ id: ctx.user.id,
2071
+ },
2072
+ },
2073
+ },
2074
+ },
2075
+ },
2076
+ });
2077
+
2078
+ if (!submission) {
2079
+ throw new TRPCError({
2080
+ code: "NOT_FOUND",
2081
+ message: "Submission not found or you are not a teacher of this class",
2082
+ });
2083
+ }
2084
+
2085
+ // Create direct upload files for annotations
2086
+ // Note: We pass submissionId as the 5th parameter, but these are annotations not submission files
2087
+ // We need to store them separately, so we'll use a different approach
2088
+ const directUploadFiles = await createDirectUploadFiles(
2089
+ files,
2090
+ ctx.user.id,
2091
+ undefined, // No specific directory
2092
+ undefined, // No assignment ID
2093
+ undefined // Don't link to submission yet (will be linked in confirmAnnotationUpload)
2094
+ );
2095
+
2096
+ // Store the submissionId in the file record so we can link it to annotations later
2097
+ await Promise.all(
2098
+ directUploadFiles.map(file =>
2099
+ prisma.file.update({
2100
+ where: { id: file.id },
2101
+ data: {
2102
+ annotationId: submissionId, // Store as annotation
2103
+ }
2104
+ })
2105
+ )
2106
+ );
2107
+
2108
+ return {
2109
+ success: true,
2110
+ uploadFiles: directUploadFiles,
2111
+ };
2112
+ }),
2113
+
2114
+ confirmAnnotationUpload: protectedTeacherProcedure
2115
+ .input(confirmAnnotationUploadSchema)
2116
+ .mutation(async ({ ctx, input }) => {
2117
+ const { fileId, uploadSuccess, errorMessage } = input;
2118
+
2119
+ if (!ctx.user) {
2120
+ throw new TRPCError({
2121
+ code: "UNAUTHORIZED",
2122
+ message: "You must be logged in",
2123
+ });
2124
+ }
2125
+
2126
+ // Verify file belongs to user and is an annotation file
2127
+ const file = await prisma.file.findFirst({
2128
+ where: {
2129
+ id: fileId,
2130
+ userId: ctx.user.id,
2131
+ annotationId: {
2132
+ not: null,
2133
+ },
2134
+ },
2135
+ });
2136
+
2137
+ if (!file) {
2138
+ throw new TRPCError({
2139
+ code: "NOT_FOUND",
2140
+ message: "File not found or you don't have permission",
2141
+ });
2142
+ }
2143
+
2144
+ await confirmDirectUpload(fileId, uploadSuccess, errorMessage);
2145
+
2146
+ return {
2147
+ success: true,
2148
+ message: uploadSuccess ? "Annotation upload confirmed successfully" : "Annotation upload failed",
2149
+ };
2150
+ }),
2151
+
2057
2152
  updateUploadProgress: protectedProcedure
2058
2153
  .input(updateUploadProgressSchema)
2059
2154
  .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