@studious-lms/server 1.13.0 → 1.15.0
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/.dockerignore +28 -0
- package/.env.example +5 -0
- package/.github/workflows/deploy.yml +123 -0
- package/Dockerfile +54 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/instrument.js +3 -3
- package/dist/instrument.js.map +1 -1
- package/dist/lib/config/cors.d.ts.map +1 -1
- package/dist/lib/config/cors.js +31 -11
- package/dist/lib/config/cors.js.map +1 -1
- package/dist/lib/config/env.d.ts +17 -0
- package/dist/lib/config/env.d.ts.map +1 -1
- package/dist/lib/config/env.js +8 -2
- package/dist/lib/config/env.js.map +1 -1
- package/dist/lib/googleCloudStorage.d.ts +10 -0
- package/dist/lib/googleCloudStorage.d.ts.map +1 -1
- package/dist/lib/googleCloudStorage.js +22 -2
- package/dist/lib/googleCloudStorage.js.map +1 -1
- package/dist/lib/inferDocumentFormat.d.ts +10 -0
- package/dist/lib/inferDocumentFormat.d.ts.map +1 -0
- package/dist/lib/inferDocumentFormat.js +48 -0
- package/dist/lib/inferDocumentFormat.js.map +1 -0
- package/dist/lib/legal.d.ts +3 -0
- package/dist/lib/legal.d.ts.map +1 -0
- package/dist/lib/legal.js +6 -0
- package/dist/lib/legal.js.map +1 -0
- package/dist/lib/prisma.d.ts +104 -5
- package/dist/lib/prisma.d.ts.map +1 -1
- package/dist/lib/prisma.js +33 -2
- package/dist/lib/prisma.js.map +1 -1
- package/dist/models/agenda.d.ts +20 -20
- package/dist/models/announcement.d.ts +211 -101
- package/dist/models/announcement.d.ts.map +1 -1
- package/dist/models/assignment.d.ts +560 -362
- package/dist/models/assignment.d.ts.map +1 -1
- package/dist/models/attendance.d.ts +197 -71
- package/dist/models/attendance.d.ts.map +1 -1
- package/dist/models/auth.d.ts +356 -114
- package/dist/models/auth.d.ts.map +1 -1
- package/dist/models/auth.js +4 -2
- package/dist/models/auth.js.map +1 -1
- package/dist/models/class.d.ts +327 -201
- package/dist/models/class.d.ts.map +1 -1
- package/dist/models/class.js +2 -2
- package/dist/models/class.js.map +1 -1
- package/dist/models/cloudStorage.d.ts +146 -56
- package/dist/models/cloudStorage.d.ts.map +1 -1
- package/dist/models/comment.d.ts +262 -110
- package/dist/models/comment.d.ts.map +1 -1
- package/dist/models/conversation.d.ts +206 -80
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/department.d.ts +196 -70
- package/dist/models/department.d.ts.map +1 -1
- package/dist/models/event.d.ts +305 -141
- package/dist/models/event.d.ts.map +1 -1
- package/dist/models/file.d.ts +422 -272
- package/dist/models/file.d.ts.map +1 -1
- package/dist/models/folder.d.ts +433 -181
- package/dist/models/folder.d.ts.map +1 -1
- package/dist/models/labChat.d.ts +205 -79
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/marketing.d.ts +79 -38
- package/dist/models/marketing.d.ts.map +1 -1
- package/dist/models/marketing.js +12 -4
- package/dist/models/marketing.js.map +1 -1
- package/dist/models/message.d.ts +97 -43
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +84 -30
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/notification.d.ts +57 -21
- package/dist/models/notification.d.ts.map +1 -1
- package/dist/models/school.d.ts +349 -133
- package/dist/models/school.d.ts.map +1 -1
- package/dist/models/section.d.ts +216 -72
- package/dist/models/section.d.ts.map +1 -1
- package/dist/models/tag.d.ts +156 -48
- package/dist/models/tag.d.ts.map +1 -1
- package/dist/models/user.d.ts +82 -28
- package/dist/models/user.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +1032 -423
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +8 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/aiLabChat.d.ts +7 -9
- package/dist/pipelines/aiLabChat.d.ts.map +1 -1
- package/dist/pipelines/aiLabChat.js +194 -125
- package/dist/pipelines/aiLabChat.js.map +1 -1
- package/dist/pipelines/aiLabChatContract.d.ts +233 -389
- package/dist/pipelines/aiLabChatContract.d.ts.map +1 -1
- package/dist/pipelines/aiLabChatContract.js +54 -70
- package/dist/pipelines/aiLabChatContract.js.map +1 -1
- package/dist/pipelines/aiNewtonChat.d.ts.map +1 -1
- package/dist/pipelines/aiNewtonChat.js +41 -41
- package/dist/pipelines/aiNewtonChat.js.map +1 -1
- package/dist/pipelines/gradeWorksheet.d.ts +8 -8
- package/dist/pipelines/labChatPrompt.d.ts +18 -11
- package/dist/pipelines/labChatPrompt.d.ts.map +1 -1
- package/dist/pipelines/labChatPrompt.js +93 -75
- package/dist/pipelines/labChatPrompt.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +1 -1
- package/dist/routers/_app.d.ts +2601 -2151
- 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/admin.d.ts +13 -13
- package/dist/routers/agenda.d.ts +18 -18
- package/dist/routers/announcement.d.ts +60 -60
- package/dist/routers/assignment.d.ts +393 -383
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/attendance.d.ts +10 -10
- package/dist/routers/auth.d.ts +4 -2
- package/dist/routers/auth.d.ts.map +1 -1
- package/dist/routers/auth.js +15 -2
- package/dist/routers/auth.js.map +1 -1
- package/dist/routers/class.d.ts +157 -153
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/comment.d.ts +18 -18
- package/dist/routers/conversation.d.ts +11 -11
- package/dist/routers/department.d.ts +34 -34
- package/dist/routers/discussion.d.ts +68 -56
- package/dist/routers/discussion.d.ts.map +1 -1
- package/dist/routers/discussion.js +6 -2
- package/dist/routers/discussion.js.map +1 -1
- package/dist/routers/embedding.d.ts +73 -0
- package/dist/routers/embedding.d.ts.map +1 -0
- package/dist/routers/embedding.js +151 -0
- package/dist/routers/embedding.js.map +1 -0
- package/dist/routers/event.d.ts +57 -55
- package/dist/routers/event.d.ts.map +1 -1
- package/dist/routers/file.d.ts +14 -14
- package/dist/routers/folder.d.ts +38 -38
- package/dist/routers/labChat.d.ts +16 -16
- package/dist/routers/marketing.d.ts +1 -14
- package/dist/routers/marketing.d.ts.map +1 -1
- package/dist/routers/message.d.ts +13 -13
- package/dist/routers/newtonChat.d.ts +4 -4
- package/dist/routers/notifications.d.ts +5 -5
- package/dist/routers/permissions.d.ts +14 -14
- package/dist/routers/school.d.ts +59 -55
- package/dist/routers/school.d.ts.map +1 -1
- package/dist/routers/search.d.ts +22 -1
- package/dist/routers/search.d.ts.map +1 -1
- package/dist/routers/search.js +9 -3
- package/dist/routers/search.js.map +1 -1
- package/dist/routers/section.d.ts +4 -4
- package/dist/routers/studentProgress.d.ts +1 -1
- package/dist/routers/tag.d.ts +7 -7
- package/dist/routers/user.d.ts +99 -10
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +19 -2
- package/dist/routers/user.js.map +1 -1
- package/dist/routers/worksheet.d.ts +93 -55
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +26 -2
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/routes/aiStream.d.ts +2 -0
- package/dist/routes/aiStream.d.ts.map +1 -0
- package/dist/routes/aiStream.js +726 -0
- package/dist/routes/aiStream.js.map +1 -0
- package/dist/seedDatabase.d.ts +3 -1
- package/dist/seedDatabase.d.ts.map +1 -1
- package/dist/seedDatabase.js +6 -6
- package/dist/seedDatabase.js.map +1 -1
- package/dist/services/adminInstitution.d.ts +8 -8
- package/dist/services/adminRunJob.d.ts +4 -4
- package/dist/services/agenda.d.ts +18 -18
- package/dist/services/aiChatStream.d.ts +88 -0
- package/dist/services/aiChatStream.d.ts.map +1 -0
- package/dist/services/aiChatStream.js +254 -0
- package/dist/services/aiChatStream.js.map +1 -0
- package/dist/services/aiModifierTools.d.ts +16 -0
- package/dist/services/aiModifierTools.d.ts.map +1 -0
- package/dist/services/aiModifierTools.js +274 -0
- package/dist/services/aiModifierTools.js.map +1 -0
- package/dist/services/aiProposalTools.d.ts +32 -0
- package/dist/services/aiProposalTools.d.ts.map +1 -0
- package/dist/services/aiProposalTools.js +207 -0
- package/dist/services/aiProposalTools.js.map +1 -0
- package/dist/services/aiStream.d.ts +22 -0
- package/dist/services/aiStream.d.ts.map +1 -0
- package/dist/services/aiStream.js +142 -0
- package/dist/services/aiStream.js.map +1 -0
- package/dist/services/aiTools.d.ts +16 -0
- package/dist/services/aiTools.d.ts.map +1 -0
- package/dist/services/aiTools.js +271 -0
- package/dist/services/aiTools.js.map +1 -0
- package/dist/services/announcement.d.ts +42 -42
- package/dist/services/announcement.d.ts.map +1 -1
- package/dist/services/announcement.js +7 -2
- package/dist/services/announcement.js.map +1 -1
- package/dist/services/assignment.d.ts +355 -345
- package/dist/services/assignment.d.ts.map +1 -1
- package/dist/services/assignment.js +30 -9
- package/dist/services/assignment.js.map +1 -1
- package/dist/services/attendance.d.ts +10 -10
- package/dist/services/auth.d.ts +4 -2
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +13 -4
- package/dist/services/auth.js.map +1 -1
- package/dist/services/class.d.ts +146 -142
- package/dist/services/class.d.ts.map +1 -1
- package/dist/services/class.js +24 -4
- package/dist/services/class.js.map +1 -1
- package/dist/services/comment.d.ts +18 -18
- package/dist/services/conversation.d.ts +11 -11
- package/dist/services/department.d.ts +33 -33
- package/dist/services/discussion.d.ts +58 -58
- package/dist/services/discussion.d.ts.map +1 -1
- package/dist/services/discussion.js +15 -8
- package/dist/services/discussion.js.map +1 -1
- package/dist/services/discussionSummary.d.ts +21 -0
- package/dist/services/discussionSummary.d.ts.map +1 -0
- package/dist/services/discussionSummary.js +179 -0
- package/dist/services/discussionSummary.js.map +1 -0
- package/dist/services/embedding.d.ts +85 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +372 -0
- package/dist/services/embedding.js.map +1 -0
- package/dist/services/event.d.ts +55 -53
- package/dist/services/event.d.ts.map +1 -1
- package/dist/services/file.d.ts +28 -28
- package/dist/services/folder.d.ts +36 -36
- package/dist/services/folder.d.ts.map +1 -1
- package/dist/services/folder.js +32 -2
- package/dist/services/folder.js.map +1 -1
- package/dist/services/labChat.d.ts +16 -16
- package/dist/services/marketing.d.ts +2 -15
- package/dist/services/marketing.d.ts.map +1 -1
- package/dist/services/marketing.js +5 -5
- package/dist/services/marketing.js.map +1 -1
- package/dist/services/message.d.ts +11 -11
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +2 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/newtonChat.d.ts +4 -4
- package/dist/services/notification.d.ts +5 -5
- package/dist/services/notification.d.ts.map +1 -1
- package/dist/services/openaiDocumentGen.d.ts +37 -0
- package/dist/services/openaiDocumentGen.d.ts.map +1 -0
- package/dist/services/openaiDocumentGen.js +262 -0
- package/dist/services/openaiDocumentGen.js.map +1 -0
- package/dist/services/permissions.d.ts +14 -14
- package/dist/services/school.d.ts +54 -50
- package/dist/services/school.d.ts.map +1 -1
- package/dist/services/search.d.ts +13 -1
- package/dist/services/search.d.ts.map +1 -1
- package/dist/services/search.js +145 -2
- package/dist/services/search.js.map +1 -1
- package/dist/services/section.d.ts +3 -3
- package/dist/services/tag.d.ts +4 -4
- package/dist/services/user.d.ts +4 -4
- package/dist/services/user.d.ts.map +1 -1
- package/dist/services/user.js +10 -15
- package/dist/services/user.js.map +1 -1
- package/dist/services/userPrivacy.d.ts +86 -0
- package/dist/services/userPrivacy.d.ts.map +1 -0
- package/dist/services/userPrivacy.js +184 -0
- package/dist/services/userPrivacy.js.map +1 -0
- package/dist/services/worksheet.d.ts +86 -57
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +57 -5
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/email.d.ts.map +1 -1
- package/dist/utils/email.js +7 -3
- package/dist/utils/email.js.map +1 -1
- package/dist/utils/streamBroadcast.d.ts +11 -0
- package/dist/utils/streamBroadcast.d.ts.map +1 -0
- package/dist/utils/streamBroadcast.js +32 -0
- package/dist/utils/streamBroadcast.js.map +1 -0
- package/dist/workers/connection.d.ts +15 -0
- package/dist/workers/connection.d.ts.map +1 -1
- package/dist/workers/connection.js +20 -2
- package/dist/workers/connection.js.map +1 -1
- package/dist/workers/index.js +4 -2
- package/dist/workers/index.js.map +1 -1
- package/dist/workers/processors/createRunJobWorker.d.ts.map +1 -1
- package/dist/workers/processors/createRunJobWorker.js +31 -8
- package/dist/workers/processors/createRunJobWorker.js.map +1 -1
- package/dist/workers/processors/embedContent.d.ts +8 -0
- package/dist/workers/processors/embedContent.d.ts.map +1 -0
- package/dist/workers/processors/embedContent.js +67 -0
- package/dist/workers/processors/embedContent.js.map +1 -0
- package/dist/workers/queues.d.ts +6 -0
- package/dist/workers/queues.d.ts.map +1 -1
- package/dist/workers/queues.js +5 -3
- package/dist/workers/queues.js.map +1 -1
- package/package.json +5 -1
- package/prisma/migrations/20260609215135_init/migration.sql +1412 -0
- package/prisma/migrations/20260610130000_add_worksheet_tags/migration.sql +13 -0
- package/prisma/migrations/20260610140000_expand_embedding_source_types/migration.sql +4 -0
- package/prisma/migrations/20260611120000_add_user_terms_acceptance/migration.sql +3 -0
- package/prisma/schema.prisma +44 -7
- package/src/index.ts +4 -0
- package/src/instrument.ts +1 -1
- package/src/lib/config/cors.ts +32 -9
- package/src/lib/config/env.ts +6 -0
- package/src/lib/googleCloudStorage.ts +21 -0
- package/src/lib/inferDocumentFormat.ts +58 -0
- package/src/lib/legal.ts +2 -0
- package/src/lib/prisma.ts +40 -1
- package/src/models/auth.ts +4 -0
- package/src/models/class.ts +3 -2
- package/src/models/marketing.ts +10 -2
- package/src/models/worksheet.ts +5 -2
- package/src/pipelines/aiLabChat.ts +245 -146
- package/src/pipelines/aiLabChatContract.ts +59 -69
- package/src/pipelines/aiNewtonChat.ts +46 -43
- package/src/pipelines/labChatPrompt.ts +115 -90
- package/src/routers/_app.ts +2 -0
- package/src/routers/auth.ts +13 -0
- package/src/routers/discussion.ts +7 -0
- package/src/routers/embedding.ts +164 -0
- package/src/routers/search.ts +10 -1
- package/src/routers/user.ts +26 -0
- package/src/routers/worksheet.ts +27 -0
- package/src/routes/aiStream.ts +827 -0
- package/src/seedDatabase.ts +4 -4
- package/src/services/aiChatStream.ts +341 -0
- package/src/services/aiModifierTools.ts +341 -0
- package/src/services/aiProposalTools.ts +274 -0
- package/src/services/aiStream.ts +175 -0
- package/src/services/aiTools.ts +290 -0
- package/src/services/announcement.ts +7 -0
- package/src/services/assignment.ts +31 -7
- package/src/services/auth.ts +14 -2
- package/src/services/class.ts +27 -2
- package/src/services/discussion.ts +22 -8
- package/src/services/discussionSummary.ts +226 -0
- package/src/services/embedding.ts +446 -0
- package/src/services/folder.ts +31 -0
- package/src/services/marketing.ts +3 -3
- package/src/services/message.ts +2 -2
- package/src/services/openaiDocumentGen.ts +319 -0
- package/src/services/search.ts +163 -1
- package/src/services/user.ts +9 -17
- package/src/services/userPrivacy.ts +196 -0
- package/src/services/worksheet.ts +77 -3
- package/src/utils/email.ts +5 -1
- package/src/utils/streamBroadcast.ts +48 -0
- package/src/workers/connection.ts +19 -0
- package/src/workers/index.ts +2 -0
- package/src/workers/processors/createRunJobWorker.ts +32 -6
- package/src/workers/processors/embedContent.ts +86 -0
- package/src/workers/queues.ts +5 -1
- package/tests/helpers/registerInput.ts +20 -0
- package/tests/integration/globalSetup.ts +10 -18
- package/tests/integration/routers/auth.test.ts +13 -18
- package/tests/unit/lib/inferDocumentFormat.test.ts +33 -0
- package/tests/unit/pipelines/aiLabChatContract.test.ts +38 -21
- package/tests/unit/pipelines/labChatPrompt.test.ts +23 -36
- package/tests/unit/services/aiChatStream.test.ts +92 -0
- package/tests/unit/services/openaiDocumentGen.test.ts +65 -0
- package/prisma/migrations/20250807062924_init/migration.sql +0 -436
- package/prisma/migrations/20250919063704_init2/migration.sql +0 -24
- package/prisma/migrations/20250920143543_add_profile_fields/migration.sql +0 -15
- package/prisma/migrations/20250922165147_add_chat_models/migration.sql +0 -68
- package/prisma/migrations/20250922165314_add_chat_roles/migration.sql +0 -5
- package/prisma/migrations/20250922171242_refactor_notification_system/migration.sql +0 -20
- package/prisma/migrations/20250922172208_add_mentions_system/migration.sql +0 -21
- package/prisma/migrations/20250925072732_add_lab_chat_system/migration.sql +0 -25
- package/prisma/migrations/20251020151505_add_upload_tracking_fields/migration.sql +0 -57
- package/prisma/migrations/20251109122857_annuoncements_comments/migration.sql +0 -30
- package/prisma/migrations/20251109135555_reactions_announcements_comments/migration.sql +0 -35
- package/prisma/migrations/20260410124000_add_submission_recommendation_state/migration.sql +0 -14
- package/prisma/migrations/20260415103500_add_session_type/migration.sql +0 -33
- package/prisma/migrations/20260415133000_add_ai_usage_logs/migration.sql +0 -19
- package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +0 -2
- package/prisma/migrations/20260502230500_assignment_submission_comments/migration.sql +0 -60
- package/prisma/migrations/20260504071500_school_memberships/migration.sql +0 -81
- package/prisma/migrations/20260506162500_consolidate_run_job_and_unify_job_status/migration.sql +0 -166
- package/prisma/migrations/20260509220000_institution_overhaul/migration.sql +0 -184
- package/prisma/migrations/20260510170000_add_folder_permissions/migration.sql +0 -93
- package/prisma/migrations/20260510183000_add_discussions/migration.sql +0 -151
- /package/.github/workflows/{npm.yml → npm-publish.yml} +0 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User privacy service — data export and account erasure (anonymization).
|
|
3
|
+
*/
|
|
4
|
+
import { TRPCError } from "@trpc/server";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { hash } from "bcryptjs";
|
|
7
|
+
import { prisma } from "../lib/prisma.js";
|
|
8
|
+
import { deleteSessionsByUserId } from "../models/auth.js";
|
|
9
|
+
|
|
10
|
+
async function assertAccountCanBeErased(userId: string) {
|
|
11
|
+
const user = await prisma.user.findUnique({
|
|
12
|
+
where: { id: userId },
|
|
13
|
+
select: {
|
|
14
|
+
id: true,
|
|
15
|
+
teacherIn: {
|
|
16
|
+
select: {
|
|
17
|
+
id: true,
|
|
18
|
+
name: true,
|
|
19
|
+
_count: { select: { students: true } },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
_count: {
|
|
23
|
+
select: {
|
|
24
|
+
assignments: true,
|
|
25
|
+
createdLabChats: true,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!user) {
|
|
32
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const classesWithStudents = user.teacherIn.filter(
|
|
36
|
+
(cls) => cls._count.students > 0,
|
|
37
|
+
);
|
|
38
|
+
if (classesWithStudents.length > 0) {
|
|
39
|
+
throw new TRPCError({
|
|
40
|
+
code: "PRECONDITION_FAILED",
|
|
41
|
+
message:
|
|
42
|
+
"Delete or transfer your classes before deleting your account.",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (user._count.assignments > 0) {
|
|
47
|
+
throw new TRPCError({
|
|
48
|
+
code: "PRECONDITION_FAILED",
|
|
49
|
+
message:
|
|
50
|
+
"Remove or reassign your assignments before deleting your account.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (user._count.createdLabChats > 0) {
|
|
55
|
+
throw new TRPCError({
|
|
56
|
+
code: "PRECONDITION_FAILED",
|
|
57
|
+
message: "Remove your AI lab chats before deleting your account.",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Export a portable snapshot of the authenticated user's personal data. */
|
|
63
|
+
export async function exportUserData(userId: string) {
|
|
64
|
+
const user = await prisma.user.findUnique({
|
|
65
|
+
where: { id: userId },
|
|
66
|
+
select: {
|
|
67
|
+
id: true,
|
|
68
|
+
username: true,
|
|
69
|
+
email: true,
|
|
70
|
+
verified: true,
|
|
71
|
+
termsAcceptedAt: true,
|
|
72
|
+
termsVersion: true,
|
|
73
|
+
profile: true,
|
|
74
|
+
teacherIn: {
|
|
75
|
+
select: { id: true, name: true, subject: true, section: true },
|
|
76
|
+
},
|
|
77
|
+
studentIn: {
|
|
78
|
+
select: { id: true, name: true, subject: true, section: true },
|
|
79
|
+
},
|
|
80
|
+
schoolMemberships: {
|
|
81
|
+
select: {
|
|
82
|
+
role: true,
|
|
83
|
+
status: true,
|
|
84
|
+
school: { select: { id: true, name: true } },
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
submissions: {
|
|
88
|
+
select: {
|
|
89
|
+
id: true,
|
|
90
|
+
createdAt: true,
|
|
91
|
+
submittedAt: true,
|
|
92
|
+
gradeReceived: true,
|
|
93
|
+
extendedResponse: true,
|
|
94
|
+
assignment: { select: { id: true, title: true, classId: true } },
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
events: {
|
|
98
|
+
select: {
|
|
99
|
+
id: true,
|
|
100
|
+
name: true,
|
|
101
|
+
startTime: true,
|
|
102
|
+
endTime: true,
|
|
103
|
+
location: true,
|
|
104
|
+
remarks: true,
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
sentMessages: {
|
|
108
|
+
select: {
|
|
109
|
+
id: true,
|
|
110
|
+
content: true,
|
|
111
|
+
createdAt: true,
|
|
112
|
+
conversationId: true,
|
|
113
|
+
},
|
|
114
|
+
take: 500,
|
|
115
|
+
orderBy: { createdAt: "desc" },
|
|
116
|
+
},
|
|
117
|
+
notificationsReceived: {
|
|
118
|
+
select: {
|
|
119
|
+
id: true,
|
|
120
|
+
title: true,
|
|
121
|
+
content: true,
|
|
122
|
+
read: true,
|
|
123
|
+
createdAt: true,
|
|
124
|
+
},
|
|
125
|
+
take: 200,
|
|
126
|
+
orderBy: { createdAt: "desc" },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!user) {
|
|
132
|
+
throw new TRPCError({ code: "NOT_FOUND", message: "User not found" });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
exportedAt: new Date().toISOString(),
|
|
137
|
+
user,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Erase personal account data by anonymizing the user record and removing
|
|
143
|
+
* sessions and memberships. Educational records tied to classes (e.g.
|
|
144
|
+
* submissions) remain with an anonymized identity where required.
|
|
145
|
+
*/
|
|
146
|
+
export async function deleteUserAccount(userId: string) {
|
|
147
|
+
await assertAccountCanBeErased(userId);
|
|
148
|
+
await deleteSessionsByUserId(userId);
|
|
149
|
+
|
|
150
|
+
const anonUsername = `deleted_${userId.slice(0, 8)}`;
|
|
151
|
+
const anonEmail = `deleted+${userId}@studious.invalid`;
|
|
152
|
+
const randomPassword = await hash(randomBytes(32).toString("hex"), 10);
|
|
153
|
+
|
|
154
|
+
await prisma.$transaction([
|
|
155
|
+
prisma.conversationMember.deleteMany({ where: { userId } }),
|
|
156
|
+
prisma.notification.deleteMany({
|
|
157
|
+
where: { OR: [{ senderId: userId }, { receiverId: userId }] },
|
|
158
|
+
}),
|
|
159
|
+
prisma.schoolMembership.deleteMany({ where: { userId } }),
|
|
160
|
+
prisma.permission.deleteMany({
|
|
161
|
+
where: { OR: [{ userId }, { grantedById: userId }] },
|
|
162
|
+
}),
|
|
163
|
+
prisma.user.update({
|
|
164
|
+
where: { id: userId },
|
|
165
|
+
data: {
|
|
166
|
+
teacherIn: { set: [] },
|
|
167
|
+
studentIn: { set: [] },
|
|
168
|
+
departments: { set: [] },
|
|
169
|
+
tags: { set: [] },
|
|
170
|
+
specificAssignments: { set: [] },
|
|
171
|
+
presentAttendance: { set: [] },
|
|
172
|
+
lateAttendance: { set: [] },
|
|
173
|
+
absentAttendance: { set: [] },
|
|
174
|
+
schoolId: null,
|
|
175
|
+
username: anonUsername,
|
|
176
|
+
email: anonEmail,
|
|
177
|
+
password: randomPassword,
|
|
178
|
+
verified: false,
|
|
179
|
+
termsAcceptedAt: null,
|
|
180
|
+
termsVersion: null,
|
|
181
|
+
profile: {
|
|
182
|
+
update: {
|
|
183
|
+
displayName: null,
|
|
184
|
+
bio: null,
|
|
185
|
+
location: null,
|
|
186
|
+
website: null,
|
|
187
|
+
profilePicture: null,
|
|
188
|
+
profilePictureThumbnail: null,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
]);
|
|
194
|
+
|
|
195
|
+
return { success: true };
|
|
196
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Integrates with gradeWorksheet pipeline for auto-grading.
|
|
4
4
|
*/
|
|
5
5
|
import { TRPCError } from "@trpc/server";
|
|
6
|
-
import { JobStatus, type WorksheetQuestionType } from "@prisma/client";
|
|
6
|
+
import { JobStatus, Prisma, type WorksheetQuestionType } from "@prisma/client";
|
|
7
7
|
import { prisma } from "../lib/prisma.js";
|
|
8
8
|
import { getFileUrl } from "../lib/fileUpload.js";
|
|
9
9
|
import { cancelGradePipeline, regradeWorksheetPipeline } from "../pipelines/gradeWorksheet.js";
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
findStudentQuestionProgressByIdForStudentOrTeacher,
|
|
39
39
|
findStudentQuestionProgressByIdForTeacherAndResponse,
|
|
40
40
|
} from "../models/worksheet.js";
|
|
41
|
+
import { enqueueEmbedding } from "./embedding.js";
|
|
41
42
|
|
|
42
43
|
type MCQOptions = { id: string; text: string; isCorrect: boolean }[];
|
|
43
44
|
|
|
@@ -108,7 +109,10 @@ export async function updateWorksheetRecord(
|
|
|
108
109
|
message: "Only class teachers can update worksheets",
|
|
109
110
|
});
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
+
const updated = await updateWorksheet(worksheetId, data);
|
|
113
|
+
// Re-index worksheet for RAG search
|
|
114
|
+
enqueueEmbedding(worksheet.classId, 'WORKSHEET', worksheetId);
|
|
115
|
+
return updated;
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
export async function deleteWorksheetRecord(worksheetId: string, userId: string) {
|
|
@@ -123,7 +127,10 @@ export async function deleteWorksheetRecord(worksheetId: string, userId: string)
|
|
|
123
127
|
}
|
|
124
128
|
|
|
125
129
|
export async function createWorksheetRecord(classId: string, name: string) {
|
|
126
|
-
|
|
130
|
+
const worksheet = await createWorksheet({ classId, name });
|
|
131
|
+
// Auto-index worksheet for RAG search
|
|
132
|
+
enqueueEmbedding(classId, 'WORKSHEET', worksheet.id);
|
|
133
|
+
return worksheet;
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
/**
|
|
@@ -316,6 +323,73 @@ export async function addQuestionToWorksheet(
|
|
|
316
323
|
});
|
|
317
324
|
}
|
|
318
325
|
|
|
326
|
+
/** A question's full editable state as captured before an AI modifier run. */
|
|
327
|
+
export interface WorksheetQuestionSnapshot {
|
|
328
|
+
id: string;
|
|
329
|
+
type: WorksheetQuestionType;
|
|
330
|
+
question: string;
|
|
331
|
+
answer: string;
|
|
332
|
+
options: unknown;
|
|
333
|
+
markScheme: unknown;
|
|
334
|
+
points: number;
|
|
335
|
+
order: number | null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const toJsonInput = (value: unknown) =>
|
|
339
|
+
value === null || value === undefined
|
|
340
|
+
? Prisma.JsonNull
|
|
341
|
+
: (value as Prisma.InputJsonValue);
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Restore a worksheet's questions to a snapshot: deletes questions not in the
|
|
345
|
+
* snapshot, re-creates deleted ones (with their original ids), and reverts
|
|
346
|
+
* edited ones. Used by AI modifier undo.
|
|
347
|
+
*/
|
|
348
|
+
export async function restoreWorksheetQuestions(
|
|
349
|
+
worksheetId: string,
|
|
350
|
+
userId: string,
|
|
351
|
+
questions: WorksheetQuestionSnapshot[]
|
|
352
|
+
) {
|
|
353
|
+
const worksheet = await findWorksheetByIdForTeacher(worksheetId, userId);
|
|
354
|
+
if (!worksheet) {
|
|
355
|
+
throw new TRPCError({
|
|
356
|
+
code: "FORBIDDEN",
|
|
357
|
+
message: "Only class teachers can modify worksheet questions",
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await prisma.$transaction([
|
|
362
|
+
prisma.worksheetQuestion.deleteMany({
|
|
363
|
+
where: { worksheetId, id: { notIn: questions.map((q) => q.id) } },
|
|
364
|
+
}),
|
|
365
|
+
...questions.map((q) =>
|
|
366
|
+
prisma.worksheetQuestion.upsert({
|
|
367
|
+
where: { id: q.id },
|
|
368
|
+
create: {
|
|
369
|
+
id: q.id,
|
|
370
|
+
worksheetId,
|
|
371
|
+
type: q.type,
|
|
372
|
+
question: q.question,
|
|
373
|
+
answer: q.answer,
|
|
374
|
+
options: toJsonInput(q.options),
|
|
375
|
+
markScheme: toJsonInput(q.markScheme),
|
|
376
|
+
points: q.points,
|
|
377
|
+
order: q.order,
|
|
378
|
+
},
|
|
379
|
+
update: {
|
|
380
|
+
type: q.type,
|
|
381
|
+
question: q.question,
|
|
382
|
+
answer: q.answer,
|
|
383
|
+
options: toJsonInput(q.options),
|
|
384
|
+
markScheme: toJsonInput(q.markScheme),
|
|
385
|
+
points: q.points,
|
|
386
|
+
order: q.order,
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
),
|
|
390
|
+
]);
|
|
391
|
+
}
|
|
392
|
+
|
|
319
393
|
export async function reorderWorksheetQuestions(
|
|
320
394
|
worksheetId: string,
|
|
321
395
|
userId: string,
|
package/src/utils/email.ts
CHANGED
|
@@ -27,7 +27,11 @@ export const transport = nodemailer.createTransport({
|
|
|
27
27
|
export const sendMail = async ({ from, to, subject, text }: sendMailProps) => {
|
|
28
28
|
// Wrapper function for sending emails
|
|
29
29
|
if (env.EMAIL_DRY_RUN == "true") {
|
|
30
|
-
logger.info(
|
|
30
|
+
logger.info("Email dry run enabled. Would have sent email", {
|
|
31
|
+
to,
|
|
32
|
+
from,
|
|
33
|
+
subject,
|
|
34
|
+
});
|
|
31
35
|
return;
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broadcast text content as sequential Pusher chunks so connected clients
|
|
3
|
+
* can render progressive streaming even when the underlying inference call
|
|
4
|
+
* returns the full text at once (structured-output pipelines).
|
|
5
|
+
*/
|
|
6
|
+
import { pusher, chatChannel } from '../lib/pusher.js';
|
|
7
|
+
|
|
8
|
+
const CHUNK_SIZE = 80;
|
|
9
|
+
|
|
10
|
+
interface BroadcastTextChunksOptions {
|
|
11
|
+
/** When true, skip the `message-stream-complete` event at the end.
|
|
12
|
+
* Callers that need to broadcast additional events (e.g. tool-call
|
|
13
|
+
* indicators) before the stream ends should set this to `true` and
|
|
14
|
+
* call {@link broadcastStreamComplete} explicitly when done. */
|
|
15
|
+
skipComplete?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function broadcastTextChunks(
|
|
19
|
+
conversationId: string,
|
|
20
|
+
messageId: string,
|
|
21
|
+
text: string,
|
|
22
|
+
options?: BroadcastTextChunksOptions,
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
for (let i = 0; i < text.length; i += CHUNK_SIZE) {
|
|
25
|
+
const chunk = text.slice(i, i + CHUNK_SIZE);
|
|
26
|
+
await pusher
|
|
27
|
+
.trigger(chatChannel(conversationId), 'message-stream-chunk', {
|
|
28
|
+
messageId,
|
|
29
|
+
chunk,
|
|
30
|
+
})
|
|
31
|
+
.catch(() => {});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!options?.skipComplete) {
|
|
35
|
+
await broadcastStreamComplete(conversationId, messageId);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function broadcastStreamComplete(
|
|
40
|
+
conversationId: string,
|
|
41
|
+
messageId: string,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
await pusher
|
|
44
|
+
.trigger(chatChannel(conversationId), 'message-stream-complete', {
|
|
45
|
+
messageId,
|
|
46
|
+
})
|
|
47
|
+
.catch(() => {});
|
|
48
|
+
}
|
|
@@ -35,3 +35,22 @@ function buildBullConnection(): IORedis {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
export const bullConnection = buildBullConnection();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Key prefix applied to every BullMQ Queue and Worker.
|
|
41
|
+
*
|
|
42
|
+
* Without this, BullMQ defaults to the `bull` namespace, so ANY process
|
|
43
|
+
* pointed at the same Redis instance shares the same queues. That is a
|
|
44
|
+
* problem when a single Redis (e.g. a shared Upstash instance) is used across
|
|
45
|
+
* environments while each environment has its own database: a worker can pick
|
|
46
|
+
* up a job enqueued elsewhere, fail to find its `RunJob` row, and crash-loop
|
|
47
|
+
* with "No record was found for an update".
|
|
48
|
+
*
|
|
49
|
+
* Namespacing by environment keeps each environment's jobs isolated. Override
|
|
50
|
+
* with `BULL_PREFIX` when you need finer-grained separation (e.g. per
|
|
51
|
+
* developer sharing one Redis).
|
|
52
|
+
*/
|
|
53
|
+
export function getBullPrefix(): string {
|
|
54
|
+
const explicit = env.BULL_PREFIX?.trim();
|
|
55
|
+
return explicit && explicit.length > 0 ? explicit : `bull:${env.NODE_ENV}`;
|
|
56
|
+
}
|
package/src/workers/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { aiNewtonChatWorker } from './processors/aiNewtonChat.js';
|
|
|
17
17
|
import { gradeWorksheetWorker } from './processors/gradeWorksheet.js';
|
|
18
18
|
import { pdfToWorksheetWorker } from './processors/pdfToWorksheet.js';
|
|
19
19
|
import { studentProgressChatWorker } from './processors/studentProgressChat.js';
|
|
20
|
+
import { embedContentWorker } from './processors/embedContent.js';
|
|
20
21
|
|
|
21
22
|
const workers: Worker<any>[] = [
|
|
22
23
|
pdfToWorksheetWorker,
|
|
@@ -24,6 +25,7 @@ const workers: Worker<any>[] = [
|
|
|
24
25
|
aiLabChatWorker,
|
|
25
26
|
aiNewtonChatWorker,
|
|
26
27
|
studentProgressChatWorker,
|
|
28
|
+
embedContentWorker,
|
|
27
29
|
];
|
|
28
30
|
|
|
29
31
|
logger.info('Workers ready', {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { Worker, type Job, type WorkerOptions } from 'bullmq';
|
|
19
19
|
import { Prisma, type RunJobType } from '@prisma/client';
|
|
20
|
-
import { bullConnection } from '../connection.js';
|
|
20
|
+
import { bullConnection, getBullPrefix } from '../connection.js';
|
|
21
21
|
import { prisma } from '../../lib/prisma.js';
|
|
22
22
|
import { logger } from '../../utils/logger.js';
|
|
23
23
|
import { QUEUE_NAMES, type JobInputs } from '../queues.js';
|
|
@@ -69,7 +69,21 @@ export function createRunJobWorker<T extends RunJobType>({
|
|
|
69
69
|
select: { status: true, result: true, userId: true, classId: true },
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
if (existing
|
|
72
|
+
if (!existing) {
|
|
73
|
+
// Orphaned job: the RunJob row this job points at does not exist in
|
|
74
|
+
// THIS database. Typically caused by a Redis instance shared across
|
|
75
|
+
// environments/databases (the `prefix` namespacing in this file's
|
|
76
|
+
// Worker options is the primary guard against that), or by a DB reset
|
|
77
|
+
// that left jobs behind in Redis. Discard instead of throwing on the
|
|
78
|
+
// `update` below and burning all retry attempts.
|
|
79
|
+
logger.warn(`${name} RunJob not found; discarding orphaned job`, {
|
|
80
|
+
runJobId,
|
|
81
|
+
bullJobId: job.id,
|
|
82
|
+
});
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (existing.status === 'SUCCEEDED') {
|
|
73
87
|
logger.info(`${name} RunJob already succeeded; skipping re-run`, {
|
|
74
88
|
runJobId,
|
|
75
89
|
bullJobId: job.id,
|
|
@@ -81,7 +95,7 @@ export function createRunJobWorker<T extends RunJobType>({
|
|
|
81
95
|
where: { id: runJobId },
|
|
82
96
|
data: {
|
|
83
97
|
status: 'RUNNING',
|
|
84
|
-
startedAt: existing
|
|
98
|
+
startedAt: existing.status === 'RUNNING' ? undefined : new Date(),
|
|
85
99
|
attempts: { increment: 1 },
|
|
86
100
|
},
|
|
87
101
|
});
|
|
@@ -90,8 +104,8 @@ export function createRunJobWorker<T extends RunJobType>({
|
|
|
90
104
|
runJobId,
|
|
91
105
|
bullJobId: job.id,
|
|
92
106
|
attempt: job.attemptsMade + 1,
|
|
93
|
-
userId: existing
|
|
94
|
-
classId: existing
|
|
107
|
+
userId: existing.userId ?? null,
|
|
108
|
+
classId: existing.classId ?? null,
|
|
95
109
|
});
|
|
96
110
|
|
|
97
111
|
await prisma.runJob.update({
|
|
@@ -106,7 +120,7 @@ export function createRunJobWorker<T extends RunJobType>({
|
|
|
106
120
|
|
|
107
121
|
return result;
|
|
108
122
|
},
|
|
109
|
-
{ connection: bullConnection, ...workerOptions },
|
|
123
|
+
{ connection: bullConnection, prefix: getBullPrefix(), ...workerOptions },
|
|
110
124
|
);
|
|
111
125
|
|
|
112
126
|
worker.on('error', (err) => {
|
|
@@ -143,6 +157,18 @@ export function createRunJobWorker<T extends RunJobType>({
|
|
|
143
157
|
},
|
|
144
158
|
})
|
|
145
159
|
.catch((updateError) => {
|
|
160
|
+
// P2025 = record not found. The RunJob row is gone (orphaned job from a
|
|
161
|
+
// shared/reset Redis), so there's nothing to flag as FAILED — log
|
|
162
|
+
// quietly instead of as an error to avoid noise.
|
|
163
|
+
if (
|
|
164
|
+
updateError instanceof Prisma.PrismaClientKnownRequestError &&
|
|
165
|
+
updateError.code === 'P2025'
|
|
166
|
+
) {
|
|
167
|
+
logger.warn(`${name} failure not recorded; RunJob no longer exists`, {
|
|
168
|
+
runJobId,
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
146
172
|
logger.error(`Failed to record ${name} failure on RunJob`, {
|
|
147
173
|
runJobId,
|
|
148
174
|
error:
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background worker for embedding content into pgvector.
|
|
3
|
+
* Handles indexing of files, assignments, worksheets, announcements,
|
|
4
|
+
* discussions, submissions, and syllabi.
|
|
5
|
+
*/
|
|
6
|
+
import { createRunJobWorker } from './createRunJobWorker.js';
|
|
7
|
+
import {
|
|
8
|
+
indexAssignment,
|
|
9
|
+
indexWorksheet,
|
|
10
|
+
indexAnnouncement,
|
|
11
|
+
indexDiscussion,
|
|
12
|
+
indexSubmission,
|
|
13
|
+
indexSyllabus,
|
|
14
|
+
indexFile,
|
|
15
|
+
indexContent,
|
|
16
|
+
} from '../../services/embedding.js';
|
|
17
|
+
import { prisma } from '../../lib/prisma.js';
|
|
18
|
+
import { logger } from '../../utils/logger.js';
|
|
19
|
+
import type { EmbeddingSourceType } from '@prisma/client';
|
|
20
|
+
|
|
21
|
+
export const embedContentWorker = createRunJobWorker({
|
|
22
|
+
type: 'EMBED_CONTENT',
|
|
23
|
+
workerOptions: {
|
|
24
|
+
concurrency: 3,
|
|
25
|
+
limiter: { max: 10, duration: 60_000 },
|
|
26
|
+
},
|
|
27
|
+
run: async ({ classId, sourceType, sourceId }) => {
|
|
28
|
+
const typedSourceType = sourceType as EmbeddingSourceType;
|
|
29
|
+
|
|
30
|
+
switch (typedSourceType) {
|
|
31
|
+
case 'ASSIGNMENT':
|
|
32
|
+
await indexAssignment(classId, sourceId);
|
|
33
|
+
break;
|
|
34
|
+
|
|
35
|
+
case 'WORKSHEET':
|
|
36
|
+
await indexWorksheet(classId, sourceId);
|
|
37
|
+
break;
|
|
38
|
+
|
|
39
|
+
case 'ANNOUNCEMENT':
|
|
40
|
+
await indexAnnouncement(classId, sourceId);
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
case 'FILE':
|
|
44
|
+
await indexFile(classId, sourceId);
|
|
45
|
+
break;
|
|
46
|
+
|
|
47
|
+
case 'DISCUSSION':
|
|
48
|
+
await indexDiscussion(classId, sourceId);
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
case 'SUBMISSION':
|
|
52
|
+
await indexSubmission(classId, sourceId);
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
case 'SYLLABUS':
|
|
56
|
+
await indexSyllabus(sourceId);
|
|
57
|
+
break;
|
|
58
|
+
|
|
59
|
+
case 'SECTION': {
|
|
60
|
+
const section = await prisma.section.findUnique({
|
|
61
|
+
where: { id: sourceId },
|
|
62
|
+
select: { name: true },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!section) {
|
|
66
|
+
return { skipped: true, reason: 'section_not_found' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await indexContent({
|
|
70
|
+
classId,
|
|
71
|
+
sourceType: 'SECTION',
|
|
72
|
+
sourceId,
|
|
73
|
+
text: `Section: ${section.name}`,
|
|
74
|
+
metadata: { name: section.name },
|
|
75
|
+
});
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
default:
|
|
80
|
+
logger.warn('Unknown embedding source type', { sourceType, sourceId });
|
|
81
|
+
return { skipped: true, reason: 'unknown_source_type' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { indexed: true, sourceType, sourceId };
|
|
85
|
+
},
|
|
86
|
+
});
|
package/src/workers/queues.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { Queue, type JobsOptions } from 'bullmq';
|
|
10
10
|
import { Prisma, type RunJobType } from '@prisma/client';
|
|
11
|
-
import { bullConnection } from './connection.js';
|
|
11
|
+
import { bullConnection, getBullPrefix } from './connection.js';
|
|
12
12
|
import { prisma } from '../lib/prisma.js';
|
|
13
13
|
import { logger } from '../utils/logger.js';
|
|
14
14
|
|
|
@@ -47,6 +47,8 @@ export type JobInputs = {
|
|
|
47
47
|
*/
|
|
48
48
|
AI_NEWTON_CHAT: { conversationId: string; aiMessageId: string };
|
|
49
49
|
STUDENT_PROGRESS_CHAT: { classId: string; studentId: string; message: string };
|
|
50
|
+
/** Embed content into pgvector for RAG search. */
|
|
51
|
+
EMBED_CONTENT: { classId: string; sourceType: string; sourceId: string };
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
export const QUEUE_NAMES: Record<RunJobType, string> = {
|
|
@@ -55,6 +57,7 @@ export const QUEUE_NAMES: Record<RunJobType, string> = {
|
|
|
55
57
|
AI_LAB_CHAT: 'aiLabChat',
|
|
56
58
|
AI_NEWTON_CHAT: 'aiNewtonChat',
|
|
57
59
|
STUDENT_PROGRESS_CHAT: 'studentProgressChat',
|
|
60
|
+
EMBED_CONTENT: 'embedContent',
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
/**
|
|
@@ -68,6 +71,7 @@ function getQueue(type: RunJobType): Queue {
|
|
|
68
71
|
if (!queue) {
|
|
69
72
|
queue = new Queue(QUEUE_NAMES[type], {
|
|
70
73
|
connection: bullConnection,
|
|
74
|
+
prefix: getBullPrefix(),
|
|
71
75
|
defaultJobOptions: DEFAULT_JOB_OPTIONS,
|
|
72
76
|
});
|
|
73
77
|
queueCache.set(type, queue);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CURRENT_TERMS_VERSION } from "../../src/lib/legal.js";
|
|
2
|
+
|
|
3
|
+
export function registerInput(
|
|
4
|
+
overrides: {
|
|
5
|
+
username: string;
|
|
6
|
+
email: string;
|
|
7
|
+
password?: string;
|
|
8
|
+
confirmPassword?: string;
|
|
9
|
+
},
|
|
10
|
+
) {
|
|
11
|
+
const password = overrides.password ?? "password_is_1234";
|
|
12
|
+
return {
|
|
13
|
+
username: overrides.username,
|
|
14
|
+
email: overrides.email,
|
|
15
|
+
password,
|
|
16
|
+
confirmPassword: overrides.confirmPassword ?? password,
|
|
17
|
+
agreeToTerms: true as const,
|
|
18
|
+
termsVersion: CURRENT_TERMS_VERSION,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -10,6 +10,7 @@ import { createTRPCContext } from '../../src/trpc';
|
|
|
10
10
|
import { clearDatabase } from '../../src/seedDatabase';
|
|
11
11
|
import { execSync } from 'child_process';
|
|
12
12
|
import { SessionType } from '@prisma/client';
|
|
13
|
+
import { registerInput } from '../helpers/registerInput.js';
|
|
13
14
|
|
|
14
15
|
async function runSetup() {
|
|
15
16
|
const ctx = await createTRPCContext({
|
|
@@ -20,26 +21,17 @@ async function runSetup() {
|
|
|
20
21
|
|
|
21
22
|
execSync('npm run test:pre');
|
|
22
23
|
|
|
23
|
-
const user1 = await caller.auth.register(
|
|
24
|
-
username: 'testuser1',
|
|
25
|
-
|
|
26
|
-
password: 'password_is_1234',
|
|
27
|
-
confirmPassword: 'password_is_1234',
|
|
28
|
-
});
|
|
24
|
+
const user1 = await caller.auth.register(
|
|
25
|
+
registerInput({ username: 'testuser1', email: 'test@studious.sh' }),
|
|
26
|
+
);
|
|
29
27
|
|
|
30
|
-
const user2 = await caller.auth.register(
|
|
31
|
-
username: 'testuser2',
|
|
32
|
-
|
|
33
|
-
password: 'password_is_1234',
|
|
34
|
-
confirmPassword: 'password_is_1234',
|
|
35
|
-
});
|
|
28
|
+
const user2 = await caller.auth.register(
|
|
29
|
+
registerInput({ username: 'testuser2', email: 'test2@studious.sh' }),
|
|
30
|
+
);
|
|
36
31
|
|
|
37
|
-
const user3 = await caller.auth.register(
|
|
38
|
-
username: 'testuser3',
|
|
39
|
-
|
|
40
|
-
password: 'password_is_1234',
|
|
41
|
-
confirmPassword: 'password_is_1234',
|
|
42
|
-
});
|
|
32
|
+
const user3 = await caller.auth.register(
|
|
33
|
+
registerInput({ username: 'testuser3', email: 'test3@studious.sh' }),
|
|
34
|
+
);
|
|
43
35
|
|
|
44
36
|
const session1 = await prisma.session.findFirst({
|
|
45
37
|
where: { userId: user1.user.id, type: SessionType.EMAIL_VERIFICATION },
|