init Backend

This commit is contained in:
JakkrapartXD 2026-01-08 06:51:33 +00:00
parent 08a4e0d8fa
commit 924000b084
29 changed files with 10080 additions and 13 deletions

View file

@ -0,0 +1,406 @@
-- CreateEnum
CREATE TYPE "CourseStatus" AS ENUM ('DRAFT', 'PENDING_APPROVAL', 'APPROVED', 'REJECTED', 'ARCHIVED');
-- CreateEnum
CREATE TYPE "LessonType" AS ENUM ('VIDEO', 'TEXT', 'PDF', 'QUIZ');
-- CreateEnum
CREATE TYPE "ScorePolicy" AS ENUM ('HIGHEST', 'LATEST', 'AVERAGE');
-- CreateTable
CREATE TABLE "roles" (
"id" SERIAL NOT NULL,
"code" VARCHAR(50) NOT NULL,
"name" JSONB NOT NULL,
"description" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" SERIAL NOT NULL,
"username" VARCHAR(50) NOT NULL,
"email" VARCHAR(255) NOT NULL,
"password" VARCHAR(255) NOT NULL,
"role_id" INTEGER NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "profiles" (
"id" SERIAL NOT NULL,
"user_id" INTEGER NOT NULL,
"first_name" VARCHAR(100),
"last_name" VARCHAR(100),
"phone" VARCHAR(20),
"avatar_url" VARCHAR(500),
"bio" JSONB,
"birth_date" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "profiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "categories" (
"id" SERIAL NOT NULL,
"code" VARCHAR(50) NOT NULL,
"name" JSONB NOT NULL,
"description" JSONB,
"icon_url" VARCHAR(500),
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "categories_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "courses" (
"id" SERIAL NOT NULL,
"title" JSONB NOT NULL,
"description" JSONB NOT NULL,
"thumbnail_url" VARCHAR(500),
"price" DECIMAL(10,2) NOT NULL DEFAULT 0,
"is_free" BOOLEAN NOT NULL DEFAULT false,
"have_certificate" BOOLEAN NOT NULL DEFAULT false,
"status" "CourseStatus" NOT NULL DEFAULT 'DRAFT',
"category_id" INTEGER NOT NULL,
"created_by" INTEGER NOT NULL,
"rejection_reason" TEXT,
"is_deleted" BOOLEAN NOT NULL DEFAULT false,
"deleted_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "courses_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "course_instructors" (
"id" SERIAL NOT NULL,
"course_id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
"is_primary" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "course_instructors_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "chapters" (
"id" SERIAL NOT NULL,
"course_id" INTEGER NOT NULL,
"title" JSONB NOT NULL,
"description" JSONB,
"order" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "chapters_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "lessons" (
"id" SERIAL NOT NULL,
"chapter_id" INTEGER NOT NULL,
"title" JSONB NOT NULL,
"description" JSONB,
"type" "LessonType" NOT NULL,
"content" JSONB,
"video_url" VARCHAR(500),
"video_duration" INTEGER,
"order" INTEGER NOT NULL,
"is_preview" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "lessons_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "lesson_prerequisites" (
"id" SERIAL NOT NULL,
"lesson_id" INTEGER NOT NULL,
"prerequisite_lesson_id" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "lesson_prerequisites_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "attachments" (
"id" SERIAL NOT NULL,
"lesson_id" INTEGER NOT NULL,
"filename" VARCHAR(255) NOT NULL,
"original_name" VARCHAR(255) NOT NULL,
"file_url" VARCHAR(500) NOT NULL,
"file_size" BIGINT NOT NULL,
"mime_type" VARCHAR(100) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "attachments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "quizzes" (
"id" SERIAL NOT NULL,
"lesson_id" INTEGER NOT NULL,
"title" JSONB NOT NULL,
"description" JSONB,
"passing_score" INTEGER NOT NULL DEFAULT 70,
"time_limit" INTEGER,
"max_attempts" INTEGER NOT NULL DEFAULT 3,
"cooldown_hours" INTEGER NOT NULL DEFAULT 24,
"score_policy" "ScorePolicy" NOT NULL DEFAULT 'HIGHEST',
"shuffle_questions" BOOLEAN NOT NULL DEFAULT true,
"shuffle_choices" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "quizzes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "quiz_questions" (
"id" SERIAL NOT NULL,
"quiz_id" INTEGER NOT NULL,
"question" JSONB NOT NULL,
"choices" JSONB NOT NULL,
"points" INTEGER NOT NULL DEFAULT 1,
"order" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "quiz_questions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "quiz_attempts" (
"id" SERIAL NOT NULL,
"quiz_id" INTEGER NOT NULL,
"user_id" INTEGER NOT NULL,
"answers" JSONB NOT NULL,
"score" INTEGER NOT NULL,
"passed" BOOLEAN NOT NULL,
"time_spent" INTEGER,
"started_at" TIMESTAMP(3) NOT NULL,
"completed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "quiz_attempts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "enrollments" (
"id" SERIAL NOT NULL,
"user_id" INTEGER NOT NULL,
"course_id" INTEGER NOT NULL,
"enrolled_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completed_at" TIMESTAMP(3),
"progress_percent" INTEGER NOT NULL DEFAULT 0,
"last_accessed_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "enrollments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "lesson_progress" (
"id" SERIAL NOT NULL,
"user_id" INTEGER NOT NULL,
"lesson_id" INTEGER NOT NULL,
"is_completed" BOOLEAN NOT NULL DEFAULT false,
"video_progress" INTEGER NOT NULL DEFAULT 0,
"completed_at" TIMESTAMP(3),
"last_watched_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "lesson_progress_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "certificates" (
"id" SERIAL NOT NULL,
"user_id" INTEGER NOT NULL,
"course_id" INTEGER NOT NULL,
"certificate_url" VARCHAR(500) NOT NULL,
"issued_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "certificates_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "roles_code_key" ON "roles"("code");
-- CreateIndex
CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "users_email_idx" ON "users"("email");
-- CreateIndex
CREATE INDEX "users_role_id_idx" ON "users"("role_id");
-- CreateIndex
CREATE UNIQUE INDEX "profiles_user_id_key" ON "profiles"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "categories_code_key" ON "categories"("code");
-- CreateIndex
CREATE INDEX "courses_category_id_idx" ON "courses"("category_id");
-- CreateIndex
CREATE INDEX "courses_status_idx" ON "courses"("status");
-- CreateIndex
CREATE INDEX "courses_created_by_idx" ON "courses"("created_by");
-- CreateIndex
CREATE INDEX "courses_created_at_idx" ON "courses"("created_at");
-- CreateIndex
CREATE INDEX "course_instructors_course_id_idx" ON "course_instructors"("course_id");
-- CreateIndex
CREATE INDEX "course_instructors_user_id_idx" ON "course_instructors"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "course_instructors_course_id_user_id_key" ON "course_instructors"("course_id", "user_id");
-- CreateIndex
CREATE INDEX "chapters_course_id_idx" ON "chapters"("course_id");
-- CreateIndex
CREATE INDEX "chapters_order_idx" ON "chapters"("order");
-- CreateIndex
CREATE INDEX "lessons_chapter_id_idx" ON "lessons"("chapter_id");
-- CreateIndex
CREATE INDEX "lessons_order_idx" ON "lessons"("order");
-- CreateIndex
CREATE INDEX "lesson_prerequisites_lesson_id_idx" ON "lesson_prerequisites"("lesson_id");
-- CreateIndex
CREATE INDEX "lesson_prerequisites_prerequisite_lesson_id_idx" ON "lesson_prerequisites"("prerequisite_lesson_id");
-- CreateIndex
CREATE UNIQUE INDEX "lesson_prerequisites_lesson_id_prerequisite_lesson_id_key" ON "lesson_prerequisites"("lesson_id", "prerequisite_lesson_id");
-- CreateIndex
CREATE INDEX "attachments_lesson_id_idx" ON "attachments"("lesson_id");
-- CreateIndex
CREATE UNIQUE INDEX "quizzes_lesson_id_key" ON "quizzes"("lesson_id");
-- CreateIndex
CREATE INDEX "quiz_questions_quiz_id_idx" ON "quiz_questions"("quiz_id");
-- CreateIndex
CREATE INDEX "quiz_questions_order_idx" ON "quiz_questions"("order");
-- CreateIndex
CREATE INDEX "quiz_attempts_quiz_id_idx" ON "quiz_attempts"("quiz_id");
-- CreateIndex
CREATE INDEX "quiz_attempts_user_id_idx" ON "quiz_attempts"("user_id");
-- CreateIndex
CREATE INDEX "quiz_attempts_completed_at_idx" ON "quiz_attempts"("completed_at");
-- CreateIndex
CREATE INDEX "enrollments_user_id_idx" ON "enrollments"("user_id");
-- CreateIndex
CREATE INDEX "enrollments_course_id_idx" ON "enrollments"("course_id");
-- CreateIndex
CREATE UNIQUE INDEX "enrollments_user_id_course_id_key" ON "enrollments"("user_id", "course_id");
-- CreateIndex
CREATE INDEX "lesson_progress_user_id_idx" ON "lesson_progress"("user_id");
-- CreateIndex
CREATE INDEX "lesson_progress_lesson_id_idx" ON "lesson_progress"("lesson_id");
-- CreateIndex
CREATE UNIQUE INDEX "lesson_progress_user_id_lesson_id_key" ON "lesson_progress"("user_id", "lesson_id");
-- CreateIndex
CREATE INDEX "certificates_user_id_idx" ON "certificates"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "certificates_user_id_course_id_key" ON "certificates"("user_id", "course_id");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "profiles" ADD CONSTRAINT "profiles_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "courses" ADD CONSTRAINT "courses_category_id_fkey" FOREIGN KEY ("category_id") REFERENCES "categories"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "courses" ADD CONSTRAINT "courses_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "course_instructors" ADD CONSTRAINT "course_instructors_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "course_instructors" ADD CONSTRAINT "course_instructors_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "chapters" ADD CONSTRAINT "chapters_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lessons" ADD CONSTRAINT "lessons_chapter_id_fkey" FOREIGN KEY ("chapter_id") REFERENCES "chapters"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lesson_prerequisites" ADD CONSTRAINT "lesson_prerequisites_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lesson_prerequisites" ADD CONSTRAINT "lesson_prerequisites_prerequisite_lesson_id_fkey" FOREIGN KEY ("prerequisite_lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "attachments" ADD CONSTRAINT "attachments_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "quizzes" ADD CONSTRAINT "quizzes_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "quiz_questions" ADD CONSTRAINT "quiz_questions_quiz_id_fkey" FOREIGN KEY ("quiz_id") REFERENCES "quizzes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "quiz_attempts" ADD CONSTRAINT "quiz_attempts_quiz_id_fkey" FOREIGN KEY ("quiz_id") REFERENCES "quizzes"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "quiz_attempts" ADD CONSTRAINT "quiz_attempts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "enrollments" ADD CONSTRAINT "enrollments_course_id_fkey" FOREIGN KEY ("course_id") REFERENCES "courses"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lesson_progress" ADD CONSTRAINT "lesson_progress_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "lesson_progress" ADD CONSTRAINT "lesson_progress_lesson_id_fkey" FOREIGN KEY ("lesson_id") REFERENCES "lessons"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "certificates" ADD CONSTRAINT "certificates_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View file

@ -0,0 +1,354 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================
// User Management
// ============================================
model Role {
id Int @id @default(autoincrement())
code String @unique @db.VarChar(50)
name Json // { th: "", en: "" }
description Json?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
users User[]
@@map("roles")
}
model User {
id Int @id @default(autoincrement())
username String @unique @db.VarChar(50)
email String @unique @db.VarChar(255)
password String @db.VarChar(255)
role_id Int
is_active Boolean @default(true)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
role Role @relation(fields: [role_id], references: [id])
profile Profile?
// Relations
created_courses Course[] @relation("CourseCreator")
instructor_courses CourseInstructor[]
enrollments Enrollment[]
lesson_progress LessonProgress[]
quiz_attempts QuizAttempt[]
certificates Certificate[]
@@index([email])
@@index([role_id])
@@map("users")
}
model Profile {
id Int @id @default(autoincrement())
user_id Int @unique
first_name String? @db.VarChar(100)
last_name String? @db.VarChar(100)
phone String? @db.VarChar(20)
avatar_url String? @db.VarChar(500)
bio Json? // { th: "", en: "" }
birth_date DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@map("profiles")
}
// ============================================
// Course Management
// ============================================
enum CourseStatus {
DRAFT
PENDING_APPROVAL
APPROVED
REJECTED
ARCHIVED
}
model Category {
id Int @id @default(autoincrement())
code String @unique @db.VarChar(50)
name Json // { th: "", en: "" }
description Json?
icon_url String? @db.VarChar(500)
is_active Boolean @default(true)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
courses Course[]
@@map("categories")
}
model Course {
id Int @id @default(autoincrement())
title Json // { th: "", en: "" }
description Json
thumbnail_url String? @db.VarChar(500)
price Decimal @default(0) @db.Decimal(10, 2)
is_free Boolean @default(false)
have_certificate Boolean @default(false)
status CourseStatus @default(DRAFT)
category_id Int
created_by Int
rejection_reason String? @db.Text
is_deleted Boolean @default(false)
deleted_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
category Category @relation(fields: [category_id], references: [id])
creator User @relation("CourseCreator", fields: [created_by], references: [id])
chapters Chapter[]
instructors CourseInstructor[]
enrollments Enrollment[]
@@index([category_id])
@@index([status])
@@index([created_by])
@@index([created_at])
@@map("courses")
}
model CourseInstructor {
id Int @id @default(autoincrement())
course_id Int
user_id Int
is_primary Boolean @default(false)
created_at DateTime @default(now())
course Course @relation(fields: [course_id], references: [id], onDelete: Cascade)
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@unique([course_id, user_id])
@@index([course_id])
@@index([user_id])
@@map("course_instructors")
}
model Chapter {
id Int @id @default(autoincrement())
course_id Int
title Json // { th: "", en: "" }
description Json?
order Int
created_at DateTime @default(now())
updated_at DateTime @updatedAt
course Course @relation(fields: [course_id], references: [id], onDelete: Cascade)
lessons Lesson[]
@@index([course_id])
@@index([order])
@@map("chapters")
}
// ============================================
// Lessons
// ============================================
enum LessonType {
VIDEO
TEXT
PDF
QUIZ
}
model Lesson {
id Int @id @default(autoincrement())
chapter_id Int
title Json // { th: "", en: "" }
description Json?
type LessonType
content Json? // For TEXT type
video_url String? @db.VarChar(500)
video_duration Int? // in seconds
order Int
is_preview Boolean @default(false)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
chapter Chapter @relation(fields: [chapter_id], references: [id], onDelete: Cascade)
attachments Attachment[]
prerequisites LessonPrerequisite[] @relation("LessonPrerequisites")
required_for LessonPrerequisite[] @relation("RequiredForLessons")
quiz Quiz?
progress LessonProgress[]
@@index([chapter_id])
@@index([order])
@@map("lessons")
}
model LessonPrerequisite {
id Int @id @default(autoincrement())
lesson_id Int
prerequisite_lesson_id Int
created_at DateTime @default(now())
lesson Lesson @relation("LessonPrerequisites", fields: [lesson_id], references: [id], onDelete: Cascade)
prerequisite_lesson Lesson @relation("RequiredForLessons", fields: [prerequisite_lesson_id], references: [id], onDelete: Cascade)
@@unique([lesson_id, prerequisite_lesson_id])
@@index([lesson_id])
@@index([prerequisite_lesson_id])
@@map("lesson_prerequisites")
}
model Attachment {
id Int @id @default(autoincrement())
lesson_id Int
filename String @db.VarChar(255)
original_name String @db.VarChar(255)
file_url String @db.VarChar(500)
file_size BigInt
mime_type String @db.VarChar(100)
created_at DateTime @default(now())
lesson Lesson @relation(fields: [lesson_id], references: [id], onDelete: Cascade)
@@index([lesson_id])
@@map("attachments")
}
// ============================================
// Quiz System
// ============================================
enum ScorePolicy {
HIGHEST
LATEST
AVERAGE
}
model Quiz {
id Int @id @default(autoincrement())
lesson_id Int @unique
title Json // { th: "", en: "" }
description Json?
passing_score Int @default(70)
time_limit Int? // in minutes
max_attempts Int @default(3)
cooldown_hours Int @default(24)
score_policy ScorePolicy @default(HIGHEST)
shuffle_questions Boolean @default(true)
shuffle_choices Boolean @default(true)
created_at DateTime @default(now())
updated_at DateTime @updatedAt
lesson Lesson @relation(fields: [lesson_id], references: [id], onDelete: Cascade)
questions QuizQuestion[]
attempts QuizAttempt[]
@@map("quizzes")
}
model QuizQuestion {
id Int @id @default(autoincrement())
quiz_id Int
question Json // { th: "", en: "" }
choices Json // [{ th: "", en: "", is_correct: boolean }]
points Int @default(1)
order Int
created_at DateTime @default(now())
updated_at DateTime @updatedAt
quiz Quiz @relation(fields: [quiz_id], references: [id], onDelete: Cascade)
@@index([quiz_id])
@@index([order])
@@map("quiz_questions")
}
model QuizAttempt {
id Int @id @default(autoincrement())
quiz_id Int
user_id Int
answers Json // [{ question_id: int, choice_index: int }]
score Int
passed Boolean
time_spent Int? // in seconds
started_at DateTime
completed_at DateTime @default(now())
quiz Quiz @relation(fields: [quiz_id], references: [id], onDelete: Cascade)
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@index([quiz_id])
@@index([user_id])
@@index([completed_at])
@@map("quiz_attempts")
}
// ============================================
// Progress Tracking
// ============================================
model Enrollment {
id Int @id @default(autoincrement())
user_id Int
course_id Int
enrolled_at DateTime @default(now())
completed_at DateTime?
progress_percent Int @default(0)
last_accessed_at DateTime @default(now())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
course Course @relation(fields: [course_id], references: [id], onDelete: Cascade)
@@unique([user_id, course_id])
@@index([user_id])
@@index([course_id])
@@map("enrollments")
}
model LessonProgress {
id Int @id @default(autoincrement())
user_id Int
lesson_id Int
is_completed Boolean @default(false)
video_progress Int @default(0) // seconds watched
completed_at DateTime?
last_watched_at DateTime @default(now())
created_at DateTime @default(now())
updated_at DateTime @updatedAt
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
lesson Lesson @relation(fields: [lesson_id], references: [id], onDelete: Cascade)
@@unique([user_id, lesson_id])
@@index([user_id])
@@index([lesson_id])
@@map("lesson_progress")
}
model Certificate {
id Int @id @default(autoincrement())
user_id Int
course_id Int
certificate_url String @db.VarChar(500)
issued_at DateTime @default(now())
user User @relation(fields: [user_id], references: [id], onDelete: Cascade)
@@unique([user_id, course_id])
@@index([user_id])
@@map("certificates")
}

218
Backend/prisma/seed.js Normal file
View file

@ -0,0 +1,218 @@
const { PrismaClient } = require('@prisma/client');
const bcrypt = require('bcrypt');
const prisma = new PrismaClient();
async function main() {
console.log('🌱 Starting database seeding...');
// Clear existing data (in development only)
if (process.env.NODE_ENV === 'development') {
console.log('🗑️ Clearing existing data...');
await prisma.quizAttempt.deleteMany();
await prisma.quizQuestion.deleteMany();
await prisma.quiz.deleteMany();
await prisma.lessonProgress.deleteMany();
await prisma.attachment.deleteMany();
await prisma.lessonPrerequisite.deleteMany();
await prisma.lesson.deleteMany();
await prisma.chapter.deleteMany();
await prisma.enrollment.deleteMany();
await prisma.courseInstructor.deleteMany();
await prisma.course.deleteMany();
await prisma.category.deleteMany();
await prisma.certificate.deleteMany();
await prisma.profile.deleteMany();
await prisma.user.deleteMany();
await prisma.role.deleteMany();
}
// Seed Roles
console.log('👥 Seeding roles...');
const roles = await Promise.all([
prisma.role.create({
data: {
code: 'ADMIN',
name: { th: 'ผู้ดูแลระบบ', en: 'Administrator' },
description: { th: 'มีสิทธิ์เข้าถึงทุกฟังก์ชัน', en: 'Full system access' }
}
}),
prisma.role.create({
data: {
code: 'INSTRUCTOR',
name: { th: 'ผู้สอน', en: 'Instructor' },
description: { th: 'สามารถสร้างและจัดการคอร์สเรียน', en: 'Can create and manage courses' }
}
}),
prisma.role.create({
data: {
code: 'STUDENT',
name: { th: 'นักเรียน', en: 'Student' },
description: { th: 'สามารถลงทะเบียนและเรียนคอร์ส', en: 'Can enroll and learn courses' }
}
})
]);
// Seed Users
console.log('👤 Seeding users...');
const hashedPassword = await bcrypt.hash('admin123', 10);
const admin = await prisma.user.create({
data: {
username: 'admin',
email: 'admin@elearning.local',
password: hashedPassword,
role_id: roles[0].id,
profile: {
create: {
first_name: 'Admin',
last_name: 'User',
bio: { th: 'ผู้ดูแลระบบ', en: 'System Administrator' }
}
}
}
});
const instructor = await prisma.user.create({
data: {
username: 'instructor',
email: 'instructor@elearning.local',
password: hashedPassword,
role_id: roles[1].id,
profile: {
create: {
first_name: 'John',
last_name: 'Doe',
bio: { th: 'ผู้สอนมืออาชีพ', en: 'Professional Instructor' }
}
}
}
});
const student = await prisma.user.create({
data: {
username: 'student',
email: 'student@elearning.local',
password: hashedPassword,
role_id: roles[2].id,
profile: {
create: {
first_name: 'Jane',
last_name: 'Smith',
bio: { th: 'นักเรียนที่กระตือรือร้น', en: 'Eager learner' }
}
}
}
});
// Seed Categories
console.log('📚 Seeding categories...');
const categories = await Promise.all([
prisma.category.create({
data: {
code: 'PROGRAMMING',
name: { th: 'การเขียนโปรแกรม', en: 'Programming' },
description: { th: 'เรียนรู้การเขียนโปรแกรมและพัฒนาซอฟต์แวร์', en: 'Learn programming and software development' }
}
}),
prisma.category.create({
data: {
code: 'DESIGN',
name: { th: 'การออกแบบ', en: 'Design' },
description: { th: 'เรียนรู้การออกแบบกราฟิกและ UI/UX', en: 'Learn graphic design and UI/UX' }
}
}),
prisma.category.create({
data: {
code: 'BUSINESS',
name: { th: 'ธุรกิจ', en: 'Business' },
description: { th: 'เรียนรู้การบริหารธุรกิจและการตลาด', en: 'Learn business management and marketing' }
}
})
]);
// Seed Sample Course
console.log('🎓 Seeding sample course...');
const course = await prisma.course.create({
data: {
title: { th: 'พื้นฐาน JavaScript', en: 'JavaScript Fundamentals' },
description: {
th: 'เรียนรู้พื้นฐาน JavaScript ตั้งแต่เริ่มต้น',
en: 'Learn JavaScript fundamentals from scratch'
},
price: 0,
is_free: true,
have_certificate: true,
status: 'APPROVED',
category_id: categories[0].id,
created_by: instructor.id,
instructors: {
create: {
user_id: instructor.id,
is_primary: true
}
},
chapters: {
create: [
{
title: { th: 'บทที่ 1: เริ่มต้น', en: 'Chapter 1: Getting Started' },
description: { th: 'แนะนำ JavaScript', en: 'Introduction to JavaScript' },
order: 1,
lessons: {
create: [
{
title: { th: 'JavaScript คืออะไร', en: 'What is JavaScript' },
description: { th: 'เรียนรู้ว่า JavaScript คืออะไร', en: 'Learn what JavaScript is' },
type: 'VIDEO',
order: 1,
is_preview: true
},
{
title: { th: 'ตัวแปรและชนิดข้อมูล', en: 'Variables and Data Types' },
description: { th: 'เรียนรู้เกี่ยวกับตัวแปร', en: 'Learn about variables' },
type: 'VIDEO',
order: 2
}
]
}
},
{
title: { th: 'บทที่ 2: ฟังก์ชัน', en: 'Chapter 2: Functions' },
description: { th: 'เรียนรู้เกี่ยวกับฟังก์ชัน', en: 'Learn about functions' },
order: 2,
lessons: {
create: [
{
title: { th: 'การสร้างฟังก์ชัน', en: 'Creating Functions' },
description: { th: 'เรียนรู้วิธีสร้างฟังก์ชัน', en: 'Learn how to create functions' },
type: 'VIDEO',
order: 1
}
]
}
}
]
}
}
});
console.log('✅ Database seeding completed!');
console.log('\n📊 Summary:');
console.log(`- Roles: ${roles.length}`);
console.log(`- Users: 3 (admin, instructor, student)`);
console.log(`- Categories: ${categories.length}`);
console.log(`- Courses: 1`);
console.log('\n🔑 Test Credentials:');
console.log('Admin: admin / admin123');
console.log('Instructor: instructor / admin123');
console.log('Student: student / admin123');
}
main()
.catch((e) => {
console.error('❌ Error seeding database:', e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});