Initial commit: teaching institution management API.
Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
This commit is contained in:
@@ -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
|
||||
};
|
||||
Reference in New Issue
Block a user