feat: Implement chapter and lesson management with new services and types, and introduce Minio service.
This commit is contained in:
parent
40b95ad902
commit
6bbbde062a
8 changed files with 382 additions and 6 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Delete, Controller, Security, Request, Put, Path } from 'tsoa';
|
import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Delete, Controller, Security, Request, Put, Path } from 'tsoa';
|
||||||
import { ValidationError } from '../middleware/errorHandler';
|
import { ValidationError } from '../middleware/errorHandler';
|
||||||
import { CategoryService } from '../services/categories.service';
|
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')
|
@Route('api/categories')
|
||||||
@Tags('Categories')
|
@Tags('Categories')
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Delete, Controller, Security, Request, Put, Path } from 'tsoa';
|
import { Get, Body, Post, Route, Tags, SuccessResponse, Response, Delete, Controller, Security, Request, Put, Path } from 'tsoa';
|
||||||
import { ValidationError } from '../middleware/errorHandler';
|
import { ValidationError } from '../middleware/errorHandler';
|
||||||
import { listCourseResponse } from '../types/courses.types';
|
import { listCourseResponse } from '../types/Courses.types';
|
||||||
import { CoursesService } from '../services/courses.service';
|
import { CoursesService } from '../services/courses.service';
|
||||||
|
|
||||||
@Route('api/courses')
|
@Route('api/courses')
|
||||||
|
|
|
||||||
56
Backend/src/services/ChaptersLesson.service.ts
Normal file
56
Backend/src/services/ChaptersLesson.service.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -143,7 +143,7 @@ export class CoursesInstructorService {
|
||||||
|
|
||||||
const course = await prisma.course.update({
|
const course = await prisma.course.update({
|
||||||
where: {
|
where: {
|
||||||
id: courseInstructorId.user_id
|
id: courseId
|
||||||
},
|
},
|
||||||
data: courseData
|
data: courseData
|
||||||
});
|
});
|
||||||
|
|
@ -167,7 +167,7 @@ export class CoursesInstructorService {
|
||||||
|
|
||||||
const course = await prisma.course.delete({
|
const course = await prisma.course.delete({
|
||||||
where: {
|
where: {
|
||||||
id: courseInstructorId.user_id
|
id: courseId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
0
Backend/src/services/Minio.service.ts
Normal file
0
Backend/src/services/Minio.service.ts
Normal file
|
|
@ -3,7 +3,7 @@ import { Prisma } from '@prisma/client';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { logger } from '../config/logger';
|
import { logger } from '../config/logger';
|
||||||
import jwt from 'jsonwebtoken';
|
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';
|
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
||||||
|
|
||||||
export class CategoryService {
|
export class CategoryService {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { prisma } from '../config/database';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { logger } from '../config/logger';
|
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';
|
import { UnauthorizedError, ValidationError, ForbiddenError } from '../middleware/errorHandler';
|
||||||
|
|
||||||
export class CoursesService {
|
export class CoursesService {
|
||||||
|
|
|
||||||
320
Backend/src/types/ChaptersLesson.typs.ts
Normal file
320
Backend/src/types/ChaptersLesson.typs.ts
Normal 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[];
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue