Files

115 lines
4.6 KiB
JavaScript

"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,
};