mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -0,0 +1,114 @@
|
||||
"use strict";
|
||||
|
||||
/***********************************************************************************************************************************************************************
|
||||
* File Name: hierarchy.util.js
|
||||
* Type of Program: Utility
|
||||
* Description: Helpers for the junction-based course hierarchy
|
||||
* (courses ⇄ course_units ⇄ units ⇄ unit_lessons ⇄ lessons).
|
||||
*
|
||||
* Sequelize belongsToMany includes surface the junction row under the through-
|
||||
* model key ("CourseUnit" / "UnitLesson"). These helpers flatten that back to
|
||||
* the flat `order_index` field the API has always exposed, so response shapes
|
||||
* stay identical to the pre-junction era.
|
||||
*
|
||||
* Author: Kenneth Obsequio (@lash0000)
|
||||
* Date Created: Jul. 7, 2026
|
||||
***********************************************************************************************************************************************************************/
|
||||
|
||||
const sequelize = require("../../config/db.config");
|
||||
|
||||
// ── Response flattening ───────────────────────────────────────────────────────
|
||||
|
||||
/** Sort lessons by their unit_lessons.order_index and flatten it onto each row. */
|
||||
function flattenLessons(lessons = []) {
|
||||
return [...lessons]
|
||||
.map((l) => {
|
||||
const { UnitLesson: link, ...rest } = l;
|
||||
return { ...rest, order_index: link?.order_index ?? rest.order_index ?? 0 };
|
||||
})
|
||||
.sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0));
|
||||
}
|
||||
|
||||
/** Sort units by their course_units.order_index, flatten it, and recurse into lessons. */
|
||||
function flattenUnits(units = []) {
|
||||
return [...units]
|
||||
.map((u) => {
|
||||
const { CourseUnit: link, ...rest } = u;
|
||||
return {
|
||||
...rest,
|
||||
order_index: link?.order_index ?? rest.order_index ?? 0,
|
||||
...(rest.lessons ? { lessons: flattenLessons(rest.lessons) } : {}),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0));
|
||||
}
|
||||
|
||||
// ── Junction row maintenance ──────────────────────────────────────────────────
|
||||
|
||||
/** Next order_index for appending to a parent (max + 1, or 0 when empty). */
|
||||
async function nextOrderIndex(JunctionModel, where, transaction) {
|
||||
const max = await JunctionModel.max("order_index", { where, transaction });
|
||||
return Number.isFinite(max) ? max + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a full ordering: ids[i] gets order_index i.
|
||||
* Ignores ids without an existing junction row.
|
||||
*/
|
||||
async function reorderJunction(JunctionModel, parentField, parentId, childField, orderedIds = [], transaction) {
|
||||
await Promise.all(orderedIds.map((id, i) =>
|
||||
JunctionModel.update(
|
||||
{ order_index: i },
|
||||
{ where: { [parentField]: parentId, [childField]: id }, transaction }
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
// ── Structure counts (raw SQL — junction traversals) ──────────────────────────
|
||||
|
||||
/** Count non-archived lessons reachable from a course through its attached units. */
|
||||
async function countCourseLessons(courseId) {
|
||||
const [row] = await sequelize.query(`
|
||||
SELECT CAST(COUNT(DISTINCT l.lesson_id) AS INTEGER) AS total
|
||||
FROM lessons l
|
||||
JOIN unit_lessons ul ON ul.lesson_id = l.lesson_id
|
||||
JOIN units u ON u.unit_id = ul.unit_id AND u."deletedAt" IS NULL
|
||||
JOIN course_units cu ON cu.unit_id = u.unit_id AND cu.course_id = :courseId
|
||||
WHERE l."deletedAt" IS NULL
|
||||
`, { replacements: { courseId }, type: sequelize.QueryTypes.SELECT });
|
||||
return Number(row?.total ?? 0);
|
||||
}
|
||||
|
||||
/** Non-archived unit ids attached to a course, in course order. */
|
||||
async function getCourseUnitIds(courseId) {
|
||||
const rows = await sequelize.query(`
|
||||
SELECT u.unit_id
|
||||
FROM units u
|
||||
JOIN course_units cu ON cu.unit_id = u.unit_id AND cu.course_id = :courseId
|
||||
WHERE u."deletedAt" IS NULL
|
||||
ORDER BY cu.order_index ASC
|
||||
`, { replacements: { courseId }, type: sequelize.QueryTypes.SELECT });
|
||||
return rows.map((r) => r.unit_id);
|
||||
}
|
||||
|
||||
/** Non-archived lesson ids attached to a unit, in unit order. */
|
||||
async function getUnitLessonIds(unitId) {
|
||||
const rows = await sequelize.query(`
|
||||
SELECT l.lesson_id
|
||||
FROM lessons l
|
||||
JOIN unit_lessons ul ON ul.lesson_id = l.lesson_id AND ul.unit_id = :unitId
|
||||
WHERE l."deletedAt" IS NULL
|
||||
ORDER BY ul.order_index ASC
|
||||
`, { replacements: { unitId }, type: sequelize.QueryTypes.SELECT });
|
||||
return rows.map((r) => r.lesson_id);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
flattenLessons,
|
||||
flattenUnits,
|
||||
nextOrderIndex,
|
||||
reorderJunction,
|
||||
countCourseLessons,
|
||||
getCourseUnitIds,
|
||||
getUnitLessonIds,
|
||||
};
|
||||
+22
-14
@@ -59,9 +59,10 @@ async function recomputeUnitDuration(unitId) {
|
||||
const Unit = require("../models/courses/units.mdl");
|
||||
|
||||
const [unitResult] = await Lesson.sequelize.query(`
|
||||
SELECT COALESCE(SUM(duration_seconds), 0) AS total
|
||||
FROM lessons
|
||||
WHERE unit_id = :unitId AND "deletedAt" IS NULL
|
||||
SELECT COALESCE(SUM(l.duration_seconds), 0) AS total
|
||||
FROM lessons l
|
||||
JOIN unit_lessons ul ON ul.lesson_id = l.lesson_id
|
||||
WHERE ul.unit_id = :unitId AND l."deletedAt" IS NULL
|
||||
`, { replacements: { unitId }, type: Lesson.sequelize.QueryTypes.SELECT });
|
||||
|
||||
await Unit.update({ duration_seconds: unitResult.total }, { where: { unit_id: unitId } });
|
||||
@@ -77,9 +78,10 @@ async function recomputeCourseDuration(courseId) {
|
||||
const { Course } = require("../models/courses/courses.mdl");
|
||||
|
||||
const [courseResult] = await Unit.sequelize.query(`
|
||||
SELECT COALESCE(SUM(duration_seconds), 0) AS total
|
||||
FROM units
|
||||
WHERE course_id = :courseId AND "deletedAt" IS NULL
|
||||
SELECT COALESCE(SUM(u.duration_seconds), 0) AS total
|
||||
FROM units u
|
||||
JOIN course_units cu ON cu.unit_id = u.unit_id
|
||||
WHERE cu.course_id = :courseId AND u."deletedAt" IS NULL
|
||||
`, { replacements: { courseId }, type: Unit.sequelize.QueryTypes.SELECT });
|
||||
|
||||
await Course.update({ duration_seconds: courseResult.total }, { where: { course_id: courseId } });
|
||||
@@ -87,12 +89,14 @@ async function recomputeCourseDuration(courseId) {
|
||||
|
||||
/**
|
||||
* Recompute and persist duration_seconds up the chain:
|
||||
* blocks → lesson → unit → course
|
||||
* blocks → lesson → every attached unit → every course those units are attached to.
|
||||
* A lesson may live in many units, and a unit in many courses, so all parents refresh.
|
||||
*/
|
||||
async function recomputeDurations(lessonId) {
|
||||
const Lesson = require("../models/courses/lessons.mdl");
|
||||
const Unit = require("../models/courses/units.mdl");
|
||||
const LessonPage = require("../models/courses/lesson_page.mdl");
|
||||
const UnitLesson = require("../models/courses/unit_lessons.mdl");
|
||||
const CourseUnit = require("../models/courses/course_units.mdl");
|
||||
|
||||
// 1. Lesson duration from blocks
|
||||
const page = await LessonPage.findOne({ where: { lesson_id: lessonId } });
|
||||
@@ -104,13 +108,17 @@ async function recomputeDurations(lessonId) {
|
||||
const safeLessonSecs = isNaN(lessonSecs) ? 0 : lessonSecs;
|
||||
await Lesson.update({ duration_seconds: safeLessonSecs }, { where: { lesson_id: lessonId } });
|
||||
|
||||
// 2. Unit duration — sum of its lessons
|
||||
const lesson = await Lesson.findOne({ where: { lesson_id: lessonId } });
|
||||
await recomputeUnitDuration(lesson.unit_id);
|
||||
// 2. Every unit that contains this lesson
|
||||
const unitLinks = await UnitLesson.findAll({ where: { lesson_id: lessonId }, attributes: ["unit_id"] });
|
||||
const unitIds = [...new Set(unitLinks.map((l) => String(l.unit_id)))];
|
||||
for (const unitId of unitIds) await recomputeUnitDuration(unitId);
|
||||
|
||||
// 3. Course duration — sum of its units
|
||||
const unit = await Unit.findOne({ where: { unit_id: lesson.unit_id } });
|
||||
await recomputeCourseDuration(unit.course_id);
|
||||
// 3. Every course that contains those units
|
||||
if (unitIds.length) {
|
||||
const courseLinks = await CourseUnit.findAll({ where: { unit_id: unitIds }, attributes: ["course_id"] });
|
||||
const courseIds = [...new Set(courseLinks.map((l) => String(l.course_id)))];
|
||||
for (const courseId of courseIds) await recomputeCourseDuration(courseId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user