feat: normalize chapter sort_order to prevent gaps and duplicates in course chapters

Add normalizeChapterSortOrder method to ensure sequential 1-based sort_order values. Update reorderChapter to normalize before reordering and validate new position against chapter count. Call normalization after chapter deletion to fill gaps in remaining chapters.
This commit is contained in:
JakkrapartXD 2026-02-02 16:27:27 +07:00
parent 2b248cad10
commit ec67c7c6b6

View file

@ -167,6 +167,10 @@ export class ChaptersLessonService {
throw new ForbiddenError('You are not permitted to delete chapter');
}
await prisma.chapter.delete({ where: { id: chapter_id } });
// Normalize sort_order for remaining chapters (fill gaps)
await this.normalizeChapterSortOrder(course_id);
return { code: 200, message: 'Chapter deleted successfully' };
} catch (error) {
logger.error(`Error deleting chapter: ${error}`);
@ -197,10 +201,22 @@ export class ChaptersLessonService {
if (currentChapter.course_id !== course_id) {
throw new NotFoundError('Chapter not found in this course');
}
const oldSortOrder = currentChapter.sort_order;
// First, normalize sort_order to fix any gaps or duplicates
await this.normalizeChapterSortOrder(course_id);
// Re-fetch the chapter to get updated sort_order after normalization
const normalizedChapter = await prisma.chapter.findUnique({ where: { id: chapter_id } });
if (!normalizedChapter) throw new NotFoundError('Chapter not found');
const oldSortOrder = normalizedChapter.sort_order;
// Get total chapter count to validate sort_order (1-based)
const chapterCount = await prisma.chapter.count({ where: { course_id } });
const validNewSortOrder = Math.max(1, Math.min(newSortOrder, chapterCount));
// If same position, no need to reorder
if (oldSortOrder === newSortOrder) {
if (oldSortOrder === validNewSortOrder) {
const chapters = await prisma.chapter.findMany({
where: { course_id },
orderBy: { sort_order: 'asc' }
@ -209,12 +225,12 @@ export class ChaptersLessonService {
}
// Shift other chapters to make room for the insert
if (oldSortOrder > newSortOrder) {
if (oldSortOrder > validNewSortOrder) {
// Moving up: shift chapters between newSortOrder and oldSortOrder-1 down by 1
await prisma.chapter.updateMany({
where: {
course_id,
sort_order: { gte: newSortOrder, lt: oldSortOrder }
sort_order: { gte: validNewSortOrder, lt: oldSortOrder }
},
data: { sort_order: { increment: 1 } }
});
@ -223,14 +239,14 @@ export class ChaptersLessonService {
await prisma.chapter.updateMany({
where: {
course_id,
sort_order: { gt: oldSortOrder, lte: newSortOrder }
sort_order: { gt: oldSortOrder, lte: validNewSortOrder }
},
data: { sort_order: { decrement: 1 } }
});
}
// Update the target chapter to the new position
await prisma.chapter.update({ where: { id: chapter_id }, data: { sort_order: newSortOrder } });
await prisma.chapter.update({ where: { id: chapter_id }, data: { sort_order: validNewSortOrder } });
// Fetch all chapters with updated order
const chapters = await prisma.chapter.findMany({
@ -1265,6 +1281,34 @@ export class ChaptersLessonService {
logger.info(`Normalized sort_order for chapter ${chapterId}: ${lessons.length} lessons`);
}
/**
* Normalize sort_order for all chapters in a course
* Ensures sort_order is sequential starting from 1 with no gaps or duplicates
*
* @param courseId Course ID to normalize
*/
private async normalizeChapterSortOrder(courseId: number): Promise<void> {
// Get all chapters ordered by current sort_order
const chapters = await prisma.chapter.findMany({
where: { course_id: courseId },
orderBy: { sort_order: 'asc' },
select: { id: true, sort_order: true }
});
if (chapters.length === 0) return;
// Update each chapter with sequential sort_order starting from 1
const updates = chapters.map((chapter, index) =>
prisma.chapter.update({
where: { id: chapter.id },
data: { sort_order: index + 1 }
})
);
await prisma.$transaction(updates);
logger.info(`Normalized sort_order for course ${courseId}: ${chapters.length} chapters`);
}
/**
* Quiz
* Update quiz settings (title, passing_score, time_limit, etc.)