added: course func()

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-05-16 11:04:36 +08:00
parent c3663572ae
commit 6907e9bb2d
8 changed files with 596 additions and 6 deletions
+441
View File
@@ -0,0 +1,441 @@
const { Op } = require("sequelize");
const sequelize = require("../../config/db.config");
const R = require("../../utils/response.util");
const { paginate } = require("../../utils/paginate.util");
const Course = require("../../models/courses/courses.mdl");
const Unit = require("../../models/courses/units.mdl");
const Lesson = require("../../models/courses/lessons.mdl");
const LessonPage = require("../../models/courses/lesson-page.mdl");
const mdl_Users = require("../../models/users/users.mdl");
const notDeleted = { deletedAt: null };
// ─── Helpers ──────────────────────────────────────────────────────────────────
const auditByFields = ["createdBy", "updatedBy", "deletedBy"];
const adminExclude = [];
// ─── COURSE ───────────────────────────────────────────────────────────────────
exports.getCourses = async (req, res) => {
try {
const result = await paginate(Course, req, {
excludeAttributes: adminExclude,
auditOptions: { mdl_Users, parentAlias: "Course" },
context: "list",
findOptions: { where: { ...notDeleted } },
});
return R.success(res, "Courses retrieved.", result);
} catch (err) {
console.error("[COURSE][GET ALL]", err);
return R.error(res, "Could not retrieve courses.", 500);
}
};
exports.getCourse = async (req, res) => {
try {
const { courseId } = req.params;
const course = await Course.findOne({
where: { course_id: courseId, ...notDeleted },
include: [
{
model: Unit,
as: "units",
where: notDeleted,
required: false,
order_index: [["order", "ASC"]],
},
],
order_index: [[{ model: Unit, as: "units" }, "order_index", "ASC"]],
});
if (!course) return R.error(res, "Course not found.", 404);
return R.success(res, "Course retrieved.", { data: course });
} catch (err) {
console.error("[COURSE][GET ONE]", err);
return R.error(res, "Could not retrieve course.", 500);
}
};
exports.createCourse = async (req, res) => {
try {
const { title, description, order_index } = req.body;
if (!title) return R.error(res, "Title is required.", 400);
const course = await Course.create({
title,
description: description ?? null,
order_index: order_index ?? 0,
createdBy: req.body.createdBy ?? null,
});
return R.success(res, "Course created.", { data: course }, 201);
} catch (err) {
console.error("[COURSE][CREATE]", err);
return R.error(res, "Could not create course.", 500);
}
};
exports.updateCourse = async (req, res) => {
try {
const { courseId } = req.params;
const course = await Course.findOne({ where: { course_id: courseId, ...notDeleted } });
if (!course) return R.error(res, "Course not found.", 404);
const { title, description, order_index } = req.body;
if (title !== undefined) course.title = title;
if (description !== undefined) course.description = description;
if (order_index !== undefined) course.order_index = order_index;
course.updatedBy = req.body.updatedBy ?? null;
await course.save();
return R.success(res, "Course updated.", { data: course });
} catch (err) {
console.error("[COURSE][UPDATE]", err);
return R.error(res, "Could not update course.", 500);
}
};
exports.deleteCourse = async (req, res) => {
try {
const { courseId } = req.params;
const course = await Course.findOne({ where: { course_id: courseId, ...notDeleted } });
if (!course) return R.error(res, "Course not found.", 404);
await course.update({ deletedBy: req.body.deletedBy ?? null });
await course.destroy();
return R.success(res, "Course deleted.");
} catch (err) {
console.error("[COURSE][DELETE]", err);
return R.error(res, "Could not delete course.", 500);
}
};
// ─── UNIT ─────────────────────────────────────────────────────────────────────
exports.getUnits = async (req, res) => {
try {
const { courseId } = req.params;
const course = await Course.findOne({ where: { course_id: courseId, ...notDeleted } });
if (!course) return R.error(res, "Course not found.", 404);
const result = await paginate(Unit, req, {
excludeAttributes: adminExclude,
auditOptions: { mdl_Users, parentAlias: "Unit" },
context: "list",
findOptions: {
where: { course_id: courseId, ...notDeleted },
order: [["order_index", "ASC"]],
},
});
return R.success(res, "Units retrieved.", result);
} catch (err) {
console.error("[UNIT][GET ALL]", err);
return R.error(res, "Could not retrieve units.", 500);
}
};
exports.getUnit = async (req, res) => {
try {
const { courseId, unitId } = req.params;
const unit = await Unit.findOne({
where: { unit_id: unitId, course_id: courseId, ...notDeleted },
include: [
{
model: Lesson,
as: "lessons",
where: notDeleted,
required: false,
order: [["order_index", "ASC"]],
},
],
order: [[{ model: Lesson, as: "lessons" }, "order_index", "ASC"]],
});
if (!unit) return R.error(res, "Unit not found.", 404);
return R.success(res, "Unit retrieved.", { data: unit });
} catch (err) {
console.error("[UNIT][GET ONE]", err);
return R.error(res, "Could not retrieve unit.", 500);
}
};
exports.createUnit = async (req, res) => {
try {
const { courseId } = req.params;
const { title, description, order_index } = req.body;
if (!title) return R.error(res, "Title is required.", 400);
const course = await Course.findOne({ where: { course_id: courseId, ...notDeleted } });
if (!course) return R.error(res, "Course not found.", 404);
const unit = await Unit.create({
course_id: courseId,
title,
description: description ?? null,
order_index: order_index ?? 0,
createdBy: req.body.createdBy ?? null,
});
return R.success(res, "Unit created.", { data: unit }, 201);
} catch (err) {
console.error("[UNIT][CREATE]", err);
return R.error(res, "Could not create unit.", 500);
}
};
exports.updateUnit = async (req, res) => {
try {
const { courseId, unitId } = req.params;
const unit = await Unit.findOne({
where: { unit_id: unitId, course_id: courseId, ...notDeleted },
});
if (!unit) return R.error(res, "Unit not found.", 404);
const { title, description, order_index } = req.body;
if (title !== undefined) unit.title = title;
if (description !== undefined) unit.description = description;
if (order !== undefined) unit.order = order;
unit.updatedBy = req.body.updatedBy ?? null;
await unit.save();
return R.success(res, "Unit updated.", { data: unit });
} catch (err) {
console.error("[UNIT][UPDATE]", err);
return R.error(res, "Could not update unit.", 500);
}
};
exports.deleteUnit = async (req, res) => {
try {
const { courseId, unitId } = req.params;
const unit = await Unit.findOne({
where: { unit_id: unitId, course_id: courseId, ...notDeleted },
});
if (!unit) return R.error(res, "Unit not found.", 404);
await unit.update({ deletedBy: req.body.deletedBy ?? null });
await unit.destroy();
return R.success(res, "Unit deleted.");
} catch (err) {
console.error("[UNIT][DELETE]", err);
return R.error(res, "Could not delete unit.", 500);
}
};
// ─── LESSON ───────────────────────────────────────────────────────────────────
exports.getLessons = async (req, res) => {
try {
const { courseId, unitId } = req.params;
const unit = await Unit.findOne({
where: { unit_id: unitId, course_id: courseId, ...notDeleted },
});
if (!unit) return R.error(res, "Unit not found.", 404);
const result = await paginate(Lesson, req, {
excludeAttributes: adminExclude,
auditOptions: { mdl_Users, parentAlias: "Lesson" },
context: "list",
findOptions: {
where: { unit_id: unitId, ...notDeleted },
order: [["order_index", "ASC"]],
},
});
return R.success(res, "Lessons retrieved.", result);
} catch (err) {
console.error("[LESSON][GET ALL]", err);
return R.error(res, "Could not retrieve lessons.", 500);
}
};
exports.getLesson = async (req, res) => {
try {
const { courseId, unitId, lessonId } = req.params;
const lesson = await Lesson.findOne({
where: { lesson_id: lessonId, unit_id: unitId, ...notDeleted },
include: [
{
model: LessonPage,
as: "page",
required: false,
},
{
model: Unit,
as: "unit",
where: { course_id: courseId, ...notDeleted },
},
],
});
if (!lesson) return R.error(res, "Lesson not found.", 404);
return R.success(res, "Lesson retrieved.", { data: lesson });
} catch (err) {
console.error("[LESSON][GET ONE]", err);
return R.error(res, "Could not retrieve lesson.", 500);
}
};
exports.createLesson = async (req, res) => {
try {
const { courseId, unitId } = req.params;
const { title, description, order_index } = req.body;
if (!title) return R.error(res, "Title is required.", 400);
const unit = await Unit.findOne({
where: { unit_id: unitId, course_id: courseId, ...notDeleted },
});
if (!unit) return R.error(res, "Unit not found.", 404);
const lesson = await Lesson.create({
unit_id: unitId,
title,
description: description ?? null,
order_index: order_index ?? 0,
createdBy: req.body.createdBy ?? null,
});
// ── Auto-create an empty page for this lesson ─────────────────────────────
await LessonPage.create({
lesson_id: lesson.lesson_id,
blocks: [],
createdBy: req.body.createdBy ?? null,
});
return R.success(res, "Lesson created.", { data: lesson }, 201);
} catch (err) {
console.error("[LESSON][CREATE]", err);
return R.error(res, "Could not create lesson.", 500);
}
};
exports.updateLesson = async (req, res) => {
try {
const { courseId, unitId, lessonId } = req.params;
const lesson = await Lesson.findOne({
where: {
lesson_id: lessonId,
unit_id: unitId,
...notDeleted,
},
include: [{
model: Unit,
as: "unit",
where: { course_id: courseId, ...notDeleted },
}],
});
if (!lesson) return R.error(res, "Lesson not found.", 404);
const { title, description, order_index } = req.body;
if (title !== undefined) lesson.title = title;
if (description !== undefined) lesson.description = description;
if (order_index !== undefined) lesson.order_index = order_index;
lesson.updatedBy = req.body.updatedBy ?? null;
await lesson.save();
return R.success(res, "Lesson updated.", { data: lesson });
} catch (err) {
console.error("[LESSON][UPDATE]", err);
return R.error(res, "Could not update lesson.", 500);
}
};
exports.deleteLesson = async (req, res) => {
try {
const { courseId, unitId, lessonId } = req.params;
const lesson = await Lesson.findOne({
where: { lesson_id: lessonId, unit_id: unitId, ...notDeleted },
include: [{
model: Unit,
as: "unit",
where: { course_id: courseId, ...notDeleted },
}],
});
if (!lesson) return R.error(res, "Lesson not found.", 404);
await lesson.update({ deletedBy: req.body.deletedBy ?? null });
await lesson.destroy();
return R.success(res, "Lesson deleted.");
} catch (err) {
console.error("[LESSON][DELETE]", err);
return R.error(res, "Could not delete lesson.", 500);
}
};
// ─── LESSON PAGE ──────────────────────────────────────────────────────────────
exports.getLessonPage = async (req, res) => {
try {
const { lessonId } = req.params;
const page = await LessonPage.findOne({
where: { lesson_id: lessonId },
});
if (!page) return R.error(res, "Lesson page not found.", 404);
return R.success(res, "Lesson page retrieved.", { data: page });
} catch (err) {
console.error("[LESSON PAGE][GET]", err);
return R.error(res, "Could not retrieve lesson page.", 500);
}
};
exports.upsertLessonPage = async (req, res) => {
try {
const { lessonId } = req.params;
const { blocks } = req.body;
if (!Array.isArray(blocks)) return R.error(res, "blocks must be an array.", 400);
const lesson = await Lesson.findOne({ where: { lesson_id: lessonId, ...notDeleted } });
if (!lesson) return R.error(res, "Lesson not found.", 404);
const [page, created] = await LessonPage.upsert({
lesson_id: lessonId,
blocks,
updatedBy: req.body.updatedBy ?? null,
createdBy: req.body.updatedBy ?? null,
}, {
returning: true,
});
return R.success(
res,
created ? "Lesson page created." : "Lesson page updated.",
{ data: page },
created ? 201 : 200,
);
} catch (err) {
console.error("[LESSON PAGE][UPSERT]", err);
return R.error(res, "Could not save lesson page.", 500);
}
};
+32
View File
@@ -0,0 +1,32 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../../config/db.config");
const mdl_Users = require("../users/users.mdl");
const Course = sequelize.define("Course", {
course_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true },
title: { type: DataTypes.STRING(255), allowNull: false, label: "Title" },
description: { type: DataTypes.TEXT, allowNull: true, label: "Description" },
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Order" },
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Updated By" },
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
}, {
tableName: "courses",
timestamps: true,
paranoid: true,
indexes: [
{ fields: ["uuid"] },
{ fields: ["order_index"] },
{ fields: ["deletedAt"] },
],
});
Course.belongsTo(mdl_Users, { as: "creator", foreignKey: "createdBy" });
Course.belongsTo(mdl_Users, { as: "updater", foreignKey: "updatedBy" });
module.exports = Course;
+23
View File
@@ -0,0 +1,23 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../../config/db.config");
const mdl_Users = require("../users/users.mdl");
const Lesson = require("./lessons.mdl");
const LessonPage = sequelize.define("LessonPage", {
page_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
lesson_id: { type: DataTypes.BIGINT, allowNull: false, unique: true },
blocks: { type: DataTypes.JSONB, allowNull: false, defaultValue: [] },
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Updated By" },
}, {
tableName: "lesson_pages",
timestamps: true,
paranoid: false,
});
LessonPage.belongsTo(Lesson, { as: "lesson", foreignKey: "lesson_id" });
LessonPage.belongsTo(mdl_Users, { as: "creator", foreignKey: "createdBy" });
LessonPage.belongsTo(mdl_Users, { as: "updater", foreignKey: "updatedBy" });
Lesson.hasOne(LessonPage, { as: "page", foreignKey: "lesson_id" });
module.exports = LessonPage;
+33
View File
@@ -0,0 +1,33 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../../config/db.config");
const mdl_Users = require("../users/users.mdl");
const Unit = require("./units.mdl");
const Lesson = sequelize.define("Lesson", {
lesson_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true },
unit_id: { type: DataTypes.BIGINT, allowNull: false },
title: { type: DataTypes.STRING(255), allowNull: false, label: "Title" },
description: { type: DataTypes.TEXT, allowNull: true, label: "Description" },
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Order" },
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Updated By" },
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
}, {
tableName: "lessons",
timestamps: true,
paranoid: true,
indexes: [
{ fields: ["uuid"] },
{ fields: ["unit_id"] },
{ fields: ["order_index"] },
{ fields: ["deletedAt"] },
],
});
Lesson.belongsTo(Unit, { as: "unit", foreignKey: "unit_id" });
Lesson.belongsTo(mdl_Users, { as: "creator", foreignKey: "createdBy" });
Lesson.belongsTo(mdl_Users, { as: "updater", foreignKey: "updatedBy" });
Unit.hasMany(Lesson, { as: "lessons", foreignKey: "unit_id" });
module.exports = Lesson;
+33
View File
@@ -0,0 +1,33 @@
const { DataTypes } = require("sequelize");
const sequelize = require("../../config/db.config");
const mdl_Users = require("../users/users.mdl");
const Course = require("./courses.mdl");
const Unit = sequelize.define("Unit", {
unit_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true },
course_id: { type: DataTypes.BIGINT, allowNull: false },
title: { type: DataTypes.STRING(255), allowNull: false, label: "Title" },
description: { type: DataTypes.TEXT, allowNull: true, label: "Description" },
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Order" },
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Updated By" },
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
}, {
tableName: "units",
timestamps: true,
paranoid: true,
indexes: [
{ fields: ["uuid"] },
{ fields: ["course_id"] },
{ fields: ["order_index"] },
{ fields: ["deletedAt"] },
],
});
Unit.belongsTo(Course, { as: "course", foreignKey: "course_id" });
Unit.belongsTo(mdl_Users, { as: "creator", foreignKey: "createdBy" });
Unit.belongsTo(mdl_Users, { as: "updater", foreignKey: "updatedBy" });
Course.hasMany(Unit, { as: "units", foreignKey: "course_id" });
module.exports = Unit;
-4
View File
@@ -53,7 +53,6 @@
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"peer": true,
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
@@ -766,7 +765,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
@@ -813,7 +811,6 @@
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.11.2.tgz",
"integrity": "sha512-a7uwwfNTh1U60ssiIkuLFWHt4hAC5yxlLGU2VP0X4YNlyEDZAqF4tK3GD3NSitVBrCQmQ0++0uOyFOgC2y4DDw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 14"
},
@@ -1830,7 +1827,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.12.0",
"pg-pool": "^3.13.0",
+30
View File
@@ -0,0 +1,30 @@
const router = require("express").Router();
const controller = require("../../controllers/admin/courses.controller");
const { sensitiveOpsLimiter } = require("../../middleware/rateLimiter.middleware");
// ─── Courses ──────────────────────────────────────────────────────────────────
router.get("/", controller.getCourses);
router.post("/", sensitiveOpsLimiter, controller.createCourse);
router.get("/:courseId", controller.getCourse);
router.patch("/:courseId", sensitiveOpsLimiter, controller.updateCourse);
router.delete("/:courseId", sensitiveOpsLimiter, controller.deleteCourse);
// ─── Units ────────────────────────────────────────────────────────────────────
router.get("/:courseId/units", controller.getUnits);
router.post("/:courseId/units", sensitiveOpsLimiter, controller.createUnit);
router.get("/:courseId/units/:unitId", controller.getUnit);
router.patch("/:courseId/units/:unitId", sensitiveOpsLimiter, controller.updateUnit);
router.delete("/:courseId/units/:unitId", sensitiveOpsLimiter, controller.deleteUnit);
// ─── Lessons ──────────────────────────────────────────────────────────────────
router.get("/:courseId/units/:unitId/lessons", controller.getLessons);
router.post("/:courseId/units/:unitId/lessons", sensitiveOpsLimiter, controller.createLesson);
router.get("/:courseId/units/:unitId/lessons/:lessonId", controller.getLesson);
router.patch("/:courseId/units/:unitId/lessons/:lessonId", sensitiveOpsLimiter, controller.updateLesson);
router.delete("/:courseId/units/:unitId/lessons/:lessonId", sensitiveOpsLimiter, controller.deleteLesson);
// ─── Lesson Page ──────────────────────────────────────────────────────────────
router.get("/:courseId/units/:unitId/lessons/:lessonId/page", controller.getLessonPage);
router.put("/:courseId/units/:unitId/lessons/:lessonId/page", sensitiveOpsLimiter, controller.upsertLessonPage);
module.exports = router;
+2
View File
@@ -33,6 +33,7 @@ const authRoutes = require('./routes/auth.routes');
const clientRoutes = require('./routes/client/client.routes');
const staffRoutes = require('./routes/staff/staff.routes');
const adminRoutes = require('./routes/admin/admin.routes');
const courseRoutes = require('./routes/admin/courses.routes');
// ── Models (ensure associations are loaded) ────────────────────────────────────
require('./models/users/users.mdl');
@@ -96,6 +97,7 @@ app.use('/api/auth', authRoutes);
app.use('/api/client', clientRoutes);
app.use('/api/staff', staffRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/admin/courses', courseRoutes);
// Health check
app.get('/api/health', (req, res) => {