Files
starr-philproperties/apps/api/utils/courses/taskPrerequisites.util.js
T

64 lines
2.5 KiB
JavaScript

"use strict";
/***********************************************************************************************************************************************************************
* File Name: taskPrerequisites.util.js
* Type of Program: Utility
* Description: Cycle detection for the explicit Task ↔ Task prerequisite graph
* (task_prerequisites junction table). A task's prerequisite set is
* only meaningful if the graph stays a DAG — this checks whether
* persisting a proposed edge set would introduce a cycle.
*
* Author: Kenneth Obsequio (@lash0000)
* Date Created: Jul. 17, 2026
***********************************************************************************************************************************************************************/
const { TaskPrerequisite } = require("../../models/task/task.mdl");
/**
* Would setting `taskId`'s prerequisites to `newPrereqIds` introduce a cycle?
*
* Loads every existing task_prerequisites edge for the task list, overlays the
* proposed edges for `taskId` (replacing whatever it currently points to), then
* DFS from `taskId` looking for a path back to itself.
*/
async function wouldCreateCycle(taskId, newPrereqIds, taskListId, transaction) {
const { Task } = require("../../models/task/task.mdl");
const siblingTasks = await Task.findAll({
where: { task_list_id: taskListId },
attributes: ["task_id"],
transaction,
});
const siblingIds = siblingTasks.map((t) => t.task_id);
const existingEdges = await TaskPrerequisite.findAll({
where: { task_id: siblingIds },
attributes: ["task_id", "prerequisite_task_id"],
transaction,
});
// adjacency: task_id -> Set(prerequisite_task_id) ("depends on")
const adjacency = new Map();
for (const { task_id, prerequisite_task_id } of existingEdges) {
if (task_id === taskId) continue; // overlay taskId's edges with the proposed set below
if (!adjacency.has(task_id)) adjacency.set(task_id, new Set());
adjacency.get(task_id).add(prerequisite_task_id);
}
adjacency.set(taskId, new Set(newPrereqIds));
// DFS from taskId looking for a path back to taskId
const visited = new Set();
const stack = [...adjacency.get(taskId)];
while (stack.length) {
const current = stack.pop();
if (current === taskId) return true;
if (visited.has(current)) continue;
visited.add(current);
for (const next of adjacency.get(current) ?? []) stack.push(next);
}
return false;
}
module.exports = { wouldCreateCycle };