feat: normalize lesson sort_order to prevent gaps and duplicates in chapter lessons

Add normalizeLessonSortOrder method to ensure sequential 1-based sort_order values. Update reorderLesson to normalize before reordering and validate new position against lesson count. Call normalization after lesson deletion to fill gaps in remaining lessons.
This commit is contained in:
JakkrapartXD 2026-02-02 16:16:28 +07:00
parent 69e4806831
commit 2b248cad10

View file

@ -483,10 +483,22 @@ export class ChaptersLessonService {
if (currentLesson.chapter_id !== chapter_id) {
throw new NotFoundError('Lesson not found in this chapter');
}
const oldSortOrder = currentLesson.sort_order;
// First, normalize sort_order to fix any gaps or duplicates
await this.normalizeLessonSortOrder(chapter_id);
// Re-fetch the lesson to get updated sort_order after normalization
const normalizedLesson = await prisma.lesson.findUnique({ where: { id: lesson_id } });
if (!normalizedLesson) throw new NotFoundError('Lesson not found');
const oldSortOrder = normalizedLesson.sort_order;
// Get total lesson count to validate sort_order (1-based)
const lessonCount = await prisma.lesson.count({ where: { chapter_id } });
const validNewSortOrder = Math.max(1, Math.min(newSortOrder, lessonCount));
// If same position, no need to reorder
if (oldSortOrder === newSortOrder) {
if (oldSortOrder === validNewSortOrder) {
const lessons = await prisma.lesson.findMany({
where: { chapter_id },
orderBy: { sort_order: 'asc' }
@ -495,12 +507,12 @@ export class ChaptersLessonService {
}
// Shift other lessons to make room for the insert
if (oldSortOrder > newSortOrder) {
if (oldSortOrder > validNewSortOrder) {
// Moving up: shift lessons between newSortOrder and oldSortOrder-1 down by 1
await prisma.lesson.updateMany({
where: {
chapter_id,
sort_order: { gte: newSortOrder, lt: oldSortOrder }
sort_order: { gte: validNewSortOrder, lt: oldSortOrder }
},
data: { sort_order: { increment: 1 } }
});
@ -509,14 +521,14 @@ export class ChaptersLessonService {
await prisma.lesson.updateMany({
where: {
chapter_id,
sort_order: { gt: oldSortOrder, lte: newSortOrder }
sort_order: { gt: oldSortOrder, lte: validNewSortOrder }
},
data: { sort_order: { decrement: 1 } }
});
}
// Update the target lesson to the new position
await prisma.lesson.update({ where: { id: lesson_id }, data: { sort_order: newSortOrder } });
await prisma.lesson.update({ where: { id: lesson_id }, data: { sort_order: validNewSortOrder } })
// Fetch all lessons with updated order
const lessons = await prisma.lesson.findMany({
@ -576,10 +588,16 @@ export class ChaptersLessonService {
}
}
// Get chapter_id before deletion for normalization
const chapterId = lesson.chapter_id;
// Delete lesson (CASCADE will delete: attachments, quiz, questions, choices)
// Based on Prisma schema: onDelete: Cascade
await prisma.lesson.delete({ where: { id: lesson_id } });
// Normalize sort_order for remaining lessons (fill gaps)
await this.normalizeLessonSortOrder(chapterId);
return { code: 200, message: 'Lesson deleted successfully' };
} catch (error) {
logger.error(`Error deleting lesson: ${error}`);
@ -1219,6 +1237,34 @@ export class ChaptersLessonService {
logger.info(`Normalized sort_order for quiz ${quizId}: ${questions.length} questions`);
}
/**
* Normalize sort_order for all lessons in a chapter
* Ensures sort_order is sequential starting from 1 with no gaps or duplicates
*
* @param chapterId Chapter ID to normalize
*/
private async normalizeLessonSortOrder(chapterId: number): Promise<void> {
// Get all lessons ordered by current sort_order
const lessons = await prisma.lesson.findMany({
where: { chapter_id: chapterId },
orderBy: { sort_order: 'asc' },
select: { id: true, sort_order: true }
});
if (lessons.length === 0) return;
// Update each lesson with sequential sort_order starting from 1
const updates = lessons.map((lesson, index) =>
prisma.lesson.update({
where: { id: lesson.id },
data: { sort_order: index + 1 }
})
);
await prisma.$transaction(updates);
logger.info(`Normalized sort_order for chapter ${chapterId}: ${lessons.length} lessons`);
}
/**
* Quiz
* Update quiz settings (title, passing_score, time_limit, etc.)