refactor: fix all P0/P1/P2 bugs and architecture issues
Bug fixes (from bugs/ directory): - Fix cross-module DB queries in 9 modules (homework, grades, parent, diagnostic, elective, proctoring, notifications, scheduling, classes) by routing through data-access functions - Fix shared/lib <-> auth circular dependency via new session.ts module - Fix divide-by-zero guard in grades data-access - Fix audit export data truncation (paginated fetch for full datasets) - Fix missing transactions in homework grading and elective lottery - Fix missing revalidatePath in course-plans actions - Fix frontend permission checks using requirePermission instead of requireAuth - Fix dashboard role routing using session.user.roles - Fix student auth pattern (migrate getDemoStudentUser to users module) - Fix ActionState return type handling in components Code quality fixes: - Remove 60+ as type assertions (replace with type guards) - Remove non-null assertions (use optional chaining or explicit checks) - Convert dynamic imports to static imports (grades, diagnostic) - Add React.cache() wrapping for read functions - Parallelize independent queries with Promise.all - Add explicit return types to 30+ arrow functions - Replace any with unknown + type guards - Fix import type for type-only imports - Add Zod validation schemas for classes and diagnostic modules - Extract duplicate code (normalizeRoleName, normalizeBcryptHash, logger IP extraction) - Add console.error to silent catch blocks - Fix permission naming consistency (exam:proctor_read -> exam:proctor:read) Architecture doc sync: - Update 004_architecture_impact_map.md and 005_architecture_data.json - Update management-modules-audit.md for P0-7 cross-module fix Moved deleted proctoring event route to deletes/ folder.
This commit is contained in:
@@ -5,16 +5,21 @@ import { and, count, eq } from "drizzle-orm"
|
||||
|
||||
import { db } from "@/shared/db"
|
||||
import {
|
||||
classes,
|
||||
classEnrollments,
|
||||
classSubjectTeachers,
|
||||
exams,
|
||||
homeworkAnswers,
|
||||
homeworkAssignmentQuestions,
|
||||
homeworkAssignmentTargets,
|
||||
homeworkAssignments,
|
||||
homeworkSubmissions,
|
||||
} from "@/shared/db/schema"
|
||||
import {
|
||||
getActiveStudentIdsByClassId,
|
||||
getClassTeacherById as getClassTeacherIdFromClass,
|
||||
getTeacherSubjectIdsByClass,
|
||||
} from "@/modules/classes/data-access"
|
||||
import {
|
||||
getExamWithQuestionsForHomework as getExamWithQuestionsFromExams,
|
||||
type ExamWithQuestionsForHomework,
|
||||
} from "@/modules/exams/data-access"
|
||||
import type { DataScope } from "@/shared/types/permissions"
|
||||
|
||||
// ---- Types ----
|
||||
@@ -25,13 +30,7 @@ export type HomeworkExamQuestionData = {
|
||||
order: number | null
|
||||
}
|
||||
|
||||
export type HomeworkExamData = {
|
||||
id: string
|
||||
title: string
|
||||
subjectId: string | null
|
||||
structure: unknown
|
||||
questions: HomeworkExamQuestionData[]
|
||||
}
|
||||
export type HomeworkExamData = ExamWithQuestionsForHomework
|
||||
|
||||
export type HomeworkSubmissionPermissionData = {
|
||||
id: string
|
||||
@@ -63,85 +62,38 @@ export type CreateHomeworkAssignmentData = {
|
||||
}
|
||||
|
||||
// ---- Query helpers (for permission/validation in actions) ----
|
||||
// These delegate to cross-module data-access interfaces to avoid direct DB queries.
|
||||
|
||||
export const getClassTeacherById = async (
|
||||
classId: string
|
||||
): Promise<{ id: string; teacherId: string } | null> => {
|
||||
const [row] = await db
|
||||
.select({ id: classes.id, teacherId: classes.teacherId })
|
||||
.from(classes)
|
||||
.where(eq(classes.id, classId))
|
||||
.limit(1)
|
||||
return row ?? null
|
||||
): Promise<{ id: string; teacherId: string | null } | null> => {
|
||||
const teacherId = await getClassTeacherIdFromClass(classId)
|
||||
if (teacherId === null) return null
|
||||
return { id: classId, teacherId }
|
||||
}
|
||||
|
||||
export const getExamWithQuestionsForHomework = async (
|
||||
examId: string
|
||||
): Promise<HomeworkExamData | null> => {
|
||||
const exam = await db.query.exams.findFirst({
|
||||
where: eq(exams.id, examId),
|
||||
with: {
|
||||
questions: {
|
||||
orderBy: (examQuestions, { asc }) => [asc(examQuestions.order)],
|
||||
},
|
||||
},
|
||||
})
|
||||
if (!exam) return null
|
||||
return {
|
||||
id: exam.id,
|
||||
title: exam.title,
|
||||
subjectId: exam.subjectId,
|
||||
structure: exam.structure,
|
||||
questions: exam.questions.map((q) => ({
|
||||
questionId: q.questionId,
|
||||
score: q.score ?? null,
|
||||
order: q.order ?? null,
|
||||
})),
|
||||
}
|
||||
return await getExamWithQuestionsFromExams(examId)
|
||||
}
|
||||
|
||||
export const getTeacherAssignedSubjectIds = async (
|
||||
classId: string,
|
||||
teacherId: string
|
||||
): Promise<string[]> => {
|
||||
const rows = await db
|
||||
.select({ subjectId: classSubjectTeachers.subjectId })
|
||||
.from(classSubjectTeachers)
|
||||
.where(
|
||||
and(
|
||||
eq(classSubjectTeachers.classId, classId),
|
||||
eq(classSubjectTeachers.teacherId, teacherId)
|
||||
)
|
||||
)
|
||||
return rows.map((r) => r.subjectId)
|
||||
return await getTeacherSubjectIdsByClass(classId, teacherId)
|
||||
}
|
||||
|
||||
export const getActiveClassStudentIdsForHomework = async (
|
||||
classId: string,
|
||||
dataScope: DataScope,
|
||||
userId: string,
|
||||
classTeacherId: string
|
||||
_dataScope: DataScope,
|
||||
_userId: string,
|
||||
_classTeacherId: string | null
|
||||
): Promise<string[]> => {
|
||||
const classScope =
|
||||
dataScope.type === "all"
|
||||
? eq(classes.id, classId)
|
||||
: classTeacherId === userId
|
||||
? eq(classes.teacherId, userId)
|
||||
: eq(classes.id, classId)
|
||||
|
||||
const rows = await db
|
||||
.select({ studentId: classEnrollments.studentId })
|
||||
.from(classEnrollments)
|
||||
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
|
||||
.where(
|
||||
and(
|
||||
eq(classEnrollments.classId, classId),
|
||||
eq(classEnrollments.status, "active"),
|
||||
classScope
|
||||
)
|
||||
)
|
||||
|
||||
return rows.map((r) => r.studentId)
|
||||
// Permission/scope filtering is handled by requirePermission in actions.ts.
|
||||
// This function returns active students for the class via the classes data-access interface.
|
||||
return await getActiveStudentIdsByClassId(classId)
|
||||
}
|
||||
|
||||
export const getHomeworkSubmissionForPermission = async (
|
||||
@@ -301,17 +253,19 @@ export const gradeHomeworkAnswers = async (
|
||||
submissionId: string,
|
||||
answers: Array<{ id: string; score: number; feedback: string | null }>
|
||||
): Promise<void> => {
|
||||
let totalScore = 0
|
||||
for (const ans of answers) {
|
||||
await db
|
||||
.update(homeworkAnswers)
|
||||
.set({ score: ans.score, feedback: ans.feedback, updatedAt: new Date() })
|
||||
.where(eq(homeworkAnswers.id, ans.id))
|
||||
totalScore += ans.score
|
||||
}
|
||||
await db.transaction(async (tx) => {
|
||||
let totalScore = 0
|
||||
for (const ans of answers) {
|
||||
await tx
|
||||
.update(homeworkAnswers)
|
||||
.set({ score: ans.score, feedback: ans.feedback, updatedAt: new Date() })
|
||||
.where(eq(homeworkAnswers.id, ans.id))
|
||||
totalScore += ans.score
|
||||
}
|
||||
|
||||
await db
|
||||
.update(homeworkSubmissions)
|
||||
.set({ score: totalScore, status: "graded", updatedAt: new Date() })
|
||||
.where(eq(homeworkSubmissions.id, submissionId))
|
||||
await tx
|
||||
.update(homeworkSubmissions)
|
||||
.set({ score: totalScore, status: "graded", updatedAt: new Date() })
|
||||
.where(eq(homeworkSubmissions.id, submissionId))
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user