Files
gameno-api/components/certificates/certificateService.js
T
kavehhn bf5e66fa95 feat: add append-only course/user data import and expand user profile
Support uploading structured JSON to create courses, classes, and students without wiping existing data, and merge user name fields while adding gender and registration details from source sheets.
2026-08-15 01:31:44 +03:30

182 lines
5.8 KiB
JavaScript

// /components/certificates/certificateService.js
const path = require('path');
const Certificate = require('./certificateModel');
const User = require('../users/userModel');
const AppError = require('../../utils/AppError');
const { commitTempFile, generatePresignedUrl, deleteFromBucket } = require('../../utils/s3Client');
const eventEmitter = require('../../events/eventEmitter');
const EVENT_NAMES = require('../../constants/eventNames');
const { parsePaginationAndSort, buildFilterQuery, calculateMeta } = require('../../utils/pagination');
const createCertificate = async (data) => {
const user = await User.findById(data.user);
if (!user) throw new AppError('USER_NOT_FOUND');
// Move file from Temp bucket to Permanent Storage bucket
const targetKey = `certificates/cert-${Date.now()}-${path.basename(data.tempFileName)}`;
const { fileKey } = await commitTempFile(data.tempFileName, targetKey);
const certificate = await Certificate.create({
user: data.user,
course: data.course || null,
title: data.title,
issuer: data.issuer || '',
fileKey,
originalName: data.originalName || path.basename(data.tempFileName),
isOfficial: data.isOfficial || false
});
user.certificates.push(certificate._id);
await user.save();
eventEmitter.emit(EVENT_NAMES.CERTIFICATE_ISSUED, { certificateId: certificate._id, userId: user._id });
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
};
const getCertificateById = async (id) => {
const certificate = await Certificate.findById(id)
.populate('user', 'name username nationalIdCode')
.populate('course', 'title type');
if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND');
}
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
};
const getAllCertificates = async (queryParams) => {
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
const filter = buildFilterQuery(queryParams, ['title', 'issuer']);
const [certificates, totalCount] = await Promise.all([
Certificate.find(filter)
.populate('user', 'name username')
.populate('course', 'title')
.sort(sort)
.skip(skip)
.limit(limit),
Certificate.countDocuments(filter)
]);
const listWithUrls = await Promise.all(
certificates.map(async (cert) => {
const item = cert.toObject();
item.presignedUrl = await generatePresignedUrl(cert.fileKey);
return item;
})
);
const meta = calculateMeta(totalCount, page, limit);
return { data: listWithUrls, meta };
};
const updateCertificate = async (id, updateData) => {
const certificate = await Certificate.findById(id);
if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND');
}
if (updateData.tempFileName) {
const targetKey = `certificates/cert-${Date.now()}-${path.basename(updateData.tempFileName)}`;
const { fileKey } = await commitTempFile(updateData.tempFileName, targetKey);
// Delete old file from storage bucket
await deleteFromBucket(certificate.fileKey);
certificate.fileKey = fileKey;
}
if (updateData.title) certificate.title = updateData.title;
if (updateData.issuer !== undefined) certificate.issuer = updateData.issuer;
if (updateData.isOfficial !== undefined) certificate.isOfficial = updateData.isOfficial;
await certificate.save();
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
};
const deleteCertificate = async (id) => {
const certificate = await Certificate.findById(id);
if (!certificate) {
throw new AppError('CERTIFICATE_NOT_FOUND');
}
await deleteFromBucket(certificate.fileKey);
await User.findByIdAndUpdate(certificate.user, { $pull: { certificates: certificate._id } });
await Certificate.findByIdAndDelete(id);
return null;
};
const searchCertificates = async (queryParams) => {
return getAllCertificates(queryParams);
};
const uploadUserCertificate = async (userId, data) => {
const user = await User.findById(userId);
if (!user) throw new AppError('USER_NOT_FOUND');
const targetKey = `certificates/cert-${Date.now()}-${path.basename(data.tempFileName)}`;
const { fileKey } = await commitTempFile(data.tempFileName, targetKey);
const certificate = await Certificate.create({
user: userId,
course: data.course || null,
title: data.title,
issuer: data.issuer || 'Self-Uploaded',
fileKey,
originalName: data.originalName || path.basename(data.tempFileName),
isOfficial: false
});
user.certificates.push(certificate._id);
await user.save();
eventEmitter.emit(EVENT_NAMES.CERTIFICATE_UPLOADED, { certificateId: certificate._id, userId });
const resultObj = certificate.toObject();
resultObj.presignedUrl = await generatePresignedUrl(certificate.fileKey);
return resultObj;
};
const getMyCertificates = async (userId, queryParams) => {
const filter = { user: userId, ...buildFilterQuery(queryParams) };
const { page, limit, skip, sort } = parsePaginationAndSort(queryParams);
const [certificates, totalCount] = await Promise.all([
Certificate.find(filter)
.populate('course', 'title type')
.sort(sort)
.skip(skip)
.limit(limit),
Certificate.countDocuments(filter)
]);
const listWithUrls = await Promise.all(
certificates.map(async (cert) => {
const item = cert.toObject();
item.presignedUrl = await generatePresignedUrl(cert.fileKey);
return item;
})
);
const meta = calculateMeta(totalCount, page, limit);
return { data: listWithUrls, meta };
};
module.exports = {
createCertificate,
getCertificateById,
getAllCertificates,
updateCertificate,
deleteCertificate,
searchCertificates,
uploadUserCertificate,
getMyCertificates
};