Initial commit: teaching institution management API.

Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
This commit is contained in:
2026-08-09 04:18:08 +02:00
commit f04c797be6
107 changed files with 9190 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
// /components/courses/courseService.js
const Course = require('./courseModel');
const Professor = require('../professors/professorModel');
const AppError = require('../../utils/AppError');
const eventEmitter = require('../../events/eventEmitter');
const EVENT_NAMES = require('../../constants/eventNames');
const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination');
const normalizeHighlights = (value) => {
if (!Array.isArray(value)) return undefined;
return value.map((item) => String(item).trim()).filter(Boolean);
};
const createCourse = async (data, actorId = null) => {
if (data.professor) {
const professor = await Professor.findById(data.professor);
if (!professor) {
throw new AppError('PROFESSOR_NOT_FOUND');
}
}
const payload = {
...data,
highlights: normalizeHighlights(data.highlights) ?? data.highlights
};
const course = await Course.create(payload);
if (data.professor) {
await Professor.findByIdAndUpdate(data.professor, { $addToSet: { courses: course._id } });
}
eventEmitter.emit(EVENT_NAMES.COURSE_CREATED, { courseId: course._id, title: course.title, actorId });
return course;
};
const getCourseById = async (id, { publicOnly = false } = {}) => {
const filter = { _id: id };
if (publicOnly) filter.showOnFrontend = { $ne: false };
const course = await Course.findOne(filter).populate('professor', 'name surname title expertise email phoneNumber');
if (!course) {
throw new AppError('COURSE_NOT_FOUND');
}
return course;
};
const getAllCourses = async (queryParams, { publicOnly = false } = {}) => {
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
const filter = buildFilterQuery(queryParams, ['title', 'description'], [
'page', 'limit', 'sortBy', 'sortOrder', 'q', 'lang', 'sort'
]);
if (publicOnly) {
// Include legacy docs that predate the field (treat missing as visible)
filter.showOnFrontend = { $ne: false };
}
// Support legacy ?sort=-createdAt style from frontend
let finalSort = sort;
if (queryParams.sort && typeof queryParams.sort === 'string') {
const raw = queryParams.sort.trim();
if (raw.startsWith('-')) {
finalSort = { [raw.slice(1)]: -1 };
} else {
finalSort = { [raw]: 1 };
}
}
const [courses, totalCount] = await Promise.all([
Course.find(filter).populate('professor', 'name surname').sort(finalSort).skip(skip).limit(limit),
Course.countDocuments(filter)
]);
const meta = calculateMeta(totalCount, page, limit);
return { data: courses, meta };
};
const updateCourse = async (id, updateData, actorId = null) => {
const course = await Course.findById(id);
if (!course) {
throw new AppError('COURSE_NOT_FOUND');
}
if (updateData.professor && updateData.professor !== String(course.professor)) {
const professor = await Professor.findById(updateData.professor);
if (!professor) throw new AppError('PROFESSOR_NOT_FOUND');
if (course.professor) {
await Professor.findByIdAndUpdate(course.professor, { $pull: { courses: course._id } });
}
await Professor.findByIdAndUpdate(updateData.professor, { $addToSet: { courses: course._id } });
}
if (updateData.price !== undefined && updateData.price !== course.price) {
eventEmitter.emit(EVENT_NAMES.COURSE_PRICE_CHANGED, {
courseId: course._id,
oldPrice: course.price,
newPrice: updateData.price,
actorId
});
}
if (updateData.highlights !== undefined) {
updateData.highlights = normalizeHighlights(updateData.highlights) || [];
}
Object.assign(course, updateData);
await course.save();
return course;
};
const deleteCourse = async (id) => {
const course = await Course.findById(id);
if (!course) {
throw new AppError('COURSE_NOT_FOUND');
}
if (course.professor) {
await Professor.findByIdAndUpdate(course.professor, { $pull: { courses: course._id } });
}
await Course.findByIdAndDelete(id);
return null;
};
const searchCourses = async (queryParams) => {
return getAllCourses(queryParams);
};
module.exports = {
createCourse,
getCourseById,
getAllCourses,
updateCourse,
deleteCourse,
searchCourses
};