@studious-lms/server 1.4.5 → 1.4.6
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/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +8 -3
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/models/comment.d.ts +163 -0
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/comment.js +145 -3
- package/dist/models/comment.js.map +1 -1
- package/dist/models/worksheet.d.ts +2 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +4 -3
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/routers/_app.d.ts +562 -8
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/announcement.d.ts +8 -0
- package/dist/routers/announcement.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +230 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +207 -2
- package/dist/routers/assignment.js.map +1 -1
- package/dist/routers/comment.d.ts +37 -0
- package/dist/routers/comment.d.ts.map +1 -1
- package/dist/routers/comment.js +14 -5
- package/dist/routers/comment.js.map +1 -1
- package/dist/routers/worksheet.d.ts +2 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/services/comment.d.ts +28 -2
- package/dist/services/comment.d.ts.map +1 -1
- package/dist/services/comment.js +97 -19
- package/dist/services/comment.js.map +1 -1
- package/dist/services/worksheet.d.ts +2 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +60 -0
- package/prisma/schema.prisma +9 -0
- package/src/lib/config/cors.ts +8 -1
- package/src/models/comment.ts +148 -1
- package/src/pipelines/aiLabChat.ts +2 -1
- package/src/routers/assignment.ts +224 -1
- package/src/routers/comment.ts +19 -2
- package/src/services/comment.ts +113 -18
- package/tests/lib/cors.test.ts +13 -1
- package/tests/routers/assignment.test.ts +68 -2
- package/tests/routers/comment.test.ts +42 -1
- package/vercel.json +8 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comment.js","sources":["services/comment.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Comment service – get comments, replies, and reactions. Used for announcements and worksheets.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport {\n findCommentById,\n findRepliesByCommentId,\n createComment,\n findReactionByUserAndComment,\n upsertReaction,\n deleteReactionById,\n findCommentWithAnnouncement,\n getReactionCountsByCommentId,\n} from \"../models/comment.js\";\n\n/** Get a comment by ID. */\nexport async function getComment(id: string) {\n const comment = await findCommentById(id);\n if (!comment) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Comment not found\",\n });\n }\n return comment;\n}\n\n/** Get all replies to a comment. */\nexport async function getReplies(commentId: string) {\n return findRepliesByCommentId(commentId);\n}\n\n/** Create a reply to an existing comment. */\nexport async function replyToComment(\n userId: string,\n parentCommentId: string,\n content: string\n) {\n return createComment({\n parentCommentId,\n content,\n authorId: userId,\n });\n}\n\n/** Add or update a reaction (emoji) on a comment. */\nexport async function addReaction(\n userId: string,\n commentId: string,\n type: string\n) {\n const comment = await findCommentById(commentId);\n if (!comment) {\n throw new TRPCError({\n code: \"INTERNAL_SERVER_ERROR\",\n message: \"Unexpected error\",\n });\n }\n\n const reaction = await upsertReaction({ userId, commentId, type });\n return { reaction };\n}\n\n/** Remove user's reaction from a comment. */\nexport async function removeReaction(userId: string, commentId: string) {\n const reaction = await findReactionByUserAndComment(userId, commentId);\n if (!reaction) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Reaction not found\",\n });\n }\n await deleteReactionById(reaction.id);\n return { success: true };\n}\n\nexport async function getReactions(userId: string, commentId: string) {\n const comment = await findCommentWithAnnouncement(commentId);\n if (!comment) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Comment not found\",\n });\n }\n\n const reactionCounts = await getReactionCountsByCommentId(commentId);\n const userReaction = await findReactionByUserAndComment(userId, commentId);\n\n const counts = {\n THUMBSUP: 0,\n CELEBRATE: 0,\n CARE: 0,\n HEART: 0,\n IDEA: 0,\n HAPPY: 0,\n };\n reactionCounts.forEach((item) => {\n counts[item.type as keyof typeof counts] = item._count.type;\n });\n\n return {\n counts,\n userReaction: userReaction?.type || null,\n total: reactionCounts.reduce((sum, item) => sum + item._count.type, 0),\n };\n}\n"],"names":[],"mappings":"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","debug_id":"381af507-0513-5413-ae9b-6bcb211761ee"}
|
|
1
|
+
{"version":3,"file":"comment.js","sources":["services/comment.ts"],"sourceRoot":"/","sourcesContent":["/**\n * Comment service – get comments, replies, and reactions.\n */\nimport { TRPCError } from \"@trpc/server\";\nimport { prisma } from \"../lib/prisma.js\";\nimport {\n findCommentById,\n findRepliesByCommentId,\n createComment,\n findReactionByUserAndComment,\n upsertReaction,\n deleteReactionById,\n findCommentWithContext,\n getReactionCountsByCommentId,\n} from \"../models/comment.js\";\n\nfunction getCommentClass(comment: any) {\n return (\n comment.announcement?.class ??\n comment.assignment?.class ??\n comment.submission?.assignment?.class ??\n comment.studentQuestionProgress?.studentWorksheetResponse?.submission?.assignment?.class ??\n comment.parentComment?.announcement?.class ??\n comment.parentComment?.assignment?.class ??\n comment.parentComment?.submission?.assignment?.class ??\n comment.parentComment?.studentQuestionProgress?.studentWorksheetResponse?.submission?.assignment?.class ??\n null\n );\n}\n\nfunction getPrivateStudentId(comment: any) {\n return (\n comment.submission?.studentId ??\n comment.studentQuestionProgress?.studentWorksheetResponse?.submission?.studentId ??\n comment.studentQuestionProgress?.studentId ??\n comment.parentComment?.submission?.studentId ??\n comment.parentComment?.studentQuestionProgress?.studentWorksheetResponse?.submission?.studentId ??\n comment.parentComment?.studentQuestionProgress?.studentId ??\n null\n );\n}\n\nfunction isClassTeacher(comment: any, userId: string) {\n return getCommentClass(comment)?.teachers?.some((teacher: { id: string }) => teacher.id === userId) ?? false;\n}\n\nfunction isClassMember(comment: any, userId: string) {\n const classData = getCommentClass(comment);\n return (\n classData?.teachers?.some((teacher: { id: string }) => teacher.id === userId) ||\n classData?.students?.some((student: { id: string }) => student.id === userId) ||\n false\n );\n}\n\nfunction canReadComment(comment: any, userId: string) {\n const privateStudentId = getPrivateStudentId(comment);\n if (privateStudentId) {\n return privateStudentId === userId || isClassTeacher(comment, userId);\n }\n return isClassMember(comment, userId);\n}\n\nfunction canDeleteComment(comment: any, userId: string) {\n return comment.authorId === userId || isClassTeacher(comment, userId);\n}\n\nasync function requireCommentAccess(commentId: string, userId: string) {\n const comment = await findCommentWithContext(commentId);\n if (!comment) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Comment not found\",\n });\n }\n if (!canReadComment(comment, userId)) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"You do not have access to this comment\",\n });\n }\n return comment;\n}\n\n/** Get a comment by ID. */\nexport async function getComment(userId: string, id: string) {\n await requireCommentAccess(id, userId);\n const comment = await findCommentById(id);\n if (!comment) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Comment not found\",\n });\n }\n return comment;\n}\n\n/** Get all replies to a comment. */\nexport async function getReplies(userId: string, commentId: string) {\n await requireCommentAccess(commentId, userId);\n return findRepliesByCommentId(commentId);\n}\n\n/** Create a reply to an existing comment. */\nexport async function replyToComment(\n userId: string,\n parentCommentId: string,\n content: string\n) {\n const parentComment = await requireCommentAccess(parentCommentId, userId);\n return createComment({\n parentCommentId,\n content,\n authorId: userId,\n announcementId: parentComment.announcementId,\n assignmentId: parentComment.assignmentId,\n submissionId: parentComment.submissionId,\n studentQuestionProgressId: parentComment.studentQuestionProgressId,\n });\n}\n\n/** Update a comment authored by the user. */\nexport async function updateComment(userId: string, id: string, content: string) {\n const comment = await requireCommentAccess(id, userId);\n if (comment.authorId !== userId) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only the comment author can update this comment\",\n });\n }\n\n return prisma.comment.update({\n where: { id },\n data: { content },\n });\n}\n\n/** Delete a comment authored by the user or moderated by a class teacher. */\nexport async function deleteComment(userId: string, id: string) {\n const comment = await requireCommentAccess(id, userId);\n if (!canDeleteComment(comment, userId)) {\n throw new TRPCError({\n code: \"FORBIDDEN\",\n message: \"Only the comment author or class teachers can delete comments\",\n });\n }\n\n await prisma.comment.delete({ where: { id } });\n return { success: true };\n}\n\n/** Add or update a reaction (emoji) on a comment. */\nexport async function addReaction(\n userId: string,\n commentId: string,\n type: string\n) {\n await requireCommentAccess(commentId, userId);\n\n const reaction = await upsertReaction({ userId, commentId, type });\n return { reaction };\n}\n\n/** Remove user's reaction from a comment. */\nexport async function removeReaction(userId: string, commentId: string) {\n await requireCommentAccess(commentId, userId);\n const reaction = await findReactionByUserAndComment(userId, commentId);\n if (!reaction) {\n throw new TRPCError({\n code: \"NOT_FOUND\",\n message: \"Reaction not found\",\n });\n }\n await deleteReactionById(reaction.id);\n return { success: true };\n}\n\nexport async function getReactions(userId: string, commentId: string) {\n await requireCommentAccess(commentId, userId);\n\n const reactionCounts = await getReactionCountsByCommentId(commentId);\n const userReaction = await findReactionByUserAndComment(userId, commentId);\n\n const counts = {\n THUMBSUP: 0,\n CELEBRATE: 0,\n CARE: 0,\n HEART: 0,\n IDEA: 0,\n HAPPY: 0,\n };\n reactionCounts.forEach((item) => {\n counts[item.type as keyof typeof counts] = item._count.type;\n });\n\n return {\n counts,\n userReaction: userReaction?.type || null,\n total: reactionCounts.reduce((sum, item) => sum + item._count.type, 0),\n };\n}\n"],"names":[],"mappings":"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","debug_id":"2f340cd4-2a5e-59d9-8448-05f1acff840a"}
|
|
@@ -249,6 +249,8 @@ export declare function gradeAnswer(questionId: string, studentWorksheetResponse
|
|
|
249
249
|
export declare function addCommentToResponse(responseId: string, comment: string, authorId: string): Promise<{
|
|
250
250
|
status: import(".prisma/client").$Enums.GenerationStatus | null;
|
|
251
251
|
id: string;
|
|
252
|
+
assignmentId: string | null;
|
|
253
|
+
submissionId: string | null;
|
|
252
254
|
announcementId: string | null;
|
|
253
255
|
createdAt: Date;
|
|
254
256
|
modifiedAt: Date | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worksheet.d.ts","sourceRoot":"/","sources":["services/worksheet.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"worksheet.d.ts","sourceRoot":"/","sources":["services/worksheet.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
ALTER TABLE "public"."Comment" ADD COLUMN "assignmentId" TEXT;
|
|
2
|
+
ALTER TABLE "public"."Comment" ADD COLUMN "submissionId" TEXT;
|
|
3
|
+
|
|
4
|
+
CREATE INDEX "Comment_assignmentId_idx" ON "public"."Comment"("assignmentId");
|
|
5
|
+
CREATE INDEX "Comment_submissionId_idx" ON "public"."Comment"("submissionId");
|
|
6
|
+
|
|
7
|
+
ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_assignmentId_fkey" FOREIGN KEY ("assignmentId") REFERENCES "public"."Assignment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
8
|
+
ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "public"."Submission"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
9
|
+
|
|
10
|
+
WITH RECURSIVE "CommentOwnerContext" AS (
|
|
11
|
+
SELECT
|
|
12
|
+
"id",
|
|
13
|
+
"parentCommentId",
|
|
14
|
+
"announcementId",
|
|
15
|
+
"assignmentId",
|
|
16
|
+
"submissionId",
|
|
17
|
+
"studentQuestionProgressId"
|
|
18
|
+
FROM "public"."Comment"
|
|
19
|
+
WHERE "parentCommentId" IS NULL
|
|
20
|
+
|
|
21
|
+
UNION ALL
|
|
22
|
+
|
|
23
|
+
SELECT
|
|
24
|
+
child."id",
|
|
25
|
+
child."parentCommentId",
|
|
26
|
+
COALESCE(child."announcementId", parent."announcementId"),
|
|
27
|
+
COALESCE(child."assignmentId", parent."assignmentId"),
|
|
28
|
+
COALESCE(child."submissionId", parent."submissionId"),
|
|
29
|
+
COALESCE(child."studentQuestionProgressId", parent."studentQuestionProgressId")
|
|
30
|
+
FROM "public"."Comment" child
|
|
31
|
+
INNER JOIN "CommentOwnerContext" parent ON child."parentCommentId" = parent."id"
|
|
32
|
+
)
|
|
33
|
+
UPDATE "public"."Comment" c
|
|
34
|
+
SET
|
|
35
|
+
"announcementId" = owner."announcementId",
|
|
36
|
+
"assignmentId" = owner."assignmentId",
|
|
37
|
+
"submissionId" = owner."submissionId",
|
|
38
|
+
"studentQuestionProgressId" = owner."studentQuestionProgressId"
|
|
39
|
+
FROM "CommentOwnerContext" owner
|
|
40
|
+
WHERE c."id" = owner."id"
|
|
41
|
+
AND c."parentCommentId" IS NOT NULL
|
|
42
|
+
AND (
|
|
43
|
+
(CASE WHEN c."assignmentId" IS NOT NULL THEN 1 ELSE 0 END) +
|
|
44
|
+
(CASE WHEN c."submissionId" IS NOT NULL THEN 1 ELSE 0 END) +
|
|
45
|
+
(CASE WHEN c."announcementId" IS NOT NULL THEN 1 ELSE 0 END) +
|
|
46
|
+
(CASE WHEN c."studentQuestionProgressId" IS NOT NULL THEN 1 ELSE 0 END)
|
|
47
|
+
) = 0
|
|
48
|
+
AND (
|
|
49
|
+
(CASE WHEN owner."assignmentId" IS NOT NULL THEN 1 ELSE 0 END) +
|
|
50
|
+
(CASE WHEN owner."submissionId" IS NOT NULL THEN 1 ELSE 0 END) +
|
|
51
|
+
(CASE WHEN owner."announcementId" IS NOT NULL THEN 1 ELSE 0 END) +
|
|
52
|
+
(CASE WHEN owner."studentQuestionProgressId" IS NOT NULL THEN 1 ELSE 0 END)
|
|
53
|
+
) = 1;
|
|
54
|
+
|
|
55
|
+
ALTER TABLE "public"."Comment" ADD CONSTRAINT "Comment_single_owner_check" CHECK (
|
|
56
|
+
(CASE WHEN "assignmentId" IS NOT NULL THEN 1 ELSE 0 END) +
|
|
57
|
+
(CASE WHEN "submissionId" IS NOT NULL THEN 1 ELSE 0 END) +
|
|
58
|
+
(CASE WHEN "announcementId" IS NOT NULL THEN 1 ELSE 0 END) +
|
|
59
|
+
(CASE WHEN "studentQuestionProgressId" IS NOT NULL THEN 1 ELSE 0 END) = 1
|
|
60
|
+
);
|
package/prisma/schema.prisma
CHANGED
|
@@ -282,6 +282,7 @@ model Assignment {
|
|
|
282
282
|
aiPolicyLevel Int @default(0)
|
|
283
283
|
|
|
284
284
|
submissions Submission[]
|
|
285
|
+
comments Comment[] @relation("AssignmentComments")
|
|
285
286
|
assignedTo User[] @relation("SpecificAssignmentAssignedTo")
|
|
286
287
|
section Section? @relation(fields: [sectionId], references: [id], onDelete: Cascade)
|
|
287
288
|
sectionId String?
|
|
@@ -325,6 +326,10 @@ model Comment {
|
|
|
325
326
|
authorId String
|
|
326
327
|
announcement Announcement? @relation(fields: [announcementId], references: [id], onDelete: Cascade)
|
|
327
328
|
announcementId String?
|
|
329
|
+
assignment Assignment? @relation("AssignmentComments", fields: [assignmentId], references: [id], onDelete: Cascade)
|
|
330
|
+
assignmentId String?
|
|
331
|
+
submission Submission? @relation("SubmissionComments", fields: [submissionId], references: [id], onDelete: Cascade)
|
|
332
|
+
submissionId String?
|
|
328
333
|
parentComment Comment? @relation("CommentReplies", fields: [parentCommentId], references: [id], onDelete: Cascade)
|
|
329
334
|
parentCommentId String?
|
|
330
335
|
status GenerationStatus?
|
|
@@ -334,6 +339,9 @@ model Comment {
|
|
|
334
339
|
modifiedAt DateTime? @updatedAt
|
|
335
340
|
studentQuestionProgress StudentQuestionProgress? @relation("StudentQuestionProgressComments", fields: [studentQuestionProgressId], references: [id], onDelete: Cascade)
|
|
336
341
|
studentQuestionProgressId String?
|
|
342
|
+
|
|
343
|
+
@@index([assignmentId])
|
|
344
|
+
@@index([submissionId])
|
|
337
345
|
}
|
|
338
346
|
|
|
339
347
|
model Reaction {
|
|
@@ -366,6 +374,7 @@ model Submission {
|
|
|
366
374
|
|
|
367
375
|
attachments File[] @relation("SubmissionFile")
|
|
368
376
|
annotations File[] @relation("SubmissionAnnotations")
|
|
377
|
+
comments Comment[] @relation("SubmissionComments")
|
|
369
378
|
worksheetResponses StudentWorksheetResponse[]
|
|
370
379
|
extendedResponse String?
|
|
371
380
|
gradeReceived Int?
|
package/src/lib/config/cors.ts
CHANGED
|
@@ -18,6 +18,10 @@ const nonProductionOrigins = [
|
|
|
18
18
|
'http://127.0.0.1:3000',
|
|
19
19
|
'http://127.0.0.1:3001',
|
|
20
20
|
];
|
|
21
|
+
|
|
22
|
+
/** Any preview / prod deployment on *.vercel.app (HTTPS only). */
|
|
23
|
+
const vercelAppOriginPattern = /^https:\/\/[a-z0-9][a-z0-9.-]*\.vercel\.app$/i;
|
|
24
|
+
|
|
21
25
|
const MAX_PATTERN_LENGTH = 200;
|
|
22
26
|
// This heuristic only targets common nested-quantifier cases like `(a+)+`.
|
|
23
27
|
// It does not catch every risky construct (for example `.*.*` or deep alternation nesting),
|
|
@@ -77,7 +81,10 @@ export const createCorsOriginMatcher = (env: CorsEnv) => {
|
|
|
77
81
|
...parseCorsOrigins(env.CORS_ALLOWED_ORIGINS),
|
|
78
82
|
].filter(isDefined)));
|
|
79
83
|
|
|
80
|
-
const allowedOriginPatterns =
|
|
84
|
+
const allowedOriginPatterns = [
|
|
85
|
+
vercelAppOriginPattern,
|
|
86
|
+
...parseCorsOriginPatterns(env.CORS_ALLOWED_ORIGIN_PATTERNS),
|
|
87
|
+
];
|
|
81
88
|
|
|
82
89
|
const isAllowedOrigin = (origin?: string) => {
|
|
83
90
|
const corsOrigin = origin ? toCorsOrigin(origin) : undefined;
|
package/src/models/comment.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Comment model – comments, replies, and reactions
|
|
2
|
+
* Comment model – comments, replies, and reactions.
|
|
3
3
|
*/
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
5
|
import type { ReactionType } from "@prisma/client";
|
|
@@ -7,6 +7,10 @@ import type { ReactionType } from "@prisma/client";
|
|
|
7
7
|
export const commentSelect = {
|
|
8
8
|
id: true,
|
|
9
9
|
content: true,
|
|
10
|
+
announcementId: true,
|
|
11
|
+
assignmentId: true,
|
|
12
|
+
submissionId: true,
|
|
13
|
+
parentCommentId: true,
|
|
10
14
|
createdAt: true,
|
|
11
15
|
modifiedAt: true,
|
|
12
16
|
author: {
|
|
@@ -77,6 +81,10 @@ export function createComment(data: {
|
|
|
77
81
|
parentCommentId: string;
|
|
78
82
|
content: string;
|
|
79
83
|
authorId: string;
|
|
84
|
+
announcementId?: string | null;
|
|
85
|
+
assignmentId?: string | null;
|
|
86
|
+
submissionId?: string | null;
|
|
87
|
+
studentQuestionProgressId?: string | null;
|
|
80
88
|
}) {
|
|
81
89
|
return prisma.comment.create({
|
|
82
90
|
data,
|
|
@@ -142,6 +150,145 @@ export function findCommentWithAnnouncement(commentId: string) {
|
|
|
142
150
|
});
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
/** @returns Comment with the owning class context for authorization. */
|
|
154
|
+
export function findCommentWithContext(commentId: string) {
|
|
155
|
+
return prisma.comment.findUnique({
|
|
156
|
+
where: { id: commentId },
|
|
157
|
+
include: {
|
|
158
|
+
announcement: {
|
|
159
|
+
select: {
|
|
160
|
+
class: {
|
|
161
|
+
select: {
|
|
162
|
+
id: true,
|
|
163
|
+
teachers: { select: { id: true } },
|
|
164
|
+
students: { select: { id: true } },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
assignment: {
|
|
170
|
+
select: {
|
|
171
|
+
class: {
|
|
172
|
+
select: {
|
|
173
|
+
id: true,
|
|
174
|
+
teachers: { select: { id: true } },
|
|
175
|
+
students: { select: { id: true } },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
submission: {
|
|
181
|
+
select: {
|
|
182
|
+
studentId: true,
|
|
183
|
+
assignment: {
|
|
184
|
+
select: {
|
|
185
|
+
class: {
|
|
186
|
+
select: {
|
|
187
|
+
id: true,
|
|
188
|
+
teachers: { select: { id: true } },
|
|
189
|
+
students: { select: { id: true } },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
studentQuestionProgress: {
|
|
197
|
+
select: {
|
|
198
|
+
studentId: true,
|
|
199
|
+
studentWorksheetResponse: {
|
|
200
|
+
select: {
|
|
201
|
+
submission: {
|
|
202
|
+
select: {
|
|
203
|
+
studentId: true,
|
|
204
|
+
assignment: {
|
|
205
|
+
select: {
|
|
206
|
+
class: {
|
|
207
|
+
select: {
|
|
208
|
+
id: true,
|
|
209
|
+
teachers: { select: { id: true } },
|
|
210
|
+
students: { select: { id: true } },
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
parentComment: {
|
|
222
|
+
include: {
|
|
223
|
+
announcement: {
|
|
224
|
+
select: {
|
|
225
|
+
class: {
|
|
226
|
+
select: {
|
|
227
|
+
id: true,
|
|
228
|
+
teachers: { select: { id: true } },
|
|
229
|
+
students: { select: { id: true } },
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
assignment: {
|
|
235
|
+
select: {
|
|
236
|
+
class: {
|
|
237
|
+
select: {
|
|
238
|
+
id: true,
|
|
239
|
+
teachers: { select: { id: true } },
|
|
240
|
+
students: { select: { id: true } },
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
submission: {
|
|
246
|
+
select: {
|
|
247
|
+
studentId: true,
|
|
248
|
+
assignment: {
|
|
249
|
+
select: {
|
|
250
|
+
class: {
|
|
251
|
+
select: {
|
|
252
|
+
id: true,
|
|
253
|
+
teachers: { select: { id: true } },
|
|
254
|
+
students: { select: { id: true } },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
studentQuestionProgress: {
|
|
262
|
+
select: {
|
|
263
|
+
studentId: true,
|
|
264
|
+
studentWorksheetResponse: {
|
|
265
|
+
select: {
|
|
266
|
+
submission: {
|
|
267
|
+
select: {
|
|
268
|
+
studentId: true,
|
|
269
|
+
assignment: {
|
|
270
|
+
select: {
|
|
271
|
+
class: {
|
|
272
|
+
select: {
|
|
273
|
+
id: true,
|
|
274
|
+
teachers: { select: { id: true } },
|
|
275
|
+
students: { select: { id: true } },
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
145
292
|
/** @returns Reaction counts by type for a comment. */
|
|
146
293
|
export function getReactionCountsByCommentId(commentId: string) {
|
|
147
294
|
return prisma.reaction.groupBy({
|
|
@@ -16,7 +16,8 @@ import { DocumentBlock } from "../lib/jsonStyles.js";
|
|
|
16
16
|
import { type LabChatResponse, labChatResponseSchema } from "./aiLabChatContract.js";
|
|
17
17
|
import { buildLabChatResponseMessages } from "./labChatPrompt.js";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/** Cap model wait time; leave headroom after inference for PDF/GCS/DB within Vercel maxDuration (500s). */
|
|
20
|
+
const LAB_CHAT_RESPONSE_TIMEOUT_MS = 400_000;
|
|
20
21
|
|
|
21
22
|
const withTimeout = async <T>(
|
|
22
23
|
task: Promise<T>,
|
|
@@ -172,6 +172,66 @@ const updateUploadProgressSchema = z.object({
|
|
|
172
172
|
progress: z.number().min(0).max(100),
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
+
const assignmentCommentSchema = z.object({
|
|
176
|
+
assignmentId: z.string(),
|
|
177
|
+
classId: z.string(),
|
|
178
|
+
content: z.string().min(1, "Comment cannot be empty"),
|
|
179
|
+
parentCommentId: z.string().optional(),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const assignmentCommentsSchema = z.object({
|
|
183
|
+
assignmentId: z.string(),
|
|
184
|
+
classId: z.string(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const submissionCommentSchema = z.object({
|
|
188
|
+
assignmentId: z.string(),
|
|
189
|
+
classId: z.string(),
|
|
190
|
+
submissionId: z.string(),
|
|
191
|
+
content: z.string().min(1, "Comment cannot be empty"),
|
|
192
|
+
parentCommentId: z.string().optional(),
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const submissionCommentsSchema = z.object({
|
|
196
|
+
assignmentId: z.string(),
|
|
197
|
+
classId: z.string(),
|
|
198
|
+
submissionId: z.string(),
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const commentInclude = {
|
|
202
|
+
author: {
|
|
203
|
+
select: {
|
|
204
|
+
id: true,
|
|
205
|
+
username: true,
|
|
206
|
+
profile: {
|
|
207
|
+
select: {
|
|
208
|
+
displayName: true,
|
|
209
|
+
profilePicture: true,
|
|
210
|
+
profilePictureThumbnail: true,
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
replies: {
|
|
216
|
+
include: {
|
|
217
|
+
author: {
|
|
218
|
+
select: {
|
|
219
|
+
id: true,
|
|
220
|
+
username: true,
|
|
221
|
+
profile: {
|
|
222
|
+
select: {
|
|
223
|
+
displayName: true,
|
|
224
|
+
profilePicture: true,
|
|
225
|
+
profilePictureThumbnail: true,
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
orderBy: { createdAt: "asc" as const },
|
|
232
|
+
},
|
|
233
|
+
} as const;
|
|
234
|
+
|
|
175
235
|
export const assignmentRouter = createTRPCRouter({
|
|
176
236
|
reorder: protectedTeacherProcedure
|
|
177
237
|
.input(z.object({
|
|
@@ -254,6 +314,170 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
254
314
|
.input(z.object({ assignmentId: z.string(), classId: z.string() }))
|
|
255
315
|
.query(({ ctx, input }) => getSubmissions(input.assignmentId, ctx.user!.id)),
|
|
256
316
|
|
|
317
|
+
getComments: protectedClassMemberProcedure
|
|
318
|
+
.input(assignmentCommentsSchema)
|
|
319
|
+
.query(async ({ input }) => {
|
|
320
|
+
const assignment = await prisma.assignment.findFirst({
|
|
321
|
+
where: { id: input.assignmentId, classId: input.classId },
|
|
322
|
+
select: { id: true },
|
|
323
|
+
});
|
|
324
|
+
if (!assignment) {
|
|
325
|
+
throw new TRPCError({
|
|
326
|
+
code: "NOT_FOUND",
|
|
327
|
+
message: "Assignment not found",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const comments = await prisma.comment.findMany({
|
|
332
|
+
where: {
|
|
333
|
+
assignmentId: input.assignmentId,
|
|
334
|
+
submissionId: null,
|
|
335
|
+
parentCommentId: null,
|
|
336
|
+
},
|
|
337
|
+
include: commentInclude,
|
|
338
|
+
orderBy: { createdAt: "asc" },
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return { comments };
|
|
342
|
+
}),
|
|
343
|
+
|
|
344
|
+
addComment: protectedClassMemberProcedure
|
|
345
|
+
.input(assignmentCommentSchema)
|
|
346
|
+
.mutation(async ({ ctx, input }) => {
|
|
347
|
+
const assignment = await prisma.assignment.findFirst({
|
|
348
|
+
where: { id: input.assignmentId, classId: input.classId },
|
|
349
|
+
select: { id: true },
|
|
350
|
+
});
|
|
351
|
+
if (!assignment) {
|
|
352
|
+
throw new TRPCError({
|
|
353
|
+
code: "NOT_FOUND",
|
|
354
|
+
message: "Assignment not found",
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (input.parentCommentId) {
|
|
359
|
+
const parentComment = await prisma.comment.findFirst({
|
|
360
|
+
where: {
|
|
361
|
+
id: input.parentCommentId,
|
|
362
|
+
assignmentId: input.assignmentId,
|
|
363
|
+
submissionId: null,
|
|
364
|
+
},
|
|
365
|
+
select: { id: true },
|
|
366
|
+
});
|
|
367
|
+
if (!parentComment) {
|
|
368
|
+
throw new TRPCError({
|
|
369
|
+
code: "NOT_FOUND",
|
|
370
|
+
message: "Parent comment not found",
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const comment = await prisma.comment.create({
|
|
376
|
+
data: {
|
|
377
|
+
content: input.content,
|
|
378
|
+
authorId: ctx.user!.id,
|
|
379
|
+
assignmentId: input.assignmentId,
|
|
380
|
+
...(input.parentCommentId && { parentCommentId: input.parentCommentId }),
|
|
381
|
+
},
|
|
382
|
+
include: commentInclude,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return { comment };
|
|
386
|
+
}),
|
|
387
|
+
|
|
388
|
+
getSubmissionComments: protectedClassMemberProcedure
|
|
389
|
+
.input(submissionCommentsSchema)
|
|
390
|
+
.query(async ({ ctx, input }) => {
|
|
391
|
+
const submission = await prisma.submission.findFirst({
|
|
392
|
+
where: {
|
|
393
|
+
id: input.submissionId,
|
|
394
|
+
assignmentId: input.assignmentId,
|
|
395
|
+
assignment: {
|
|
396
|
+
classId: input.classId,
|
|
397
|
+
class: {
|
|
398
|
+
OR: [
|
|
399
|
+
{ teachers: { some: { id: ctx.user!.id } } },
|
|
400
|
+
{ students: { some: { id: ctx.user!.id } } },
|
|
401
|
+
],
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
OR: [
|
|
405
|
+
{ studentId: ctx.user!.id },
|
|
406
|
+
{ assignment: { class: { teachers: { some: { id: ctx.user!.id } } } } },
|
|
407
|
+
],
|
|
408
|
+
},
|
|
409
|
+
select: { id: true },
|
|
410
|
+
});
|
|
411
|
+
if (!submission) {
|
|
412
|
+
throw new TRPCError({
|
|
413
|
+
code: "NOT_FOUND",
|
|
414
|
+
message: "Submission not found",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const comments = await prisma.comment.findMany({
|
|
419
|
+
where: {
|
|
420
|
+
submissionId: input.submissionId,
|
|
421
|
+
parentCommentId: null,
|
|
422
|
+
},
|
|
423
|
+
include: commentInclude,
|
|
424
|
+
orderBy: { createdAt: "asc" },
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return { comments };
|
|
428
|
+
}),
|
|
429
|
+
|
|
430
|
+
addSubmissionComment: protectedClassMemberProcedure
|
|
431
|
+
.input(submissionCommentSchema)
|
|
432
|
+
.mutation(async ({ ctx, input }) => {
|
|
433
|
+
const submission = await prisma.submission.findFirst({
|
|
434
|
+
where: {
|
|
435
|
+
id: input.submissionId,
|
|
436
|
+
assignmentId: input.assignmentId,
|
|
437
|
+
assignment: { classId: input.classId },
|
|
438
|
+
OR: [
|
|
439
|
+
{ studentId: ctx.user!.id },
|
|
440
|
+
{ assignment: { class: { teachers: { some: { id: ctx.user!.id } } } } },
|
|
441
|
+
],
|
|
442
|
+
},
|
|
443
|
+
select: { id: true },
|
|
444
|
+
});
|
|
445
|
+
if (!submission) {
|
|
446
|
+
throw new TRPCError({
|
|
447
|
+
code: "NOT_FOUND",
|
|
448
|
+
message: "Submission not found",
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (input.parentCommentId) {
|
|
453
|
+
const parentComment = await prisma.comment.findFirst({
|
|
454
|
+
where: {
|
|
455
|
+
id: input.parentCommentId,
|
|
456
|
+
submissionId: input.submissionId,
|
|
457
|
+
},
|
|
458
|
+
select: { id: true },
|
|
459
|
+
});
|
|
460
|
+
if (!parentComment) {
|
|
461
|
+
throw new TRPCError({
|
|
462
|
+
code: "NOT_FOUND",
|
|
463
|
+
message: "Parent comment not found",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const comment = await prisma.comment.create({
|
|
469
|
+
data: {
|
|
470
|
+
content: input.content,
|
|
471
|
+
authorId: ctx.user!.id,
|
|
472
|
+
submissionId: input.submissionId,
|
|
473
|
+
...(input.parentCommentId && { parentCommentId: input.parentCommentId }),
|
|
474
|
+
},
|
|
475
|
+
include: commentInclude,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
return { comment };
|
|
479
|
+
}),
|
|
480
|
+
|
|
257
481
|
updateSubmissionAsTeacher: protectedTeacherProcedure
|
|
258
482
|
.input(updateSubmissionSchema)
|
|
259
483
|
.mutation(({ ctx, input }) =>
|
|
@@ -660,4 +884,3 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
660
884
|
};
|
|
661
885
|
}),
|
|
662
886
|
});
|
|
663
|
-
|
package/src/routers/comment.ts
CHANGED
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
getComment,
|
|
6
6
|
getReplies,
|
|
7
7
|
replyToComment,
|
|
8
|
+
updateComment,
|
|
9
|
+
deleteComment,
|
|
8
10
|
addReaction,
|
|
9
11
|
removeReaction,
|
|
10
12
|
getReactions,
|
|
@@ -15,11 +17,11 @@ export { commentSelect } from "../models/comment.js";
|
|
|
15
17
|
export const commentRouter = createTRPCRouter({
|
|
16
18
|
get: protectedProcedure
|
|
17
19
|
.input(z.object({ id: z.string() }))
|
|
18
|
-
.query(({ input }) => getComment(input.id)),
|
|
20
|
+
.query(({ ctx, input }) => getComment(ctx.user!.id, input.id)),
|
|
19
21
|
|
|
20
22
|
getReplies: protectedProcedure
|
|
21
23
|
.input(z.object({ commentId: z.string() }))
|
|
22
|
-
.query(({ input }) => getReplies(input.commentId)),
|
|
24
|
+
.query(({ ctx, input }) => getReplies(ctx.user!.id, input.commentId)),
|
|
23
25
|
|
|
24
26
|
replyToComment: protectedProcedure
|
|
25
27
|
.input(
|
|
@@ -32,6 +34,21 @@ export const commentRouter = createTRPCRouter({
|
|
|
32
34
|
replyToComment(ctx.user!.id, input.parentCommentId, input.content)
|
|
33
35
|
),
|
|
34
36
|
|
|
37
|
+
update: protectedProcedure
|
|
38
|
+
.input(
|
|
39
|
+
z.object({
|
|
40
|
+
id: z.string(),
|
|
41
|
+
content: z.string().min(1, "Comment cannot be empty"),
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
.mutation(({ ctx, input }) =>
|
|
45
|
+
updateComment(ctx.user!.id, input.id, input.content)
|
|
46
|
+
),
|
|
47
|
+
|
|
48
|
+
delete: protectedProcedure
|
|
49
|
+
.input(z.object({ id: z.string() }))
|
|
50
|
+
.mutation(({ ctx, input }) => deleteComment(ctx.user!.id, input.id)),
|
|
51
|
+
|
|
35
52
|
addReaction: protectedProcedure
|
|
36
53
|
.input(
|
|
37
54
|
z.object({
|