Express/MongoDB backend with JWT auth, RBAC, S3 uploads, notifications, and scheduled jobs.
140 lines
4.1 KiB
JavaScript
140 lines
4.1 KiB
JavaScript
// /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
|
|
};
|