237 lines
7.0 KiB
TypeScript
237 lines
7.0 KiB
TypeScript
import "dotenv/config";
|
|
import { db } from "../src/shared/db";
|
|
import {
|
|
users, roles, usersToRoles,
|
|
questions, knowledgePoints, questionsToKnowledgePoints,
|
|
exams, examQuestions, examSubmissions, submissionAnswers,
|
|
textbooks, chapters
|
|
} 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 = [
|
|
"submission_answers", "exam_submissions", "exam_questions", "exams",
|
|
"questions_to_knowledge_points", "questions", "knowledge_points",
|
|
"chapters", "textbooks",
|
|
"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"
|
|
};
|
|
|
|
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" }
|
|
]);
|
|
|
|
// 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 },
|
|
]);
|
|
|
|
// --- 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 },
|
|
]);
|
|
|
|
// --- 3. Question Bank (Rich Content) ---
|
|
console.log("📚 Seeding Question Bank...");
|
|
|
|
// 3.1 Simple Single Choice
|
|
const qSimpleId = createId();
|
|
await db.insert(questions).values({
|
|
id: qSimpleId,
|
|
authorId: "user_teacher_math",
|
|
type: "single_choice",
|
|
difficulty: 1,
|
|
content: {
|
|
text: "What is 2 + 2?",
|
|
options: [
|
|
{ id: "A", text: "3", isCorrect: false },
|
|
{ id: "B", text: "4", isCorrect: true },
|
|
{ id: "C", text: "5", isCorrect: false }
|
|
]
|
|
}
|
|
});
|
|
|
|
// Link to KP
|
|
await db.insert(questionsToKnowledgePoints).values({
|
|
questionId: qSimpleId,
|
|
knowledgePointId: kpLinearId // Just for demo
|
|
});
|
|
|
|
// 3.2 Composite Question (Reading Comprehension)
|
|
const qParentId = createId();
|
|
const qChild1Id = createId();
|
|
const qChild2Id = createId();
|
|
|
|
// Parent (Passage)
|
|
await db.insert(questions).values({
|
|
id: qParentId,
|
|
authorId: "user_teacher_math",
|
|
type: "composite",
|
|
difficulty: 3,
|
|
content: {
|
|
text: "Read the following passage about Algebra...\n(Long text here)...",
|
|
assets: []
|
|
}
|
|
});
|
|
|
|
// Children
|
|
await db.insert(questions).values([
|
|
{
|
|
id: qChild1Id,
|
|
authorId: "user_teacher_math",
|
|
parentId: qParentId, // <--- Key: Nested
|
|
type: "single_choice",
|
|
difficulty: 2,
|
|
content: {
|
|
text: "What is the main topic?",
|
|
options: [
|
|
{ id: "A", text: "Geometry", isCorrect: false },
|
|
{ id: "B", text: "Algebra", isCorrect: true }
|
|
]
|
|
}
|
|
},
|
|
{
|
|
id: qChild2Id,
|
|
authorId: "user_teacher_math",
|
|
parentId: qParentId,
|
|
type: "text",
|
|
difficulty: 4,
|
|
content: {
|
|
text: "Explain the concept of variables.",
|
|
}
|
|
}
|
|
]);
|
|
|
|
// --- 4. Exams (New Structure) ---
|
|
console.log("📝 Seeding Exams...");
|
|
|
|
const examId = createId();
|
|
const examStructure = [
|
|
{
|
|
type: "group",
|
|
title: "Part 1: Basics",
|
|
children: [
|
|
{ type: "question", questionId: qSimpleId, score: 10 }
|
|
]
|
|
},
|
|
{
|
|
type: "group",
|
|
title: "Part 2: Reading",
|
|
children: [
|
|
// For composite questions, we usually add the parent, and the system fetches children
|
|
{ type: "question", questionId: qParentId, score: 20 }
|
|
]
|
|
}
|
|
];
|
|
|
|
await db.insert(exams).values({
|
|
id: examId,
|
|
title: "Algebra Mid-Term 2025",
|
|
description: "Comprehensive assessment",
|
|
creatorId: "user_teacher_math",
|
|
status: "published",
|
|
startTime: new Date(),
|
|
structure: examStructure as any // Bypass strict typing for seed
|
|
});
|
|
|
|
// Link questions physically (Source of Truth)
|
|
await db.insert(examQuestions).values([
|
|
{ examId, questionId: qSimpleId, score: 10, order: 0 },
|
|
{ examId, questionId: qParentId, score: 20, order: 1 },
|
|
// Note: Child questions are often implicitly included or explicitly added depending on logic.
|
|
// For this seed, we assume linking Parent is enough for the relation,
|
|
// but let's link children too for completeness if the query strategy requires it.
|
|
{ examId, questionId: qChild1Id, score: 0, order: 2 },
|
|
{ examId, questionId: qChild2Id, score: 0, order: 3 },
|
|
]);
|
|
|
|
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);
|
|
});
|