feat: Implement lesson creation with file uploads (video, attachments) and quiz data, integrating MinIO for storage.
This commit is contained in:
parent
4851182f4a
commit
04e2da43c4
6 changed files with 715 additions and 4 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue