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

@ -0,0 +1,139 @@
import { Request, Response, NextFunction } from 'express';
import { ChaptersLessonService } from '../services/ChaptersLesson.service';
import { ValidationError } from '../middleware/errorHandler';
import {
lessonUpload,
LessonUploadRequest,
validateTotalAttachmentSize,
validateVideoSize
} from '../middleware/upload';
import { UploadedFileInfo, CreateLessonInput } from '../types/ChaptersLesson.typs';
const chaptersLessonService = new ChaptersLessonService();
/**
* Controller for handling lesson CRUD operations with file uploads
*/
export class LessonsController {
/**
*
* Create a new lesson with optional video and attachments
*
* @route POST /api/instructors/courses/:courseId/chapters/:chapterId/lessons
* @contentType multipart/form-data
*
* @param {number} courseId - / Course ID
* @param {number} chapterId - / Chapter ID
* @param {string} title - (JSON: { th: "", en: "" })
* @param {string} [content] - (JSON: { th: "", en: "" })
* @param {string} type - (VIDEO | QUIZ)
* @param {number} [sort_order] -
* @param {File} [video] - ( type=VIDEO )
* @param {File[]} [attachments] - (PDFs, , )
*/
async createLesson(req: LessonUploadRequest, res: Response, next: NextFunction): Promise<void> {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
throw new ValidationError('No token provided');
}
const courseId = parseInt(req.params.courseId, 10);
const chapterId = parseInt(req.params.chapterId, 10);
if (isNaN(courseId) || isNaN(chapterId)) {
throw new ValidationError('Invalid course ID or chapter ID');
}
// Parse JSON fields from multipart form
const title = JSON.parse(req.body.title || '{}');
const content = req.body.content ? JSON.parse(req.body.content) : undefined;
const type = req.body.type as 'VIDEO' | 'QUIZ';
const sortOrder = req.body.sort_order ? parseInt(req.body.sort_order, 10) : undefined;
if (!type || !['VIDEO', 'QUIZ'].includes(type)) {
throw new ValidationError('Invalid lesson type. Must be VIDEO or QUIZ');
}
if (!title.th || !title.en) {
throw new ValidationError('Title must have both Thai (th) and English (en) values');
}
// Process uploaded files
const files = req.files as { video?: Express.Multer.File[]; attachments?: Express.Multer.File[] } | undefined;
let video: UploadedFileInfo | undefined;
let attachments: UploadedFileInfo[] | undefined;
if (files?.video && files.video.length > 0) {
const videoFile = files.video[0];
validateVideoSize(videoFile);
video = {
originalname: videoFile.originalname,
mimetype: videoFile.mimetype,
size: videoFile.size,
buffer: videoFile.buffer,
};
}
if (files?.attachments && files.attachments.length > 0) {
validateTotalAttachmentSize(files.attachments);
attachments = files.attachments.map(file => ({
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
buffer: file.buffer,
}));
}
// Validate VIDEO type must have video file (optional - can be uploaded later)
// if (type === 'VIDEO' && !video) {
// throw new ValidationError('Video file is required for VIDEO type lessons');
// }
const input: CreateLessonInput = {
token,
course_id: courseId,
chapter_id: chapterId,
title,
content,
type,
sort_order: sortOrder,
video,
attachments,
};
const result = await chaptersLessonService.createLesson(input);
res.status(201).json(result);
} catch (error) {
next(error);
}
}
}
/**
* Express router middleware wrapper for file upload
* Use this in routes like:
*
* router.post(
* '/api/instructors/courses/:courseId/chapters/:chapterId/lessons',
* authenticateMiddleware,
* handleLessonUpload,
* lessonsController.createLesson.bind(lessonsController)
* );
*/
export const handleLessonUpload = (req: Request, res: Response, next: NextFunction) => {
lessonUpload(req, res, (err) => {
if (err) {
return res.status(400).json({
error: {
code: 'FILE_UPLOAD_ERROR',
message: err.message,
}
});
}
next();
});
};
export const lessonsController = new LessonsController();