import { mysqlTable, varchar, text, timestamp, int, primaryKey, index, json, mysqlEnum, boolean, foreignKey } from "drizzle-orm/mysql-core"; import { createId } from "@paralleldrive/cuid2"; import type { AdapterAccountType } from "next-auth/adapters"; // --- Helper for ID generation (CUID2) --- const id = (name: string) => varchar(name, { length: 128 }).notNull().$defaultFn(() => createId()); // --- 1. Users & Auth (Auth.js v5 Standard + RBAC) --- export const users = mysqlTable("users", { id: id("id").primaryKey(), name: varchar("name", { length: 255 }), email: varchar("email", { length: 255 }).notNull().unique(), emailVerified: timestamp("emailVerified", { mode: "date" }), image: varchar("image", { length: 255 }), // Custom Role Field for RBAC (Default Role) role: varchar("role", { length: 50 }).default("student"), // Credentials Auth (Optional) password: varchar("password", { length: 255 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ emailIdx: index("email_idx").on(table.email), })); // Auth.js: Accounts (OAuth providers) export const accounts = mysqlTable("accounts", { userId: varchar("userId", { length: 128 }) .notNull() .references(() => users.id, { onDelete: "cascade" }), type: varchar("type", { length: 255 }).$type().notNull(), provider: varchar("provider", { length: 255 }).notNull(), providerAccountId: varchar("providerAccountId", { length: 255 }).notNull(), refresh_token: text("refresh_token"), access_token: text("access_token"), expires_at: int("expires_at"), token_type: varchar("token_type", { length: 255 }), scope: varchar("scope", { length: 255 }), id_token: text("id_token"), session_state: varchar("session_state", { length: 255 }), }, (account) => ({ compoundKey: primaryKey({ columns: [account.provider, account.providerAccountId], }), userIdIdx: index("account_userId_idx").on(account.userId), }) ); // Auth.js: Sessions export const sessions = mysqlTable("sessions", { sessionToken: varchar("sessionToken", { length: 255 }).primaryKey(), userId: varchar("userId", { length: 128 }) .notNull() .references(() => users.id, { onDelete: "cascade" }), expires: timestamp("expires", { mode: "date" }).notNull(), }, (table) => ({ userIdIdx: index("session_userId_idx").on(table.userId), })); // Auth.js: Verification Tokens export const verificationTokens = mysqlTable("verificationTokens", { identifier: varchar("identifier", { length: 255 }).notNull(), token: varchar("token", { length: 255 }).notNull(), expires: timestamp("expires", { mode: "date" }).notNull(), }, (vt) => ({ compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }), })); // --- Custom RBAC Extensions --- export const roles = mysqlTable("roles", { id: id("id").primaryKey(), name: varchar("name", { length: 50 }).notNull().unique(), // e.g., 'admin', 'teacher', 'student', 'grade_head' description: varchar("description", { length: 255 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }); // Many-to-Many: Users <-> Roles // Solves: "A teacher can also be a Grade Head" export const usersToRoles = mysqlTable("users_to_roles", { userId: varchar("user_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }), roleId: varchar("role_id", { length: 128 }).notNull().references(() => roles.id, { onDelete: "cascade" }), }, (table) => ({ pk: primaryKey({ columns: [table.userId, table.roleId] }), userIdIdx: index("user_id_idx").on(table.userId), })); // --- 2. Knowledge Points (Tree Structure) --- export const knowledgePoints = mysqlTable("knowledge_points", { id: id("id").primaryKey(), name: varchar("name", { length: 255 }).notNull(), description: text("description"), // Tree Structure: Parent KP parentId: varchar("parent_id", { length: 128 }), // Self-reference defined in relations chapterId: varchar("chapter_id", { length: 128 }), // Metadata for ordering or level level: int("level").default(0), order: int("order").default(0), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ parentIdIdx: index("parent_id_idx").on(table.parentId), chapterIdIdx: index("kp_chapter_id_idx").on(table.chapterId), })); // --- 3. Question Bank (Core) --- export const questionTypeEnum = mysqlEnum("type", ["single_choice", "multiple_choice", "text", "judgment", "composite"]); export const questions = mysqlTable("questions", { id: id("id").primaryKey(), // Content can be JSON to store rich text, images, etc. or just text. // Using JSON for flexibility in a modern ed-tech app (e.g. SlateJS nodes). content: json("content").notNull(), type: questionTypeEnum.notNull(), difficulty: int("difficulty").default(1), // 1-5 // Self-reference for "Infinite Nesting" (Parent Question) // e.g., A reading comprehension passage (Parent) -> 5 sub-questions (Children) parentId: varchar("parent_id", { length: 128 }), authorId: varchar("author_id", { length: 128 }).notNull().references(() => users.id), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ parentIdIdx: index("parent_id_idx").on(table.parentId), // Critical for querying children authorIdIdx: index("author_id_idx").on(table.authorId), // In a real large-scale system, we might add Full-Text Search index on content })); // Many-to-Many: Questions <-> Knowledge Points export const questionsToKnowledgePoints = mysqlTable("questions_to_knowledge_points", { questionId: varchar("question_id", { length: 128 }).notNull(), knowledgePointId: varchar("knowledge_point_id", { length: 128 }).notNull(), }, (table) => ({ pk: primaryKey({ columns: [table.questionId, table.knowledgePointId] }), kpIdx: index("kp_idx").on(table.knowledgePointId), // For querying "All questions under this KP" qFk: foreignKey({ columns: [table.questionId], foreignColumns: [questions.id], name: "q_kp_qid_fk" }).onDelete("cascade"), kpFk: foreignKey({ columns: [table.knowledgePointId], foreignColumns: [knowledgePoints.id], name: "q_kp_kpid_fk" }).onDelete("cascade"), })); // --- 4. Academic / Teaching Flow --- export const textbooks = mysqlTable("textbooks", { id: id("id").primaryKey(), title: varchar("title", { length: 255 }).notNull(), subject: varchar("subject", { length: 100 }).notNull(), grade: varchar("grade", { length: 50 }), publisher: varchar("publisher", { length: 100 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }); export const chapters = mysqlTable("chapters", { id: id("id").primaryKey(), textbookId: varchar("textbook_id", { length: 128 }).notNull().references(() => textbooks.id, { onDelete: "cascade" }), title: varchar("title", { length: 255 }).notNull(), order: int("order").default(0), // Chapters can also be nested parentId: varchar("parent_id", { length: 128 }), content: text("content"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ textbookIdx: index("textbook_idx").on(table.textbookId), parentIdIdx: index("parent_id_idx").on(table.parentId), })); // --- 5. School Management --- export const departments = mysqlTable("departments", { id: id("id").primaryKey(), name: varchar("name", { length: 255 }).notNull().unique(), description: text("description"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ nameIdx: index("departments_name_idx").on(table.name), })) export const classrooms = mysqlTable("classrooms", { id: id("id").primaryKey(), name: varchar("name", { length: 255 }).notNull().unique(), building: varchar("building", { length: 100 }), floor: int("floor"), capacity: int("capacity"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ nameIdx: index("classrooms_name_idx").on(table.name), })) export const academicYears = mysqlTable("academic_years", { id: id("id").primaryKey(), name: varchar("name", { length: 100 }).notNull().unique(), startDate: timestamp("start_date", { mode: "date" }).notNull(), endDate: timestamp("end_date", { mode: "date" }).notNull(), isActive: boolean("is_active").default(false).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ nameIdx: index("academic_years_name_idx").on(table.name), activeIdx: index("academic_years_active_idx").on(table.isActive), })) export const schools = mysqlTable("schools", { id: id("id").primaryKey(), name: varchar("name", { length: 255 }).notNull().unique(), code: varchar("code", { length: 50 }).unique(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ nameIdx: index("schools_name_idx").on(table.name), codeIdx: index("schools_code_idx").on(table.code), })) export const grades = mysqlTable("grades", { id: id("id").primaryKey(), schoolId: varchar("school_id", { length: 128 }).notNull(), name: varchar("name", { length: 100 }).notNull(), order: int("order").default(0).notNull(), gradeHeadId: varchar("grade_head_id", { length: 128 }), teachingHeadId: varchar("teaching_head_id", { length: 128 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ schoolIdx: index("grades_school_idx").on(table.schoolId), schoolNameUnique: index("grades_school_name_uniq").on(table.schoolId, table.name), gradeHeadIdx: index("grades_grade_head_idx").on(table.gradeHeadId), teachingHeadIdx: index("grades_teaching_head_idx").on(table.teachingHeadId), schoolFk: foreignKey({ columns: [table.schoolId], foreignColumns: [schools.id], name: "g_s_fk", }).onDelete("cascade"), gradeHeadFk: foreignKey({ columns: [table.gradeHeadId], foreignColumns: [users.id], name: "g_gh_fk", }).onDelete("set null"), teachingHeadFk: foreignKey({ columns: [table.teachingHeadId], foreignColumns: [users.id], name: "g_th_fk", }).onDelete("set null"), })) // --- 6. Classes / Enrollment / Schedule --- export const classEnrollmentStatusEnum = mysqlEnum("class_enrollment_status", ["active", "inactive"]); export const classes = mysqlTable("classes", { id: id("id").primaryKey(), schoolName: varchar("school_name", { length: 255 }), schoolId: varchar("school_id", { length: 128 }), name: varchar("name", { length: 255 }).notNull(), grade: varchar("grade", { length: 50 }).notNull(), gradeId: varchar("grade_id", { length: 128 }), homeroom: varchar("homeroom", { length: 50 }), room: varchar("room", { length: 50 }), invitationCode: varchar("invitation_code", { length: 6 }).unique(), teacherId: varchar("teacher_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ teacherIdx: index("classes_teacher_idx").on(table.teacherId), gradeIdx: index("classes_grade_idx").on(table.grade), schoolIdx: index("classes_school_idx").on(table.schoolId), gradeIdIdx: index("classes_grade_id_idx").on(table.gradeId), schoolFk: foreignKey({ columns: [table.schoolId], foreignColumns: [schools.id], name: "c_s_fk", }).onDelete("set null"), gradeFk: foreignKey({ columns: [table.gradeId], foreignColumns: [grades.id], name: "c_g_fk", }).onDelete("set null"), })); export const classSubjectEnum = mysqlEnum("subject", ["语文", "数学", "英语", "美术", "体育", "科学", "社会", "音乐"]); export const classSubjectTeachers = mysqlTable("class_subject_teachers", { classId: varchar("class_id", { length: 128 }).notNull(), subject: classSubjectEnum.notNull(), teacherId: varchar("teacher_id", { length: 128 }).references(() => users.id, { onDelete: "set null" }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ pk: primaryKey({ columns: [table.classId, table.subject] }), classIdx: index("class_subject_teachers_class_idx").on(table.classId), teacherIdx: index("class_subject_teachers_teacher_idx").on(table.teacherId), classFk: foreignKey({ columns: [table.classId], foreignColumns: [classes.id], name: "cst_c_fk", }).onDelete("cascade"), })); export const classEnrollments = mysqlTable("class_enrollments", { classId: varchar("class_id", { length: 128 }).notNull(), studentId: varchar("student_id", { length: 128 }).notNull(), status: classEnrollmentStatusEnum.default("active").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }, (table) => ({ pk: primaryKey({ columns: [table.classId, table.studentId] }), classIdx: index("class_enrollments_class_idx").on(table.classId), studentIdx: index("class_enrollments_student_idx").on(table.studentId), classFk: foreignKey({ columns: [table.classId], foreignColumns: [classes.id], name: "ce_c_fk", }).onDelete("cascade"), studentFk: foreignKey({ columns: [table.studentId], foreignColumns: [users.id], name: "ce_s_fk", }).onDelete("cascade"), })); export const classSchedule = mysqlTable("class_schedule", { id: id("id").primaryKey(), classId: varchar("class_id", { length: 128 }).notNull(), weekday: int("weekday").notNull(), startTime: varchar("start_time", { length: 5 }).notNull(), endTime: varchar("end_time", { length: 5 }).notNull(), course: varchar("course", { length: 255 }).notNull(), location: varchar("location", { length: 100 }), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ classIdx: index("class_schedule_class_idx").on(table.classId), classDayIdx: index("class_schedule_class_day_idx").on(table.classId, table.weekday), classFk: foreignKey({ columns: [table.classId], foreignColumns: [classes.id], name: "cs_c_fk", }).onDelete("cascade"), })); export const exams = mysqlTable("exams", { id: id("id").primaryKey(), title: varchar("title", { length: 255 }).notNull(), description: text("description"), /** * Stores the hierarchical structure of the exam. * Expected JSON Schema: * type ExamStructure = Array< * | { type: 'group', title: string, children: ExamStructure } * | { type: 'question', questionId: string, score: number } * > * * Note: This is for UI presentation/ordering. * Real relational integrity is maintained in 'exam_questions' table. */ structure: json("structure"), creatorId: varchar("creator_id", { length: 128 }).notNull().references(() => users.id), startTime: timestamp("start_time"), endTime: timestamp("end_time"), // Status: draft, published, ongoing, finished status: varchar("status", { length: 50 }).default("draft"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }); // Linking Exams to Questions (Many-to-Many often, or One-to-Many if specific to exam) // Usually questions are reused, so Many-to-Many export const examQuestions = mysqlTable("exam_questions", { examId: varchar("exam_id", { length: 128 }).notNull().references(() => exams.id, { onDelete: "cascade" }), questionId: varchar("question_id", { length: 128 }).notNull().references(() => questions.id, { onDelete: "cascade" }), score: int("score").default(0), order: int("order").default(0), }, (table) => ({ pk: primaryKey({ columns: [table.examId, table.questionId] }), })); export const examSubmissions = mysqlTable("exam_submissions", { id: id("id").primaryKey(), examId: varchar("exam_id", { length: 128 }).notNull().references(() => exams.id, { onDelete: "cascade" }), studentId: varchar("student_id", { length: 128 }).notNull().references(() => users.id, { onDelete: "cascade" }), score: int("score"), // Total score status: varchar("status", { length: 50 }).default("started"), // started, submitted, graded submittedAt: timestamp("submitted_at"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ examStudentIdx: index("exam_student_idx").on(table.examId, table.studentId), })); export const submissionAnswers = mysqlTable("submission_answers", { id: id("id").primaryKey(), submissionId: varchar("submission_id", { length: 128 }).notNull().references(() => examSubmissions.id, { onDelete: "cascade" }), questionId: varchar("question_id", { length: 128 }).notNull().references(() => questions.id), answerContent: json("answer_content"), // Student's answer score: int("score"), // Score for this specific question feedback: text("feedback"), // Teacher's feedback createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ submissionIdx: index("submission_idx").on(table.submissionId), })); export const homeworkAssignments = mysqlTable("homework_assignments", { id: id("id").primaryKey(), sourceExamId: varchar("source_exam_id", { length: 128 }).notNull(), title: varchar("title", { length: 255 }).notNull(), description: text("description"), structure: json("structure"), status: varchar("status", { length: 50 }).default("draft"), creatorId: varchar("creator_id", { length: 128 }).notNull(), availableAt: timestamp("available_at"), dueAt: timestamp("due_at"), allowLate: boolean("allow_late").default(false).notNull(), lateDueAt: timestamp("late_due_at"), maxAttempts: int("max_attempts").default(1).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ creatorIdx: index("hw_assignment_creator_idx").on(table.creatorId), sourceExamIdx: index("hw_assignment_source_exam_idx").on(table.sourceExamId), statusIdx: index("hw_assignment_status_idx").on(table.status), sourceExamFk: foreignKey({ columns: [table.sourceExamId], foreignColumns: [exams.id], name: "hw_asg_exam_fk", }).onDelete("cascade"), creatorFk: foreignKey({ columns: [table.creatorId], foreignColumns: [users.id], name: "hw_asg_creator_fk", }).onDelete("cascade"), })); export const homeworkAssignmentQuestions = mysqlTable("homework_assignment_questions", { assignmentId: varchar("assignment_id", { length: 128 }).notNull(), questionId: varchar("question_id", { length: 128 }).notNull(), score: int("score").default(0), order: int("order").default(0), }, (table) => ({ pk: primaryKey({ columns: [table.assignmentId, table.questionId] }), assignmentIdx: index("hw_assignment_questions_assignment_idx").on(table.assignmentId), assignmentFk: foreignKey({ columns: [table.assignmentId], foreignColumns: [homeworkAssignments.id], name: "hw_aq_a_fk", }).onDelete("cascade"), questionFk: foreignKey({ columns: [table.questionId], foreignColumns: [questions.id], name: "hw_aq_q_fk", }).onDelete("cascade"), })); export const homeworkAssignmentTargets = mysqlTable("homework_assignment_targets", { assignmentId: varchar("assignment_id", { length: 128 }).notNull(), studentId: varchar("student_id", { length: 128 }).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }, (table) => ({ pk: primaryKey({ columns: [table.assignmentId, table.studentId] }), assignmentIdx: index("hw_assignment_targets_assignment_idx").on(table.assignmentId), studentIdx: index("hw_assignment_targets_student_idx").on(table.studentId), assignmentFk: foreignKey({ columns: [table.assignmentId], foreignColumns: [homeworkAssignments.id], name: "hw_at_a_fk", }).onDelete("cascade"), studentFk: foreignKey({ columns: [table.studentId], foreignColumns: [users.id], name: "hw_at_s_fk", }).onDelete("cascade"), })); export const homeworkSubmissions = mysqlTable("homework_submissions", { id: id("id").primaryKey(), assignmentId: varchar("assignment_id", { length: 128 }).notNull(), studentId: varchar("student_id", { length: 128 }).notNull(), attemptNo: int("attempt_no").default(1).notNull(), score: int("score"), status: varchar("status", { length: 50 }).default("started"), startedAt: timestamp("started_at").defaultNow().notNull(), submittedAt: timestamp("submitted_at"), isLate: boolean("is_late").default(false).notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ assignmentStudentIdx: index("hw_assignment_student_idx").on(table.assignmentId, table.studentId), assignmentFk: foreignKey({ columns: [table.assignmentId], foreignColumns: [homeworkAssignments.id], name: "hw_sub_a_fk", }).onDelete("cascade"), studentFk: foreignKey({ columns: [table.studentId], foreignColumns: [users.id], name: "hw_sub_student_fk", }).onDelete("cascade"), })); export const homeworkAnswers = mysqlTable("homework_answers", { id: id("id").primaryKey(), submissionId: varchar("submission_id", { length: 128 }).notNull(), questionId: varchar("question_id", { length: 128 }).notNull(), answerContent: json("answer_content"), score: int("score"), feedback: text("feedback"), createdAt: timestamp("created_at").defaultNow().notNull(), updatedAt: timestamp("updated_at").defaultNow().onUpdateNow().notNull(), }, (table) => ({ submissionIdx: index("hw_answer_submission_idx").on(table.submissionId), submissionQuestionIdx: index("hw_answer_submission_question_idx").on(table.submissionId, table.questionId), submissionFk: foreignKey({ columns: [table.submissionId], foreignColumns: [homeworkSubmissions.id], name: "hw_ans_sub_fk", }).onDelete("cascade"), questionFk: foreignKey({ columns: [table.questionId], foreignColumns: [questions.id], name: "hw_ans_q_fk", }), })); // Re-export old courses table if needed or deprecate it. // Assuming we are replacing the old simple schema with this robust one. // But if there were existing tables, we might keep them or comment them out. // For this task, I will overwrite completely as this is a "System Architect" redesign.