feat: Implement chapter and lesson management with new services and types, and introduce Minio service.

This commit is contained in:
JakkrapartXD 2026-01-20 13:39:42 +07:00
parent 40b95ad902
commit 6bbbde062a
8 changed files with 382 additions and 6 deletions

View file

@ -1,7 +1,7 @@
import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Delete, Controller, Security, Request, Put, Path } from 'tsoa';
import { ValidationError } from '../middleware/errorHandler';
import { CategoryService } from '../services/categories.service';
import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, listCategoriesResponse } from '../types/categories.type';
import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, listCategoriesResponse } from '../types/Categories.type';
@Route('api/categories')
@Tags('Categories')

View file

@ -1,6 +1,6 @@
import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Delete, Controller, Security, Request, Put, Path } from 'tsoa';
import { ValidationError } from '../middleware/errorHandler';
import { listCourseResponse } from '../types/courses.types';
import { listCourseResponse } from '../types/Courses.types';
import { CoursesService } from '../services/courses.service';
@Route('api/courses')

View file

@ -0,0 +1,56 @@
import { prisma } from '../config/database';
import { Prisma } from '@prisma/client';
import { config } from '../config';
import { logger } from '../config/logger';
import { UnauthorizedError, ValidationError, ForbiddenError, NotFoundError } from '../middleware/errorHandler';
import jwt from 'jsonwebtoken';
import {
LessonAttachmentData,
LessonData,
ChapterData,
CreateLessonInput,
UpdateLessonInput,
CreateChapterInput,
UpdateChapterInput,
ChaptersRequest,
DeleteChapterRequest,
ReorderChapterRequest,
ListChaptersResponse,
GetChapterResponse,
CreateChapterResponse,
UpdateChapterResponse,
DeleteChapterResponse,
ReorderChapterResponse,
ChapterWithLessonsResponse,
ListLessonsRequest,
GetLessonRequest,
CreateLessonRequest,
UpdateLessonRequest,
DeleteLessonRequest,
ReorderLessonsRequest,
ListLessonsResponse,
GetLessonResponse,
CreateLessonResponse,
UpdateLessonResponse,
DeleteLessonResponse,
ReorderLessonsResponse,
} from "../types/ChaptersLesson.typs";
export class ChaptersLessonService {
async listChapters(request: ChaptersRequest): Promise<ListChaptersResponse> {
try {
const { token, course_id } = request;
const decodedToken = jwt.verify(token, config.jwt.secret) as { id: number };
const user = await prisma.user.findUnique({ where: { id: decodedToken.id } });
if (!user) {
throw new UnauthorizedError('Invalid token');
}
const chapters = await prisma.chapter.findMany({ where: { course_id } });
return { code: 200, message: 'Chapters fetched successfully', data: chapters as ChapterData[], total: chapters.length };
} catch (error) {
logger.error(`Error fetching chapters: ${error}`);
throw error;
}
}
}

View file

