feat: Add student and instructor chapter/lesson controllers and refactor instructor lesson file upload endpoints with TSOA.
This commit is contained in:
parent
0c369d1197
commit
9a7eb50d17
3 changed files with 949 additions and 249 deletions
|
|
@ -1,35 +1,27 @@
|
|||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ChaptersLessonService } from '../services/ChaptersLesson.service';
|
||||
import { Body, FormField, Path, Post, Put, Request, Response, Route, Security, SuccessResponse, Tags, UploadedFile, UploadedFiles } from 'tsoa';
|
||||
import { ValidationError } from '../middleware/errorHandler';
|
||||
import {
|
||||
lessonUpload,
|
||||
LessonUploadRequest,
|
||||
validateTotalAttachmentSize,
|
||||
validateVideoSize
|
||||
} from '../middleware/upload';
|
||||
import { UploadedFileInfo, CreateLessonInput } from '../types/ChaptersLesson.typs';
|
||||
import { ChaptersLessonService } from '../services/ChaptersLesson.service';
|
||||
import { MultiLanguageText } from '../types/index';
|
||||
import { UploadedFileInfo, CreateLessonInput, CreateLessonResponse, UpdateLessonResponse } from '../types/ChaptersLesson.typs';
|
||||
|
||||
const chaptersLessonService = new ChaptersLessonService();
|
||||
|
||||
/**
|
||||
* Controller for handling lesson CRUD operations with file uploads
|
||||
*/
|
||||
@Route('api/instructors/courses/{courseId}/chapters/{chapterId}/lessons')
|
||||
@Tags('Lessons - File Upload')
|
||||
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, เอกสาร, รูปภาพ)
|
||||
* @param courseId Course ID
|
||||
* @param chapterId Chapter ID
|
||||
* @param title ชื่อบทเรียน (JSON string: { th: "", en: "" })
|
||||
* @param type ประเภทบทเรียน (VIDEO | QUIZ)
|
||||
* @param content เนื้อหาบทเรียน (JSON string: { th: "", en: "" })
|
||||
* @param sort_order ลำดับการแสดงผล
|
||||
* @param video ไฟล์วิดีโอ (สำหรับ type=VIDEO เท่านั้น)
|
||||
* @param attachments ไฟล์แนบ (PDFs, เอกสาร, รูปภาพ)
|
||||
*/
|
||||
async createLesson(req: LessonUploadRequest, res: Response, next: NextFunction): Promise<void> {
|
||||
try {
|
||||
|
|
@ -38,8 +30,8 @@ export class LessonsController {
|
|||
throw new ValidationError('No token provided');
|
||||
}
|
||||
|
||||
const courseId = parseInt(req.params.courseId as string, 10);
|
||||
const chapterId = parseInt(req.params.chapterId as string, 10);
|
||||
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');
|
||||
|
|
@ -51,89 +43,162 @@ export class LessonsController {
|
|||
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);
|
||||
if (!parsedTitle.th || !parsedTitle.en) {
|
||||
throw new ValidationError('Title must have both Thai (th) and English (en) values');
|
||||
}
|
||||
|
||||
// Transform files to UploadedFileInfo
|
||||
let videoInfo: UploadedFileInfo | undefined;
|
||||
let attachmentsInfo: UploadedFileInfo[] | undefined;
|
||||
|
||||
if (video) {
|
||||
videoInfo = {
|
||||
originalname: video.originalname,
|
||||
mimetype: video.mimetype,
|
||||
size: video.size,
|
||||
buffer: video.buffer,
|
||||
};
|
||||
}
|
||||
|
||||
if (attachments && attachments.length > 0) {
|
||||
attachmentsInfo = attachments.map(file => ({
|
||||
originalname: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
buffer: file.buffer,
|
||||
}));
|
||||
}
|
||||
|
||||
const input: CreateLessonInput = {
|
||||
token,
|
||||
course_id: courseId,
|
||||
chapter_id: chapterId,
|
||||
title: parsedTitle,
|
||||
content: parsedContent,
|
||||
type,
|
||||
sort_order: sortOrder,
|
||||
video: videoInfo,
|
||||
attachments: attachmentsInfo,
|
||||
};
|
||||
|
||||
return await chaptersLessonService.createLesson(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* เพิ่มวิดีโอและไฟล์แนบให้บทเรียน VIDEO ที่มีอยู่แล้ว
|
||||
* Add video and attachments to an existing VIDEO type lesson
|
||||
*
|
||||
* @param courseId Course ID
|
||||
* @param chapterId Chapter ID
|
||||
* @param lessonId Lesson ID
|
||||
* @param video ไฟล์วิดีโอ (required)
|
||||
* @param attachments ไฟล์แนบ
|
||||
*/
|
||||
@Post('{lessonId}/video')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Video added successfully')
|
||||
@Response('400', 'Validation error')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
@Response('404', 'Lesson not found')
|
||||
public async addVideoToLesson(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Path() lessonId: number,
|
||||
@UploadedFile() video: Express.Multer.File,
|
||||
@UploadedFiles() attachments?: Express.Multer.File[]
|
||||
): Promise<CreateLessonResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
|
||||
if (!video) {
|
||||
throw new ValidationError('Video file is required');
|
||||
}
|
||||
|
||||
// Transform files to UploadedFileInfo
|
||||
const videoInfo: UploadedFileInfo = {
|
||||
originalname: video.originalname,
|
||||
mimetype: video.mimetype,
|
||||
size: video.size,
|
||||
buffer: video.buffer,
|
||||
};
|
||||
|
||||
let attachmentsInfo: UploadedFileInfo[] | undefined;
|
||||
if (attachments && attachments.length > 0) {
|
||||
attachmentsInfo = attachments.map(file => ({
|
||||
originalname: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
buffer: file.buffer,
|
||||
}));
|
||||
}
|
||||
|
||||
return await chaptersLessonService.addVideoLesson({
|
||||
token,
|
||||
course_id: courseId,
|
||||
lesson_id: lessonId,
|
||||
video: videoInfo,
|
||||
attachments: attachmentsInfo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* อัปเดตวิดีโอและไฟล์แนบของบทเรียน VIDEO
|
||||
* Update video and attachments of an existing VIDEO type lesson
|
||||
*
|
||||
* @param courseId Course ID
|
||||
* @param chapterId Chapter ID
|
||||
* @param lessonId Lesson ID
|
||||
* @param video ไฟล์วิดีโอใหม่
|
||||
* @param attachments ไฟล์แนบใหม่
|
||||
*/
|
||||
@Put('{lessonId}/video')
|
||||
@Security('jwt', ['instructor'])
|
||||
@SuccessResponse('200', 'Video updated successfully')
|
||||
@Response('400', 'Validation error')
|
||||
@Response('401', 'Unauthorized')
|
||||
@Response('403', 'Forbidden')
|
||||
@Response('404', 'Lesson not found')
|
||||
public async updateVideoLesson(
|
||||
@Request() request: any,
|
||||
@Path() courseId: number,
|
||||
@Path() chapterId: number,
|
||||
@Path() lessonId: number,
|
||||
@UploadedFile() video?: Express.Multer.File,
|
||||
@UploadedFiles() attachments?: Express.Multer.File[]
|
||||
): Promise<UpdateLessonResponse> {
|
||||
const token = request.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) throw new ValidationError('No token provided');
|
||||
|
||||
// Transform files to UploadedFileInfo
|
||||
let videoInfo: UploadedFileInfo | undefined;
|
||||
let attachmentsInfo: UploadedFileInfo[] | undefined;
|
||||
|
||||
if (video) {
|
||||
videoInfo = {
|
||||
originalname: video.originalname,
|
||||
mimetype: video.mimetype,
|
||||
size: video.size,
|
||||
buffer: video.buffer,
|
||||
};
|
||||
}
|
||||
|
||||
if (attachments && attachments.length > 0) {
|
||||
attachmentsInfo = attachments.map(file => ({
|
||||
originalname: file.originalname,
|
||||
mimetype: file.mimetype,
|
||||
size: file.size,
|
||||
buffer: file.buffer,
|
||||
}));
|
||||
}
|
||||
|
||||
return await chaptersLessonService.updateVideoLesson({
|
||||
token,
|
||||
course_id: courseId,
|
||||
lesson_id: lessonId,
|
||||
video: videoInfo,
|
||||
attachments: attachmentsInfo,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue