@studious-lms/server 1.4.4 → 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/index.js +3 -17
- package/dist/index.js.map +1 -1
- 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/conversation.d.ts +22 -0
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/conversation.js +13 -2
- package/dist/models/conversation.js.map +1 -1
- package/dist/models/labChat.d.ts +3 -0
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/message.d.ts +2 -0
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +1 -0
- package/dist/models/newtonChat.d.ts.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 +665 -1
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.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/conversation.d.ts +14 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +6 -3
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/labChat.d.ts +2 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/search.d.ts +41 -0
- package/dist/routers/search.d.ts.map +1 -0
- package/dist/routers/search.js +15 -0
- package/dist/routers/search.js.map +1 -0
- 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/conversation.d.ts +8 -0
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/conversation.js +24 -6
- package/dist/services/conversation.js.map +1 -1
- package/dist/services/labChat.d.ts +2 -0
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +9 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/search.d.ts +21 -0
- package/dist/services/search.d.ts.map +1 -0
- package/dist/services/search.js +352 -0
- package/dist/services/search.js.map +1 -0
- 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/20260417083000_add_conversation_member_hidden_at/migration.sql +2 -0
- package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +60 -0
- package/prisma/schema.prisma +10 -0
- package/src/index.ts +1 -17
- package/src/lib/config/cors.ts +8 -1
- package/src/models/comment.ts +148 -1
- package/src/models/conversation.ts +12 -0
- package/src/pipelines/aiLabChat.ts +2 -1
- package/src/routers/_app.ts +2 -0
- package/src/routers/assignment.ts +224 -1
- package/src/routers/comment.ts +19 -2
- package/src/routers/conversation.ts +7 -0
- package/src/routers/search.ts +14 -0
- package/src/services/comment.ts +113 -18
- package/src/services/conversation.ts +27 -3
- package/src/services/message.ts +8 -0
- package/src/services/search.ts +397 -0
- 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/tests/routers/conversation.test.ts +114 -2
- package/tests/routers/search.test.ts +109 -0
- package/vercel.json +8 -0
|
@@ -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?
|
|
@@ -512,6 +521,7 @@ model ConversationMember {
|
|
|
512
521
|
conversationId String
|
|
513
522
|
role ConversationRole @default(MEMBER)
|
|
514
523
|
joinedAt DateTime @default(now())
|
|
524
|
+
hiddenAt DateTime? // When user last hid this conversation from their list
|
|
515
525
|
lastViewedAt DateTime? // When user last viewed this conversation
|
|
516
526
|
lastViewedMentionAt DateTime? // When user last viewed mentions in this conversation
|
|
517
527
|
|
package/src/index.ts
CHANGED
|
@@ -520,22 +520,6 @@ async function handleFileUpload(req: Request, res: Response) {
|
|
|
520
520
|
}
|
|
521
521
|
|
|
522
522
|
res.header('Access-Control-Allow-Credentials', 'true');
|
|
523
|
-
|
|
524
|
-
const user = await getAuthenticatedRequestUser(req);
|
|
525
|
-
if (!user) {
|
|
526
|
-
return res.status(401).json({ error: 'Authentication required' });
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const uploadSessionId = getUploadSessionId(req);
|
|
530
|
-
if (!uploadSessionId) {
|
|
531
|
-
return res.status(401).json({ error: 'Upload session is required' });
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
await validateDirectUploadIntent({
|
|
535
|
-
filePath,
|
|
536
|
-
uploadSessionId,
|
|
537
|
-
userId: user.id,
|
|
538
|
-
});
|
|
539
523
|
|
|
540
524
|
const contentType = req.headers['content-type'] || 'application/octet-stream';
|
|
541
525
|
const file = bucket.file(filePath);
|
|
@@ -547,7 +531,7 @@ async function handleFileUpload(req: Request, res: Response) {
|
|
|
547
531
|
});
|
|
548
532
|
|
|
549
533
|
writeStream.on('error', (error) => {
|
|
550
|
-
logger.error('Error uploading file:', {error, filePath
|
|
534
|
+
logger.error('Error uploading file:', {error, filePath});
|
|
551
535
|
if (!res.headersSent) {
|
|
552
536
|
res.status(500).json({ error: 'Error uploading file' });
|
|
553
537
|
}
|
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({
|
|
@@ -190,6 +190,18 @@ export function createConversationMember(data: {
|
|
|
190
190
|
});
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/** Hide a conversation for a single member until new activity occurs. */
|
|
194
|
+
export function hideConversationMember(userId: string, conversationId: string) {
|
|
195
|
+
return prisma.conversationMember.update({
|
|
196
|
+
where: {
|
|
197
|
+
userId_conversationId: { userId, conversationId },
|
|
198
|
+
},
|
|
199
|
+
data: {
|
|
200
|
+
hiddenAt: new Date(),
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
193
205
|
/** Remove a member from a conversation. */
|
|
194
206
|
export function deleteConversationMember(userId: string, conversationId: string) {
|
|
195
207
|
return prisma.conversationMember.delete({
|
|
@@ -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>,
|
package/src/routers/_app.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { marketingRouter } from "./marketing.js";
|
|
|
21
21
|
import { worksheetRouter } from "./worksheet.js";
|
|
22
22
|
import { commentRouter } from "./comment.js";
|
|
23
23
|
import { studentProgressRouter } from "./studentProgress.js";
|
|
24
|
+
import { searchRouter } from "./search.js";
|
|
24
25
|
|
|
25
26
|
export const appRouter = createTRPCRouter({
|
|
26
27
|
class: classRouter,
|
|
@@ -43,6 +44,7 @@ export const appRouter = createTRPCRouter({
|
|
|
43
44
|
worksheet: worksheetRouter,
|
|
44
45
|
comment: commentRouter,
|
|
45
46
|
studentProgress: studentProgressRouter,
|
|
47
|
+
search: searchRouter,
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
// Export type router type definition
|
|
@@ -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({
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createConversationRecord,
|
|
6
6
|
getConversation,
|
|
7
7
|
addMemberToConversation,
|
|
8
|
+
hideConversation,
|
|
8
9
|
removeMemberFromConversation,
|
|
9
10
|
} from "../services/conversation.js";
|
|
10
11
|
|
|
@@ -36,6 +37,12 @@ export const conversationRouter = createTRPCRouter({
|
|
|
36
37
|
getConversation(ctx.user!.id, input.conversationId)
|
|
37
38
|
),
|
|
38
39
|
|
|
40
|
+
hide: protectedProcedure
|
|
41
|
+
.input(z.object({ conversationId: z.string() }))
|
|
42
|
+
.mutation(({ ctx, input }) =>
|
|
43
|
+
hideConversation(ctx.user!.id, input.conversationId)
|
|
44
|
+
),
|
|
45
|
+
|
|
39
46
|
addMember: protectedProcedure
|
|
40
47
|
.input(
|
|
41
48
|
z.object({
|