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.
160 lines
5.0 KiB
TypeScript
160 lines
5.0 KiB
TypeScript
import "server-only"
|
|
|
|
import { eq } from "drizzle-orm"
|
|
|
|
import { db } from "@/shared/db"
|
|
import { classSchedule } from "@/shared/db/schema"
|
|
import {
|
|
getTeacherIdForMutations,
|
|
verifyTeacherOwnsClass,
|
|
} from "@/modules/classes/data-access"
|
|
import {
|
|
insertClassScheduleItem,
|
|
updateClassScheduleItemById,
|
|
deleteClassScheduleItemById,
|
|
} from "./data-access"
|
|
import type {
|
|
CreateClassScheduleItemInput,
|
|
UpdateClassScheduleItemInput,
|
|
} from "./types"
|
|
|
|
const isTimeHHMM = (v: string): boolean => /^\d{2}:\d{2}$/.test(v)
|
|
|
|
/**
|
|
* Create a single classSchedule item.
|
|
* Ownership: the caller (teacher) must own the target class.
|
|
* DB write is delegated to the unified scheduling write entry point.
|
|
*/
|
|
export async function createClassScheduleItem(
|
|
data: CreateClassScheduleItemInput,
|
|
): Promise<string> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
|
|
const classId = data.classId.trim()
|
|
const course = data.course.trim()
|
|
const startTime = data.startTime.trim()
|
|
const endTime = data.endTime.trim()
|
|
const location = data.location?.trim() || null
|
|
const weekday = data.weekday
|
|
|
|
if (!classId) throw new Error("Class is required")
|
|
if (!course) throw new Error("Course is required")
|
|
if (!isTimeHHMM(startTime) || !isTimeHHMM(endTime)) throw new Error("Invalid time format")
|
|
if (startTime >= endTime) throw new Error("Start time must be earlier than end time")
|
|
if (weekday < 1 || weekday > 7) throw new Error("Invalid weekday")
|
|
|
|
const owned = await verifyTeacherOwnsClass(classId, teacherId)
|
|
if (!owned) throw new Error("Class not found")
|
|
|
|
return insertClassScheduleItem({
|
|
classId,
|
|
weekday,
|
|
startTime,
|
|
endTime,
|
|
course,
|
|
location,
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Update a classSchedule item by id.
|
|
* Ownership: the teacher must own the class associated with the schedule item
|
|
* (and the target class when classId is being changed).
|
|
*/
|
|
export async function updateClassScheduleItem(
|
|
scheduleId: string,
|
|
data: UpdateClassScheduleItemInput,
|
|
): Promise<void> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
const id = scheduleId.trim()
|
|
if (!id) throw new Error("Missing schedule id")
|
|
|
|
const [existing] = await db
|
|
.select({
|
|
id: classSchedule.id,
|
|
classId: classSchedule.classId,
|
|
startTime: classSchedule.startTime,
|
|
endTime: classSchedule.endTime,
|
|
})
|
|
.from(classSchedule)
|
|
.where(eq(classSchedule.id, id))
|
|
.limit(1)
|
|
|
|
if (!existing) throw new Error("Schedule item not found")
|
|
|
|
const ownedExisting = await verifyTeacherOwnsClass(existing.classId, teacherId)
|
|
if (!ownedExisting) throw new Error("Schedule item not found")
|
|
|
|
const update: Partial<typeof classSchedule.$inferSelect> = {}
|
|
|
|
if (typeof data.classId === "string") {
|
|
const nextClassId = data.classId.trim()
|
|
if (!nextClassId) throw new Error("Class is required")
|
|
|
|
const ownedNext = await verifyTeacherOwnsClass(nextClassId, teacherId)
|
|
if (!ownedNext) throw new Error("Class not found")
|
|
update.classId = nextClassId
|
|
}
|
|
|
|
if (typeof data.weekday === "number") {
|
|
if (data.weekday < 1 || data.weekday > 7) throw new Error("Invalid weekday")
|
|
update.weekday = data.weekday
|
|
}
|
|
|
|
if (typeof data.course === "string") {
|
|
const course = data.course.trim()
|
|
if (!course) throw new Error("Course is required")
|
|
update.course = course
|
|
}
|
|
|
|
const nextStart = typeof data.startTime === "string" ? data.startTime.trim() : undefined
|
|
const nextEnd = typeof data.endTime === "string" ? data.endTime.trim() : undefined
|
|
if (nextStart !== undefined) {
|
|
if (!isTimeHHMM(nextStart)) throw new Error("Invalid time format")
|
|
update.startTime = nextStart
|
|
}
|
|
if (nextEnd !== undefined) {
|
|
if (!isTimeHHMM(nextEnd)) throw new Error("Invalid time format")
|
|
update.endTime = nextEnd
|
|
}
|
|
|
|
if (update.startTime !== undefined || update.endTime !== undefined) {
|
|
const mergedStart = update.startTime ?? existing.startTime
|
|
const mergedEnd = update.endTime ?? existing.endTime
|
|
if (typeof mergedStart === "string" && typeof mergedEnd === "string" && mergedStart >= mergedEnd) {
|
|
throw new Error("Start time must be earlier than end time")
|
|
}
|
|
}
|
|
|
|
if (data.location !== undefined) {
|
|
update.location = data.location?.trim() || null
|
|
}
|
|
|
|
if (Object.keys(update).length === 0) return
|
|
|
|
await updateClassScheduleItemById(id, update)
|
|
}
|
|
|
|
/**
|
|
* Delete a classSchedule item by id.
|
|
* Ownership: the teacher must own the class associated with the schedule item.
|
|
*/
|
|
export async function deleteClassScheduleItem(scheduleId: string): Promise<void> {
|
|
const teacherId = await getTeacherIdForMutations()
|
|
const id = scheduleId.trim()
|
|
if (!id) throw new Error("Missing schedule id")
|
|
|
|
const [existing] = await db
|
|
.select({ id: classSchedule.id, classId: classSchedule.classId })
|
|
.from(classSchedule)
|
|
.where(eq(classSchedule.id, id))
|
|
.limit(1)
|
|
|
|
if (!existing) throw new Error("Schedule item not found")
|
|
|
|
const owned = await verifyTeacherOwnsClass(existing.classId, teacherId)
|
|
if (!owned) throw new Error("Schedule item not found")
|
|
|
|
await deleteClassScheduleItemById(id)
|
|
}
|