feat: Implement lesson creation with file uploads (video, attachments) and quiz data, integrating MinIO for storage.

This commit is contained in:
JakkrapartXD 2026-01-20 16:51:42 +07:00
parent 4851182f4a
commit 04e2da43c4
6 changed files with 715 additions and 4 deletions

View file

@ -2,6 +2,7 @@ import { prisma } from '../config/database';
import { Prisma } from '@prisma/client';
import { config } from '../config';
import { logger } from '../config/logger';
import { generateFilePath, uploadFile } from '../config/minio';
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
import jwt from 'jsonwebtoken';
import {
@ -35,6 +36,7 @@ import {
DeleteLessonResponse,
ReorderLessonsResponse,
} from "../types/ChaptersLesson.typs";
import { CoursesInstructorService } from './CoursesInstructor.service';
export class ChaptersLessonService {
async listChapters(request: ChaptersRequest): Promise<ListChaptersResponse> {
@ -45,7 +47,10 @@ export class ChaptersLessonService {
if (!user) {
throw new UnauthorizedError('Invalid token');
}
const chapters = await prisma.chapter.findMany({ where: { course_id } });
const chapters = await prisma.chapter.findMany({
where: { course_id }, orderBy: { sort_order: 'asc' },
include: { lessons: { orderBy: { sort_order: 'asc' } } }
});
return { code: 200, message: 'Chapters fetched successfully', data: chapters as ChapterData[], total: chapters.length };
} catch (error) {
logger.error(`Error fetching chapters: ${error}`);
@ -53,4 +58,240 @@ export class ChaptersLessonService {
}
}
async createChapter(request: CreateChapterInput): Promise<CreateChapterResponse> {
try {
const { token, course_id, title, description, sort_order } = request;
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
await CoursesInstructorService.validateCourseStatus(course_id);
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
if (!user) {
throw new UnauthorizedError('Invalid token');
}
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
if (!courseInstructor) {
throw new ForbiddenError('You are not permitted to create chapter');
}
const chapter = await prisma.chapter.create({ data: { course_id, title, description, sort_order } });
return { code: 200, message: 'Chapter created successfully', data: chapter as ChapterData };
} catch (error) {
logger.error(`Error creating chapter: ${error}`);
throw error;
}
}
async updateChapter(request: UpdateChapterInput): Promise<UpdateChapterResponse> {
try {
const { token, course_id, chapter_id, title, description, sort_order } = request;
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
await CoursesInstructorService.validateCourseStatus(course_id);
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
if (!user) {
throw new UnauthorizedError('Invalid token');
}
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
if (!courseInstructor) {
throw new ForbiddenError('You are not permitted to update chapter');
}
const chapter = await prisma.chapter.update({ where: { id: chapter_id }, data: { title, description, sort_order } });
return { code: 200, message: 'Chapter updated successfully', data: chapter as ChapterData };
} catch (error) {
logger.error(`Error updating chapter: ${error}`);
throw error;
}
}
async deleteChapter(request: DeleteChapterRequest): Promise<DeleteChapterResponse> {
try {
const { token, course_id, chapter_id } = request;
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
await CoursesInstructorService.validateCourseStatus(course_id);
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
if (!user) {
throw new UnauthorizedError('Invalid token');
}
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
if (!courseInstructor) {
throw new ForbiddenError('You are not permitted to delete chapter');
}
await prisma.chapter.delete({ where: { id: chapter_id } });
return { code: 200, message: 'Chapter deleted successfully' };
} catch (error) {
logger.error(`Error deleting chapter: ${error}`);
throw error;
}
}
async reorderChapter(request: ReorderChapterRequest): Promise<ReorderChapterResponse> {
try {
const { token, course_id, chapter_id, sort_order } = request;
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
await CoursesInstructorService.validateCourseStatus(course_id);
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
if (!user) {
throw new UnauthorizedError('Invalid token');
}
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
if (!courseInstructor) {
throw new ForbiddenError('You are not permitted to reorder chapter');
}
const chapter = await prisma.chapter.update({ where: { id: chapter_id }, data: { sort_order } });
return { code: 200, message: 'Chapter reordered successfully', data: [chapter as ChapterData] };
} catch (error) {
logger.error(`Error reordering chapter: ${error}`);
throw error;
}
}
async createLesson(request: CreateLessonInput): Promise<CreateLessonResponse> {
try {
const { token, course_id, chapter_id, title, content, type, sort_order, video, attachments } = request;
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
await CoursesInstructorService.validateCourseStatus(course_id);
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
if (!user) {
throw new UnauthorizedError('Invalid token');
}
const courseInstructor = await CoursesInstructorService.validateCourseInstructor(token, course_id);
if (!courseInstructor) {
throw new ForbiddenError('You are not permitted to create lesson');
}
// Create the lesson first
const lesson = await prisma.lesson.create({
data: { chapter_id, title, content, type, sort_order }
});
const uploadedAttachments: { file_name: string; file_path: string; file_size: number; mime_type: string }[] = [];
// Handle video upload for VIDEO type lessons
if (type === 'VIDEO' && video) {
const videoPath = generateFilePath(course_id, lesson.id, 'video', video.originalname);
await uploadFile(videoPath, video.buffer, video.mimetype);
// Save video as attachment
await prisma.lessonAttachment.create({
data: {
lesson_id: lesson.id,
file_name: video.originalname,
file_size: video.size,
mime_type: video.mimetype,
file_path: videoPath,
sort_order: 0,
}
});
uploadedAttachments.push({
file_name: video.originalname,
file_path: videoPath,
file_size: video.size,
mime_type: video.mimetype,
});
// Handle additional attachments (PDFs, documents, etc.)
if (attachments && attachments.length > 0) {
for (let i = 0; i < attachments.length; i++) {
const attachment = attachments[i];
const attachmentPath = generateFilePath(course_id, lesson.id, 'attachment', attachment.originalname);
await uploadFile(attachmentPath, attachment.buffer, attachment.mimetype);
await prisma.lessonAttachment.create({
data: {
lesson_id: lesson.id,
file_name: attachment.originalname,
file_size: attachment.size,
mime_type: attachment.mimetype,
file_path: attachmentPath,
sort_order: i + 1, // Start from 1 since video is 0
}
});
uploadedAttachments.push({
file_name: attachment.originalname,
file_path: attachmentPath,
file_size: attachment.size,
mime_type: attachment.mimetype,
});
}
}
} else if (type === 'QUIZ' && request.quiz_data) {
// Create Quiz with Questions and Choices
const { quiz_data } = request;
const userId = decodedToken.id;
// Create the quiz
const quiz = await prisma.quiz.create({
data: {
lesson_id: lesson.id,
title: quiz_data.title,
description: quiz_data.description,
passing_score: quiz_data.passing_score ?? 60,
time_limit: quiz_data.time_limit,
shuffle_questions: quiz_data.shuffle_questions ?? false,
shuffle_choices: quiz_data.shuffle_choices ?? false,
show_answers_after_completion: quiz_data.show_answers_after_completion ?? true,
created_by: userId,
}
});
// Create questions with choices
if (quiz_data.questions && quiz_data.questions.length > 0) {
for (let i = 0; i < quiz_data.questions.length; i++) {
const questionInput = quiz_data.questions[i];
// Create the question
const question = await prisma.question.create({
data: {
quiz_id: quiz.id,
question: questionInput.question,
explanation: questionInput.explanation,
question_type: questionInput.question_type,
score: questionInput.score ?? 1,
sort_order: questionInput.sort_order ?? i,
}
});
// Create choices for this question
if (questionInput.choices && questionInput.choices.length > 0) {
for (let j = 0; j < questionInput.choices.length; j++) {
const choiceInput = questionInput.choices[j];
await prisma.choice.create({
data: {
question_id: question.id,
text: choiceInput.text,
is_correct: choiceInput.is_correct,
sort_order: choiceInput.sort_order ?? j,
}
});
}
}
}
}
}
// Fetch the complete lesson with attachments and quiz
const completeLesson = await prisma.lesson.findUnique({
where: { id: lesson.id },
include: {
attachments: { orderBy: { sort_order: 'asc' } },
quiz: {
include: {
questions: {
orderBy: { sort_order: 'asc' },
include: {
choices: { orderBy: { sort_order: 'asc' } }
}
}
}
}
}
});
return { code: 200, message: 'Lesson created successfully', data: completeLesson as LessonData };
} catch (error) {
logger.error(`Error creating lesson: ${error}`);
throw error;
}
}
}

View file

@ -2,7 +2,7 @@ import { prisma } from '../config/database';
import { Prisma } from '@prisma/client';
import { config } from '../config';
import { logger } from '../config/logger';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
import jwt from 'jsonwebtoken';
import {
CreateCourseInput,
@ -190,6 +190,14 @@ export class CoursesInstructorService {
submitted_by: decoded.id,
}
});
await prisma.course.update({
where: {
id: sendCourseForReview.course_id
},
data: {
status: 'PENDING'
}
});
return {
code: 200,
message: 'Course sent for review successfully',
@ -288,7 +296,7 @@ export class CoursesInstructorService {
}
}
private static async validateCourseInstructor(token: string, courseId: number): Promise<{ user_id: number; is_primary: boolean }> {
static async validateCourseInstructor(token: string, courseId: number): Promise<{ user_id: number; is_primary: boolean }> {
const decoded = jwt.verify(token, config.jwt.secret) as { id: number; type: string };
const courseInstructor = await prisma.courseInstructor.findFirst({
where: {
@ -301,4 +309,14 @@ export class CoursesInstructorService {
throw new ForbiddenError('You are not an instructor of this course');
} else return { user_id: courseInstructor.user_id, is_primary: courseInstructor.is_primary };
}
static async validateCourseStatus(courseId: number): Promise<void> {
const course = await prisma.course.findUnique({ where: { id: courseId } });
if (!course) {
throw new NotFoundError('Course not found');
}
if (course.status === 'APPROVED') {
throw new ForbiddenError('Course is already approved Cannot Edit');
}
}
}