@studious-lms/server 1.1.21 → 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.
- package/API_SPECIFICATION.md +146 -10
- package/BASE64_REMOVAL_SUMMARY.md +11 -0
- package/dist/lib/fileUpload.d.ts.map +1 -1
- package/dist/lib/fileUpload.js +11 -68
- package/dist/lib/googleCloudStorage.d.ts +0 -7
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +0 -19
- package/dist/routers/_app.d.ts +114 -48
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +31 -24
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +99 -25
- package/dist/routers/folder.d.ts +27 -0
- package/dist/routers/folder.d.ts.map +1 -1
- package/dist/routers/folder.js +93 -0
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +1 -2
- package/dist/utils/logger.d.ts +0 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +2 -27
- package/package.json +2 -2
- package/src/lib/fileUpload.ts +12 -70
- package/src/lib/googleCloudStorage.ts +0 -19
- package/src/routers/assignment.ts +121 -26
- package/src/routers/folder.ts +109 -0
- package/src/seedDatabase.ts +1 -2
- package/src/utils/logger.ts +2 -29
- package/tests/setup.ts +9 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
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 }) => {
|
package/src/routers/folder.ts
CHANGED
|
@@ -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
|
});
|
package/src/seedDatabase.ts
CHANGED
|
@@ -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.
|
|
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'),
|
package/src/utils/logger.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|