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:
@@ -26,6 +26,12 @@ import type {
|
||||
import { getClassHomeworkInsights } from "./data-access-stats"
|
||||
import { getClassSchedule } from "./data-access-schedule"
|
||||
|
||||
const isClassSubject = (v: unknown): v is ClassSubject =>
|
||||
typeof v === "string" && (DEFAULT_CLASS_SUBJECTS as readonly string[]).includes(v)
|
||||
|
||||
const toClassSubject = (v: string): ClassSubject | null =>
|
||||
isClassSubject(v) ? v : null
|
||||
|
||||
export const getSessionTeacherId = async (): Promise<string | null> => {
|
||||
const { auth } = await import("@/auth")
|
||||
const session = await auth()
|
||||
@@ -118,14 +124,44 @@ export const compareClassLike = (
|
||||
}
|
||||
|
||||
export const getAccessibleClassIdsForTeacher = async (teacherId: string): Promise<string[]> => {
|
||||
const ownedIds = await db.select({ id: classes.id }).from(classes).where(eq(classes.teacherId, teacherId))
|
||||
const assignedIds = await db
|
||||
.select({ id: classSubjectTeachers.classId })
|
||||
.from(classSubjectTeachers)
|
||||
.where(eq(classSubjectTeachers.teacherId, teacherId))
|
||||
const [ownedIds, assignedIds] = await Promise.all([
|
||||
db.select({ id: classes.id }).from(classes).where(eq(classes.teacherId, teacherId)),
|
||||
db
|
||||
.select({ id: classSubjectTeachers.classId })
|
||||
.from(classSubjectTeachers)
|
||||
.where(eq(classSubjectTeachers.teacherId, teacherId)),
|
||||
])
|
||||
return Array.from(new Set([...ownedIds.map((x) => x.id), ...assignedIds.map((x) => x.id)]))
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that a teacher owns a class (teacherId match on classes row).
|
||||
* Used by scheduling module to gate classSchedule writes.
|
||||
*/
|
||||
export async function verifyTeacherOwnsClass(classId: string, teacherId: string): Promise<boolean> {
|
||||
const [owned] = await db
|
||||
.select({ id: classes.id })
|
||||
.from(classes)
|
||||
.where(and(eq(classes.id, classId), eq(classes.teacherId, teacherId)))
|
||||
.limit(1)
|
||||
return Boolean(owned)
|
||||
}
|
||||
|
||||
export const getClassGradeIdsByClassIds = async (classIds: string[]): Promise<Map<string, string>> => {
|
||||
if (classIds.length === 0) return new Map()
|
||||
const rows = await db
|
||||
.select({ id: classes.id, gradeId: classes.gradeId })
|
||||
.from(classes)
|
||||
.where(inArray(classes.id, classIds))
|
||||
const map = new Map<string, string>()
|
||||
for (const row of rows) {
|
||||
if (typeof row.gradeId === "string" && row.gradeId.trim().length > 0) {
|
||||
map.set(row.id, row.gradeId)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
export const getTeacherSubjectIdsForClass = async (teacherId: string, classId: string): Promise<string[]> => {
|
||||
const rows = await db
|
||||
.select({ subjectId: classSubjectTeachers.subjectId })
|
||||
@@ -134,6 +170,178 @@ export const getTeacherSubjectIdsForClass = async (teacherId: string, classId: s
|
||||
return Array.from(new Set(rows.map((r) => String(r.subjectId))))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取班级的教师 ID(班主任)。
|
||||
* 供跨模块调用使用,避免直接查询 classes 表。
|
||||
*/
|
||||
export const getClassTeacherById = async (classId: string): Promise<string | null> => {
|
||||
const [row] = await db
|
||||
.select({ teacherId: classes.teacherId })
|
||||
.from(classes)
|
||||
.where(eq(classes.id, classId))
|
||||
.limit(1)
|
||||
return row?.teacherId ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取班级所有学生 ID(不限状态)。
|
||||
* 供跨模块调用使用,避免直接查询 classEnrollments 表。
|
||||
*/
|
||||
export const getStudentIdsByClassId = async (classId: string): Promise<string[]> => {
|
||||
const rows = await db
|
||||
.select({ studentId: classEnrollments.studentId })
|
||||
.from(classEnrollments)
|
||||
.where(eq(classEnrollments.classId, classId))
|
||||
return rows.map((r) => r.studentId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个班级的所有学生 ID(不限状态)。
|
||||
* 供跨模块调用使用,避免直接查询 classEnrollments 表。
|
||||
*/
|
||||
export const getStudentIdsByClassIds = async (classIds: string[]): Promise<string[]> => {
|
||||
if (classIds.length === 0) return []
|
||||
const rows = await db
|
||||
.select({ studentId: classEnrollments.studentId })
|
||||
.from(classEnrollments)
|
||||
.where(inArray(classEnrollments.classId, classIds))
|
||||
return Array.from(new Set(rows.map((r) => r.studentId)))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取班级所有活跃学生 ID(status = 'active')。
|
||||
* 供跨模块调用使用,避免直接查询 classEnrollments 表。
|
||||
*/
|
||||
export const getActiveStudentIdsByClassId = async (classId: string): Promise<string[]> => {
|
||||
const rows = await db
|
||||
.select({ studentId: classEnrollments.studentId })
|
||||
.from(classEnrollments)
|
||||
.where(and(eq(classEnrollments.classId, classId), eq(classEnrollments.status, "active")))
|
||||
return rows.map((r) => r.studentId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取教师在一个班级所教的科目 ID 列表。
|
||||
* 参数顺序为 (classId, teacherId),供跨模块调用使用。
|
||||
*/
|
||||
export const getTeacherSubjectIdsByClass = async (classId: string, teacherId: string): Promise<string[]> => {
|
||||
return getTeacherSubjectIdsForClass(teacherId, classId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学生当前活跃班级的 ID。
|
||||
* 供跨模块调用使用,避免直接查询 classEnrollments 表。
|
||||
*/
|
||||
export const getStudentActiveClassId = async (studentId: string): Promise<string | null> => {
|
||||
const [row] = await db
|
||||
.select({ classId: classEnrollments.classId })
|
||||
.from(classEnrollments)
|
||||
.where(and(eq(classEnrollments.studentId, studentId), eq(classEnrollments.status, "active")))
|
||||
.orderBy(asc(classEnrollments.createdAt))
|
||||
.limit(1)
|
||||
return row?.classId ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取学生当前活跃班级对应的年级 ID。
|
||||
* 供跨模块调用使用,避免直接查询 classEnrollments/classes 表。
|
||||
*/
|
||||
export const getStudentActiveGradeId = async (studentId: string): Promise<string | null> => {
|
||||
const [row] = await db
|
||||
.select({ gradeId: classes.gradeId })
|
||||
.from(classEnrollments)
|
||||
.innerJoin(classes, eq(classes.id, classEnrollments.classId))
|
||||
.where(and(eq(classEnrollments.studentId, studentId), eq(classEnrollments.status, "active")))
|
||||
.orderBy(asc(classEnrollments.createdAt))
|
||||
.limit(1)
|
||||
return row?.gradeId ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验班级是否存在。
|
||||
* 供跨模块调用使用,避免直接查询 classes 表。
|
||||
*/
|
||||
export const getClassExists = async (classId: string): Promise<boolean> => {
|
||||
const [row] = await db
|
||||
.select({ id: classes.id })
|
||||
.from(classes)
|
||||
.where(eq(classes.id, classId))
|
||||
.limit(1)
|
||||
return Boolean(row)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取班级名称。
|
||||
* 供跨模块调用使用,避免直接查询 classes 表。
|
||||
*/
|
||||
export const getClassNameById = async (classId: string): Promise<string | null> => {
|
||||
const [row] = await db
|
||||
.select({ name: classes.name })
|
||||
.from(classes)
|
||||
.where(eq(classes.id, classId))
|
||||
.limit(1)
|
||||
return row?.name ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取班级关联的年级 ID。
|
||||
* 供跨模块调用使用,避免直接查询 classes 表。
|
||||
*/
|
||||
export const getClassGradeId = async (classId: string): Promise<string | null> => {
|
||||
const [row] = await db
|
||||
.select({ gradeId: classes.gradeId })
|
||||
.from(classes)
|
||||
.where(eq(classes.id, classId))
|
||||
.limit(1)
|
||||
return row?.gradeId ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多个班级关联的年级 ID 列表(去重,过滤空值)。
|
||||
* 供跨模块调用使用,避免直接查询 classes 表。
|
||||
*/
|
||||
export const getGradeIdsByClassIds = async (classIds: string[]): Promise<string[]> => {
|
||||
if (classIds.length === 0) return []
|
||||
const rows = await db
|
||||
.selectDistinct({ gradeId: classes.gradeId })
|
||||
.from(classes)
|
||||
.where(inArray(classes.id, classIds))
|
||||
return rows
|
||||
.map((r) => r.gradeId)
|
||||
.filter((id): id is string => typeof id === "string" && id.length > 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取班级名称(Map<classId, name>)。
|
||||
* 供跨模块调用使用,避免直接查询 classes 表。
|
||||
*/
|
||||
export const getClassNamesByIds = async (classIds: string[]): Promise<Map<string, string>> => {
|
||||
const result = new Map<string, string>()
|
||||
const uniqueIds = Array.from(new Set(classIds.filter((v): v is string => typeof v === "string" && v.length > 0)))
|
||||
if (uniqueIds.length === 0) return result
|
||||
|
||||
const rows = await db
|
||||
.select({ id: classes.id, name: classes.name })
|
||||
.from(classes)
|
||||
.where(inArray(classes.id, uniqueIds))
|
||||
|
||||
for (const r of rows) result.set(r.id, r.name)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定年级下的所有班级(id + name)。
|
||||
* 供跨模块调用使用,避免直接查询 classes 表。
|
||||
*/
|
||||
export const getClassesByGradeId = async (gradeId: string): Promise<Array<{ id: string; name: string }>> => {
|
||||
if (!gradeId) return []
|
||||
const rows = await db
|
||||
.select({ id: classes.id, name: classes.name })
|
||||
.from(classes)
|
||||
.where(eq(classes.gradeId, gradeId))
|
||||
return rows.map((r) => ({ id: r.id, name: r.name }))
|
||||
}
|
||||
|
||||
export const getTeacherClasses = cache(async (params?: { teacherId?: string }): Promise<TeacherClass[]> => {
|
||||
const teacherId = params?.teacherId ?? (await getSessionTeacherId())
|
||||
if (!teacherId) return []
|
||||
@@ -237,8 +445,8 @@ export const getTeacherTeachingSubjects = cache(async (): Promise<ClassSubject[]
|
||||
.orderBy(asc(subjects.name))
|
||||
|
||||
return rows
|
||||
.map((r) => r.subject as ClassSubject)
|
||||
.filter((s) => DEFAULT_CLASS_SUBJECTS.includes(s))
|
||||
.map((r) => toClassSubject(r.subject))
|
||||
.filter((s): s is ClassSubject => s !== null)
|
||||
})
|
||||
|
||||
export async function createTeacherClass(data: CreateTeacherClassInput): Promise<string> {
|
||||
@@ -263,7 +471,11 @@ export async function createTeacherClass(data: CreateTeacherClassInput): Promise
|
||||
.select({ id: subjects.id, name: subjects.name })
|
||||
.from(subjects)
|
||||
.where(inArray(subjects.name, DEFAULT_CLASS_SUBJECTS))
|
||||
const idByName = new Map(subjectRows.map((r) => [r.name as ClassSubject, r.id]))
|
||||
const idByName = new Map<ClassSubject, string>()
|
||||
for (const r of subjectRows) {
|
||||
const subject = toClassSubject(r.name)
|
||||
if (subject) idByName.set(subject, r.id)
|
||||
}
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(classes).values({
|
||||
@@ -279,13 +491,11 @@ export async function createTeacherClass(data: CreateTeacherClassInput): Promise
|
||||
teacherId,
|
||||
})
|
||||
|
||||
const values = DEFAULT_CLASS_SUBJECTS
|
||||
.filter((name) => idByName.has(name))
|
||||
.map((name) => ({
|
||||
classId: id,
|
||||
subjectId: idByName.get(name)!,
|
||||
teacherId: null,
|
||||
}))
|
||||
const values = DEFAULT_CLASS_SUBJECTS.flatMap((name) => {
|
||||
const subjectId = idByName.get(name)
|
||||
if (!subjectId) return []
|
||||
return [{ classId: id, subjectId, teacherId: null }]
|
||||
})
|
||||
await tx.insert(classSubjectTeachers).values(values)
|
||||
})
|
||||
return id
|
||||
@@ -295,8 +505,6 @@ export async function createTeacherClass(data: CreateTeacherClassInput): Promise
|
||||
}
|
||||
}
|
||||
throw new Error("Failed to create class")
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
export async function ensureClassInvitationCode(classId: string): Promise<string> {
|
||||
@@ -558,15 +766,17 @@ export async function setClassSubjectTeachers(params: {
|
||||
.select({ id: subjects.id, name: subjects.name })
|
||||
.from(subjects)
|
||||
.where(inArray(subjects.name, DEFAULT_CLASS_SUBJECTS))
|
||||
const idByName = new Map(subjectRows.map((r) => [r.name as ClassSubject, r.id]))
|
||||
const idByName = new Map<ClassSubject, string>()
|
||||
for (const r of subjectRows) {
|
||||
const subject = toClassSubject(r.name)
|
||||
if (subject) idByName.set(subject, r.id)
|
||||
}
|
||||
|
||||
const values = DEFAULT_CLASS_SUBJECTS
|
||||
.filter((name) => idByName.has(name))
|
||||
.map((name) => ({
|
||||
classId,
|
||||
subjectId: idByName.get(name)!,
|
||||
teacherId: teacherBySubject.get(name) ?? null,
|
||||
}))
|
||||
const values = DEFAULT_CLASS_SUBJECTS.flatMap((name) => {
|
||||
const subjectId = idByName.get(name)
|
||||
if (!subjectId) return []
|
||||
return [{ classId, subjectId, teacherId: teacherBySubject.get(name) ?? null }]
|
||||
})
|
||||
|
||||
await db
|
||||
.insert(classSubjectTeachers)
|
||||
|
||||
Reference in New Issue
Block a user