import "dotenv/config"; import { db } from "../src/shared/db"; import { users, roles, usersToRoles, questions, knowledgePoints, questionsToKnowledgePoints, exams, examQuestions, homeworkAssignments, homeworkAssignmentQuestions, homeworkAssignmentTargets, homeworkSubmissions, homeworkAnswers, textbooks, chapters, schools, grades, classes, classEnrollments, classSchedule } from "../src/shared/db/schema"; import { createId } from "@paralleldrive/cuid2"; import { faker } from "@faker-js/faker"; import { sql } from "drizzle-orm"; /** * Enterprise-Grade Seed Script for Next_Edu * * Scenarios Covered: * 1. IAM: RBAC with multiple roles (Teacher & Grade Head). * 2. Knowledge Graph: Nested Knowledge Points (Math -> Algebra -> Linear Equations). * 3. Question Bank: Rich Text Content & Nested Questions (Reading Comprehension). * 4. Exams: JSON Structure for Sectioning. */ async function seed() { console.log("🌱 Starting Database Seed..."); const start = performance.now(); // --- 0. Cleanup (Optional: Truncate tables for fresh start) --- // Note: Order matters due to foreign keys if checks are enabled. // Ideally, use: SET FOREIGN_KEY_CHECKS = 0; try { await db.execute(sql`SET FOREIGN_KEY_CHECKS = 0;`); const tables = [ "class_schedule", "class_enrollments", "classes", "homework_answers", "homework_submissions", "homework_assignment_targets", "homework_assignment_questions", "homework_assignments", "submission_answers", "exam_submissions", "exam_questions", "exams", "questions_to_knowledge_points", "questions", "knowledge_points", "chapters", "textbooks", "grades", "schools", "users_to_roles", "roles", "users", "accounts", "sessions" ]; for (const table of tables) { await db.execute(sql.raw(`TRUNCATE TABLE \`${table}\`;`)); } await db.execute(sql`SET FOREIGN_KEY_CHECKS = 1;`); console.log("🧹 Cleaned up existing data."); } catch (e) { console.warn("⚠️ Cleanup warning (might be fresh DB):", e); } // --- 1. IAM & Roles --- console.log("👤 Seeding IAM..."); // Roles const roleMap = { admin: "role_admin", teacher: "role_teacher", student: "role_student", grade_head: "role_grade_head", teaching_head: "role_teaching_head" }; await db.insert(roles).values([ { id: roleMap.admin, name: "admin", description: "System Administrator" }, { id: roleMap.teacher, name: "teacher", description: "Academic Instructor" }, { id: roleMap.student, name: "student", description: "Learner" }, { id: roleMap.grade_head, name: "grade_head", description: "Head of Grade Year" }, { id: roleMap.teaching_head, name: "teaching_head", description: "Teaching Research Lead" } ]); // Users const usersData = [ { id: "user_admin", name: "Admin User", email: "admin@next-edu.com", role: "admin", // Legacy field image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Admin" }, { id: "user_teacher_math", name: "Mr. Math", email: "math@next-edu.com", role: "teacher", image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Math" }, { id: "user_student_1", name: "Alice Student", email: "alice@next-edu.com", role: "student", image: "https://api.dicebear.com/7.x/avataaars/svg?seed=Alice" } ]; await db.insert(users).values(usersData); // Assign Roles (RBAC) await db.insert(usersToRoles).values([ { userId: "user_admin", roleId: roleMap.admin }, { userId: "user_teacher_math", roleId: roleMap.teacher }, // Math teacher is also a Grade Head { userId: "user_teacher_math", roleId: roleMap.grade_head }, { userId: "user_student_1", roleId: roleMap.student }, ]); const extraStudentIds: string[] = []; for (let i = 0; i < 12; i++) { const studentId = createId(); extraStudentIds.push(studentId); await db.insert(users).values({ id: studentId, name: faker.person.fullName(), email: faker.internet.email().toLowerCase(), role: "student", image: `https://api.dicebear.com/7.x/avataaars/svg?seed=${studentId}`, }); await db.insert(usersToRoles).values({ userId: studentId, roleId: roleMap.student }); } const schoolId = "school_nextedu" const grade10Id = "grade_10" await db.insert(schools).values([ { id: schoolId, name: "Next_Edu School", code: "NEXTEDU" }, { id: "school_demo_2", name: "Demo School No.2", code: "DEMO2" }, ]) await db.insert(grades).values([ { id: grade10Id, schoolId, name: "Grade 10", order: 10, gradeHeadId: "user_teacher_math", teachingHeadId: "user_teacher_math", }, ]) await db.insert(classes).values([ { id: "class_10_3", schoolName: "Next_Edu School", schoolId, name: "Grade 10 · Class 3", grade: "Grade 10", gradeId: grade10Id, homeroom: "10-3", room: "Room 304", invitationCode: "100003", teacherId: "user_teacher_math", }, { id: "class_10_7", schoolName: "Next_Edu School", schoolId, name: "Grade 10 · Class 7", grade: "Grade 10", gradeId: grade10Id, homeroom: "10-7", room: "Room 201", invitationCode: "100007", teacherId: "user_teacher_math", }, ]); await db.insert(classEnrollments).values([ { classId: "class_10_3", studentId: "user_student_1", status: "active" }, ...extraStudentIds.slice(0, 8).map((studentId) => ({ classId: "class_10_3", studentId, status: "active" as const })), ...extraStudentIds.slice(8, 12).map((studentId) => ({ classId: "class_10_7", studentId, status: "active" as const })), ]); await db.insert(classSchedule).values([ { id: "cs_001", classId: "class_10_3", weekday: 1, startTime: "09:00", endTime: "09:45", course: "Mathematics", location: "Room 304" }, { id: "cs_002", classId: "class_10_3", weekday: 3, startTime: "14:00", endTime: "14:45", course: "Physics", location: "Lab A" }, { id: "cs_003", classId: "class_10_7", weekday: 2, startTime: "11:00", endTime: "11:45", course: "Mathematics", location: "Room 201" }, ]); await db.insert(textbooks).values([ { id: "tb_01", title: "Advanced Mathematics Grade 10", subject: "Mathematics", grade: "Grade 10", publisher: "Next Education Press", }, ]) await db.insert(chapters).values([ { id: "ch_01", textbookId: "tb_01", title: "Chapter 1: Real Numbers", order: 1, parentId: null, content: "# Chapter 1: Real Numbers\n\nIn this chapter, we will explore the properties of real numbers...", }, { id: "ch_01_01", textbookId: "tb_01", title: "1.1 Introduction to Real Numbers", order: 1, parentId: "ch_01", content: "## 1.1 Introduction\n\nReal numbers include rational and irrational numbers.", }, ]) // --- 2. Knowledge Graph (Tree) --- console.log("🧠 Seeding Knowledge Graph..."); const kpMathId = createId(); const kpAlgebraId = createId(); const kpLinearId = createId(); await db.insert(knowledgePoints).values([ { id: kpMathId, name: "Mathematics", level: 0 }, { id: kpAlgebraId, name: "Algebra", parentId: kpMathId, level: 1 }, { id: kpLinearId, name: "Linear Equations", parentId: kpAlgebraId, level: 2 }, ]); await db.insert(knowledgePoints).values([ { id: "kp_01", name: "Real Numbers", description: "Definition and properties of real numbers", level: 1, order: 1, chapterId: "ch_01", }, { id: "kp_02", name: "Rational Numbers", description: "Numbers that can be expressed as a fraction", level: 2, order: 1, chapterId: "ch_01_01", }, ]) // --- 3. Question Bank (Rich Content) --- console.log("📚 Seeding Question Bank..."); const mathExamQuestions: Array<{ id: string; type: "single_choice" | "text" | "judgment"; difficulty: number; content: unknown; score: number; }> = [ { id: createId(), type: "single_choice", difficulty: 1, score: 4, content: { text: "1) What is 2 + 2?", options: [ { id: "A", text: "3", isCorrect: false }, { id: "B", text: "4", isCorrect: true }, { id: "C", text: "5", isCorrect: false }, { id: "D", text: "6", isCorrect: false }, ], }, }, { id: createId(), type: "single_choice", difficulty: 2, score: 4, content: { text: "2) If f(x) = 2x + 1, then f(3) = ?", options: [ { id: "A", text: "5", isCorrect: false }, { id: "B", text: "7", isCorrect: true }, { id: "C", text: "8", isCorrect: false }, { id: "D", text: "10", isCorrect: false }, ], }, }, { id: createId(), type: "single_choice", difficulty: 2, score: 4, content: { text: "3) Solve 3x - 5 = 7. What is x?", options: [ { id: "A", text: "3", isCorrect: false }, { id: "B", text: "4", isCorrect: true }, { id: "C", text: "5", isCorrect: false }, { id: "D", text: "6", isCorrect: false }, ], }, }, { id: createId(), type: "single_choice", difficulty: 3, score: 4, content: { text: "4) Which is a factor of x^2 - 9?", options: [ { id: "A", text: "(x - 3)", isCorrect: true }, { id: "B", text: "(x + 9)", isCorrect: false }, { id: "C", text: "(x - 9)", isCorrect: false }, { id: "D", text: "(x^2 + 9)", isCorrect: false }, ], }, }, { id: createId(), type: "single_choice", difficulty: 2, score: 4, content: { text: "5) If a^2 = 49 and a > 0, then a = ?", options: [ { id: "A", text: "-7", isCorrect: false }, { id: "B", text: "0", isCorrect: false }, { id: "C", text: "7", isCorrect: true }, { id: "D", text: "49", isCorrect: false }, ], }, }, { id: createId(), type: "single_choice", difficulty: 3, score: 4, content: { text: "6) Simplify (x^2 y)(x y^3).", options: [ { id: "A", text: "x^2 y^3", isCorrect: false }, { id: "B", text: "x^3 y^4", isCorrect: true }, { id: "C", text: "x^3 y^3", isCorrect: false }, { id: "D", text: "x^4 y^4", isCorrect: false }, ], }, }, { id: createId(), type: "single_choice", difficulty: 2, score: 4, content: { text: "7) The slope of the line y = -3x + 2 is:", options: [ { id: "A", text: "2", isCorrect: false }, { id: "B", text: "-3", isCorrect: true }, { id: "C", text: "3", isCorrect: false }, { id: "D", text: "-2", isCorrect: false }, ], }, }, { id: createId(), type: "single_choice", difficulty: 1, score: 4, content: { text: "8) The probability of getting heads in one fair coin toss is:", options: [ { id: "A", text: "0", isCorrect: false }, { id: "B", text: "1/4", isCorrect: false }, { id: "C", text: "1/2", isCorrect: true }, { id: "D", text: "1", isCorrect: false }, ], }, }, { id: createId(), type: "single_choice", difficulty: 2, score: 4, content: { text: "9) In an arithmetic sequence with a1 = 2 and d = 3, a5 = ?", options: [ { id: "A", text: "11", isCorrect: false }, { id: "B", text: "12", isCorrect: false }, { id: "C", text: "14", isCorrect: true }, { id: "D", text: "17", isCorrect: false }, ], }, }, { id: createId(), type: "single_choice", difficulty: 2, score: 4, content: { text: "10) The solution set of x^2 = 0 is:", options: [ { id: "A", text: "{0}", isCorrect: true }, { id: "B", text: "{1}", isCorrect: false }, { id: "C", text: "{-1, 1}", isCorrect: false }, { id: "D", text: "Empty set", isCorrect: false }, ], }, }, { id: createId(), type: "text", difficulty: 1, score: 4, content: { text: "11) Fill in the blank: √81 = ____.", correctAnswer: "9" } }, { id: createId(), type: "text", difficulty: 2, score: 4, content: { text: "12) Fill in the blank: (a - b)^2 = ____.", correctAnswer: ["a^2 - 2ab + b^2", "a² - 2ab + b²"] } }, { id: createId(), type: "text", difficulty: 1, score: 4, content: { text: "13) Fill in the blank: 2^5 = ____.", correctAnswer: "32" } }, { id: createId(), type: "text", difficulty: 2, score: 4, content: { text: "14) Fill in the blank: The area of a circle with radius r is ____.", correctAnswer: ["πr^2", "pi r^2", "πr²"] } }, { id: createId(), type: "text", difficulty: 1, score: 4, content: { text: "15) Fill in the blank: If x = -2, then x^3 = ____.", correctAnswer: "-8" } }, { id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "16) If x > y, then x + 1 > y + 1.", correctAnswer: true } }, { id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "17) The graph of y = 2x is a parabola.", correctAnswer: false } }, { id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "18) The sum of interior angles of a triangle is 180°.", correctAnswer: true } }, { id: createId(), type: "judgment", difficulty: 2, score: 2, content: { text: "19) (x + y)^2 = x^2 + y^2.", correctAnswer: false } }, { id: createId(), type: "judgment", difficulty: 1, score: 2, content: { text: "20) 0 is a positive number.", correctAnswer: false } }, { id: createId(), type: "text", difficulty: 3, score: 10, content: { text: "21) Solve the system: x + y = 5, x - y = 1." } }, { id: createId(), type: "text", difficulty: 3, score: 10, content: { text: "22) Expand and simplify: (2x - 3)(x + 4)." } }, { id: createId(), type: "text", difficulty: 3, score: 10, content: { text: "23) In a right triangle with legs 6 and 8, find the hypotenuse and the area." } }, ]; await db.insert(questions).values( mathExamQuestions.map((q) => ({ id: q.id, type: q.type, difficulty: q.difficulty, content: q.content, authorId: "user_teacher_math", })) ); await db.insert(questionsToKnowledgePoints).values({ questionId: mathExamQuestions[0].id, knowledgePointId: kpLinearId }); // --- 4. Exams (New Structure) --- console.log("📝 Seeding Exams..."); const examId = createId(); const makeGroup = (title: string, children: unknown[]) => ({ id: createId(), type: "group", title, children, }); const makeQuestionNode = (questionId: string, score: number) => ({ id: createId(), type: "question", questionId, score, }); const choiceIds = mathExamQuestions.slice(0, 10).map((q) => q.id); const fillIds = mathExamQuestions.slice(10, 15).map((q) => q.id); const judgmentIds = mathExamQuestions.slice(15, 20).map((q) => q.id); const shortAnswerIds = mathExamQuestions.slice(20, 23).map((q) => q.id); const examStructure = [ makeGroup( "第一部分:单项选择题(共10题,每题4分,共40分)", choiceIds.map((id) => makeQuestionNode(id, 4)) ), makeGroup( "第二部分:填空题(共5题,每题4分,共20分)", fillIds.map((id) => makeQuestionNode(id, 4)) ), makeGroup( "第三部分:判断题(共5题,每题2分,共10分)", judgmentIds.map((id) => makeQuestionNode(id, 2)) ), makeGroup( "第四部分:解答题(共3题,每题10分,共30分)", shortAnswerIds.map((id) => makeQuestionNode(id, 10)) ), ]; await db.insert(exams).values({ id: examId, title: "Grade 10 Mathematics Final Exam (Seed)", description: JSON.stringify({ subject: "Mathematics", grade: "Grade 10", difficulty: 3, totalScore: 100, durationMin: 120, questionCount: 23, tags: ["seed", "math", "grade10", "final"], }), creatorId: "user_teacher_math", status: "published", startTime: new Date(), structure: examStructure as unknown }); // Link questions physically (Source of Truth) const orderedQuestionIds = [...choiceIds, ...fillIds, ...judgmentIds, ...shortAnswerIds]; const scoreById = new Map(mathExamQuestions.map((q) => [q.id, q.score] as const)); await db.insert(examQuestions).values( orderedQuestionIds.map((questionId, order) => ({ examId, questionId, score: scoreById.get(questionId) ?? 0, order, })) ); console.log("📌 Seeding Homework Assignments..."); const assignmentId = createId(); const now = new Date(); const dueAt = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); const lateDueAt = new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000); await db.insert(homeworkAssignments).values({ id: assignmentId, sourceExamId: examId, title: "Grade 10 Mathematics Final - Homework (Seed)", description: "Auto-generated homework assignment from seeded math exam.", structure: examStructure as unknown, status: "published", creatorId: "user_teacher_math", availableAt: now, dueAt, allowLate: true, lateDueAt, maxAttempts: 2, }); await db.insert(homeworkAssignmentQuestions).values( orderedQuestionIds.map((questionId, order) => ({ assignmentId, questionId, score: scoreById.get(questionId) ?? 0, order, })) ); const targetStudentIds = ["user_student_1", ...extraStudentIds.slice(0, 4)]; await db.insert(homeworkAssignmentTargets).values( targetStudentIds.map((studentId) => ({ assignmentId, studentId, })) ); const scoreForQuestion = (questionId: string) => scoreById.get(questionId) ?? 0; const buildAnswer = (questionId: string, type: string) => { if (type === "single_choice") return { answer: "B" }; if (type === "judgment") return { answer: true }; return { answer: "Seed answer" }; }; const questionTypeById = new Map(mathExamQuestions.map((q) => [q.id, q.type] as const)); const submissionIds: string[] = []; for (let i = 0; i < 3; i++) { const studentId = targetStudentIds[i]; const submissionId = createId(); submissionIds.push(submissionId); const submittedAt = new Date(now.getTime() - (i + 1) * 24 * 60 * 60 * 1000); const status = i === 0 ? "graded" : i === 1 ? "graded" : "submitted"; const perQuestionScores = orderedQuestionIds.map((qid, idx) => { const max = scoreForQuestion(qid); if (status !== "graded") return null; if (max <= 0) return 0; if (idx % 7 === 0) return Math.max(0, max - 1); return max; }); const totalScore = status === "graded" ? perQuestionScores.reduce((sum, s) => sum + Number(s ?? 0), 0) : null; await db.insert(homeworkSubmissions).values({ id: submissionId, assignmentId, studentId, attemptNo: 1, score: totalScore, status, startedAt: submittedAt, submittedAt, isLate: false, createdAt: submittedAt, updatedAt: submittedAt, }); await db.insert(homeworkAnswers).values( orderedQuestionIds.map((questionId, idx) => { const questionType = questionTypeById.get(questionId) ?? "text"; const score = status === "graded" ? (perQuestionScores[idx] ?? 0) : null; return { id: createId(), submissionId, questionId, answerContent: buildAnswer(questionId, questionType), score, feedback: status === "graded" ? (score === scoreForQuestion(questionId) ? "Good" : "Check calculation") : null, createdAt: submittedAt, updatedAt: submittedAt, }; }) ); } const end = performance.now(); console.log(`✅ Seed completed in ${(end - start).toFixed(2)}ms`); process.exit(0); } seed().catch((err) => { console.error("❌ Seed failed:", err); process.exit(1); });