@ -143,7 +143,7 @@ export class CoursesInstructorService {
const course = await prisma.course.update({
where: {
id: courseInstructorId.user_id
id: courseId
},
data: courseData
});
@ -167,7 +167,7 @@ export class CoursesInstructorService {
const course = await prisma.course.delete({
where: {
id: courseInstructorId.user_id
id: courseId
}
});
return {

View file

View file

@ -3,7 +3,7 @@ import { Prisma } from '@prisma/client';
import { config } from '../config';
import { logger } from '../config/logger';
import jwt from 'jsonwebtoken';
import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, listCategoriesResponse, Category } from '../types/categories.type';
import { createCategory, createCategoryResponse, deleteCategoryResponse, updateCategory, updateCategoryResponse, listCategoriesResponse, Category } from '../types/Categories.type';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
export class CategoryService {

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 {listCourseResponse, getCourseResponse } from '../types/courses.types';
import { listCourseResponse, getCourseResponse } from '../types/Courses.types';
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
export class CoursesService {

View file

@ -0,0 +1,320 @@
// ============================================
// Multi-language Types
// ============================================
import { MultiLanguageText } from './index';
// Use MultiLanguageText from index.ts for consistency
export type MultiLangText = MultiLanguageText;
// ============================================
// Lesson Types (for nested inclusion)
// ============================================
export interface LessonAttachmentData {
id: number;
lesson_id: number;
file_name: string;
file_path: string;
file_size: number;
mime_type: string;
description: MultiLanguageText | null;
sort_order: number;
created_at: Date;
}
export interface QuizData {
id: number;
lesson_id: number;
title: MultiLanguageText;
description: MultiLanguageText | null;
passing_score: number;
time_limit: number | null;
shuffle_questions: boolean;
shuffle_choices: boolean;
show_answers_after_completion: boolean;
created_at: Date;
created_by: number;
updated_at: Date | null;
updated_by: number | null;
}
export interface LessonProgressData {
id: number;
user_id: number;
lesson_id: number;
is_completed: boolean;
completed_at: Date | null;
video_progress_seconds: number;
video_duration_seconds: number | null;
video_progress_percentage: number | null;
last_watched_at: Date | null;
created_at: Date;
updated_at: Date | null;
}
export interface LessonData {
id: number;
chapter_id: number;
title: MultiLanguageText;
content: MultiLanguageText | null;
type: 'VIDEO' | 'QUIZ';
duration_minutes: number | null;
sort_order: number;
is_sequential: boolean;
prerequisite_lesson_ids: number[] | null;
require_pass_quiz: boolean;
is_published: boolean;
created_at: Date;
updated_at: Date | null;
attachments?: LessonAttachmentData[];
quiz?: QuizData | null;
progress?: LessonProgressData[];
}
// ============================================
// Chapter Types
// ============================================
export interface ChapterData {
id: number;
course_id: number;
title: MultiLanguageText;
description: MultiLanguageText | null;
sort_order: number;
is_published: boolean;
created_at: Date;
updated_at: Date | null;
lessons?: LessonData[];
}
// ============================================
// Request Types
// ============================================
export interface ChaptersRequest {
token: string;
course_id: number;
}
export interface GetChapterRequest {
token: string;
course_id: number;
chapter_id: number;
}
export interface CreateChapterInput {
title: MultiLanguageText;
description?: MultiLanguageText;
sort_order?: number;
is_published?: boolean;
}
export interface CreateChapterRequest {
token: string;
course_id: number;
data: CreateChapterInput;
}
export interface UpdateChapterInput {
title?: MultiLanguageText;
description?: MultiLanguageText;
sort_order?: number;
is_published?: boolean;
}
export interface UpdateChapterRequest {
token: string;
course_id: number;
chapter_id: number;
data: UpdateChapterInput;
}
export interface DeleteChapterRequest {
token: string;
course_id: number;
chapter_id: number;
}
export interface ReorderChapterRequest {
token: string;
course_id: number;
chapter_ids: number[]; // Ordered array of chapter IDs
}
// ============================================
// Response Types
// ============================================
export interface ListChaptersResponse {
code: number;
message: string;
data: ChapterData[];
total: number;
}
export interface GetChapterResponse {
code: number;
message: string;
data: ChapterData;
}
export interface CreateChapterResponse {
code: number;
message: string;
data: ChapterData;
}
export interface UpdateChapterResponse {
code: number;
message: string;
data: ChapterData;
}
export interface DeleteChapterResponse {
code: number;
message: string;
}
export interface ReorderChapterResponse {
code: number;
message: string;
data: ChapterData[];
}
// ============================================
// Chapter with Full Lessons (for detailed view)
// ============================================
export interface ChapterWithLessonsResponse {
code: number;
message: string;
data: ChapterData & {
lessons: LessonData[];
};
}
// ============================================
// Lesson Request Types
// ============================================
export interface ListLessonsRequest {
token: string;
course_id: number;
chapter_id: number;
}
export interface GetLessonRequest {
token: string;
course_id: number;
chapter_id: number;
lesson_id: number;
}
export interface CreateLessonInput {
title: MultiLanguageText;
content?: MultiLanguageText;
type: 'VIDEO' | 'QUIZ';
duration_minutes?: number;
sort_order?: number;
is_sequential?: boolean;
prerequisite_lesson_ids?: number[];
require_pass_quiz?: boolean;
is_published?: boolean;
}
export interface CreateLessonRequest {
token: string;
course_id: number;
chapter_id: number;
data: CreateLessonInput;
}
export interface UpdateLessonInput {
title?: MultiLanguageText;
content?: MultiLanguageText;
type?: 'VIDEO' | 'QUIZ';
duration_minutes?: number;
sort_order?: number;
is_sequential?: boolean;
prerequisite_lesson_ids?: number[];
require_pass_quiz?: boolean;
is_published?: boolean;
}
export interface UpdateLessonRequest {
token: string;
course_id: number;
chapter_id: number;
lesson_id: number;
data: UpdateLessonInput;
}
export interface DeleteLessonRequest {
token: string;
course_id: number;
chapter_id: number;
lesson_id: number;
}
export interface ReorderLessonsRequest {
token: string;
course_id: number;
chapter_id: number;
lesson_ids: number[]; // Ordered array of lesson IDs
}
// ============================================
// Lesson Response Types
// ============================================
export interface ListLessonsResponse {
code: number;
message: string;
data: LessonData[];
total: number;
}
export interface GetLessonResponse {
code: number;
message: string;
data: LessonData;
}
export interface CreateLessonResponse {
code: number;
message: string;
data: LessonData;
}
export interface UpdateLessonResponse {
code: number;
message: string;
data: LessonData;
}
export interface DeleteLessonResponse {
code: number;
message: string;
}
export interface ReorderLessonsResponse {
code: number;
message: string;
data: LessonData[];
}
// ============================================
// Lesson with Full Details (attachments, quiz, progress)
// ============================================
export interface LessonWithDetailsResponse {
code: number;
message: string;
data: LessonData & {
attachments: LessonAttachmentData[];
quiz: QuizData | null;
progress: LessonProgressData[];
};
}