mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
course,tasklist,task and completed validation
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
@@ -18,26 +18,26 @@ async function syncObjectivesCreate(Model, parentField, parentId, objectives, tr
|
||||
}
|
||||
|
||||
/**
|
||||
* For UPDATE — upsert by objective_id, hard delete removed ones
|
||||
* For UPDATE — upsert by PK field (default "objective_id"), hard delete removed ones
|
||||
* objectives: [{ objective_id: "123", text: "text1" }, { text: "new" }]
|
||||
*/
|
||||
async function syncObjectivesUpdate(Model, parentField, parentId, objectives, transaction) {
|
||||
async function syncObjectivesUpdate(Model, parentField, parentId, objectives, transaction, pkField = "objective_id") {
|
||||
if (!objectives?.length) {
|
||||
await Model.destroy({ where: { [parentField]: parentId }, force: true, transaction });
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = await Model.findAll({ where: { [parentField]: parentId }, transaction });
|
||||
const existingMap = new Map(existing.map((o) => [String(o.objective_id), o]));
|
||||
const existingMap = new Map(existing.map((o) => [String(o[pkField]), o]));
|
||||
const incomingIds = new Set(
|
||||
objectives.filter((o) => o.objective_id).map((o) => String(o.objective_id))
|
||||
objectives.filter((o) => o[pkField]).map((o) => String(o[pkField]))
|
||||
);
|
||||
|
||||
// Hard delete removed
|
||||
const toDelete = existing.filter((o) => !incomingIds.has(String(o.objective_id)));
|
||||
const toDelete = existing.filter((o) => !incomingIds.has(String(o[pkField])));
|
||||
if (toDelete.length) {
|
||||
await Model.destroy({
|
||||
where: { objective_id: toDelete.map((o) => o.objective_id) },
|
||||
where: { [pkField]: toDelete.map((o) => o[pkField]) },
|
||||
force: true,
|
||||
transaction,
|
||||
});
|
||||
@@ -46,7 +46,7 @@ async function syncObjectivesUpdate(Model, parentField, parentId, objectives, tr
|
||||
// Update existing or create new
|
||||
for (let i = 0; i < objectives.length; i++) {
|
||||
const item = objectives[i];
|
||||
const record = item.objective_id ? existingMap.get(String(item.objective_id)) : null;
|
||||
const record = item[pkField] ? existingMap.get(String(item[pkField])) : null;
|
||||
|
||||
if (record) {
|
||||
await record.update({ text: item.text, order_index: i }, { transaction });
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
"use strict";
|
||||
|
||||
const REF_TYPE_CONFIG = {
|
||||
course: { pk: "course_id" },
|
||||
unit: { pk: "unit_id" },
|
||||
lesson: { pk: "lesson_id" },
|
||||
};
|
||||
|
||||
/**
|
||||
* Enriches CoursePrerequisite rows ({ref_type, ref_id, ...}) with the
|
||||
* referenced entity's title (and subscription tier slug, where the entity
|
||||
* has one) — one batched findAll per ref_type instead of a query per row.
|
||||
* Rows whose referenced entity no longer exists (deleted) get title: null
|
||||
* so callers can drop or flag them instead of crashing.
|
||||
*/
|
||||
async function resolvePrerequisiteTitles(prereqs, { Course, Unit, Lesson }) {
|
||||
if (!prereqs?.length) return prereqs ?? [];
|
||||
|
||||
const models = { course: Course, unit: Unit, lesson: Lesson };
|
||||
// Only Course/Unit carry a subscription tier gate — Lesson has none.
|
||||
const attrsByType = { course: ["title", "subscription"], unit: ["title", "subscription"], lesson: ["title"] };
|
||||
const idsByType = { course: new Set(), unit: new Set(), lesson: new Set() };
|
||||
for (const p of prereqs) {
|
||||
if (idsByType[p.ref_type]) idsByType[p.ref_type].add(p.ref_id);
|
||||
}
|
||||
|
||||
const infoMaps = {};
|
||||
for (const [type, { pk }] of Object.entries(REF_TYPE_CONFIG)) {
|
||||
const ids = [...idsByType[type]];
|
||||
infoMaps[type] = new Map();
|
||||
if (!ids.length) continue;
|
||||
const rows = await models[type].findAll({
|
||||
where: { [pk]: ids },
|
||||
attributes: [pk, ...attrsByType[type]],
|
||||
});
|
||||
for (const row of rows) infoMaps[type].set(String(row[pk]), row);
|
||||
}
|
||||
|
||||
return prereqs.map((p) => {
|
||||
const info = infoMaps[p.ref_type]?.get(String(p.ref_id));
|
||||
return {
|
||||
...p,
|
||||
title: info?.title ?? null,
|
||||
subscription: info?.subscription ?? null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Enriches prerequisite rows (already carrying `title`) with `completed` —
|
||||
* whether the given learner has finished the referenced course/unit/lesson.
|
||||
* "Completed" means: a Certificate exists for a course prereq, or the
|
||||
* matching UnitReadingProgress/LessonReadingProgress row has status
|
||||
* "completed" for a unit/lesson prereq.
|
||||
*/
|
||||
async function resolvePrerequisiteCompletion(prereqs, { Certificate, UnitReadingProgress, LessonReadingProgress }, userId) {
|
||||
if (!prereqs?.length) return prereqs ?? [];
|
||||
|
||||
const idsByType = { course: new Set(), unit: new Set(), lesson: new Set() };
|
||||
for (const p of prereqs) {
|
||||
if (idsByType[p.ref_type]) idsByType[p.ref_type].add(p.ref_id);
|
||||
}
|
||||
|
||||
const completedIds = { course: new Set(), unit: new Set(), lesson: new Set() };
|
||||
|
||||
const courseIds = [...idsByType.course];
|
||||
if (courseIds.length) {
|
||||
const certs = await Certificate.findAll({
|
||||
where: { user_id: userId, course_id: courseIds },
|
||||
attributes: ["course_id"],
|
||||
});
|
||||
certs.forEach((c) => completedIds.course.add(String(c.course_id)));
|
||||
}
|
||||
|
||||
const unitIds = [...idsByType.unit];
|
||||
if (unitIds.length) {
|
||||
const rows = await UnitReadingProgress.findAll({
|
||||
where: { user_id: userId, unit_id: unitIds, status: "completed" },
|
||||
attributes: ["unit_id"],
|
||||
});
|
||||
rows.forEach((r) => completedIds.unit.add(String(r.unit_id)));
|
||||
}
|
||||
|
||||
const lessonIds = [...idsByType.lesson];
|
||||
if (lessonIds.length) {
|
||||
const rows = await LessonReadingProgress.findAll({
|
||||
where: { user_id: userId, lesson_id: lessonIds, status: "completed" },
|
||||
attributes: ["lesson_id"],
|
||||
});
|
||||
rows.forEach((r) => completedIds.lesson.add(String(r.lesson_id)));
|
||||
}
|
||||
|
||||
return prereqs.map((p) => ({
|
||||
...p,
|
||||
completed: completedIds[p.ref_type]?.has(String(p.ref_id)) ?? false,
|
||||
}));
|
||||
}
|
||||
|
||||
module.exports = { resolvePrerequisiteTitles, resolvePrerequisiteCompletion };
|
||||
@@ -0,0 +1,63 @@
|
||||
"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 };
|
||||
Reference in New Issue
Block a user