This commit is contained in:
rgrgogu
2026-05-21 11:48:57 +08:00
parent db0e6f00f6
commit 7f06bf83f6
7 changed files with 137 additions and 230 deletions
+2 -45
View File
@@ -10,6 +10,7 @@ const R = require('../../utils/response.util');
const { paginate } = require("../../utils/paginate.util");
const { adminExclude, jsonbSchemas, computedAttributes, } = require("../../models/assets/assets.attributes");
const mdl_Users = require('../../models/users/users.mdl');
const { getFieldValues } = require("../../utils/fieldValues.util");
const { Op, Sequelize } = require('sequelize');
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
@@ -581,48 +582,4 @@ exports.getArchivedAssets = async (req, res) => {
}
};
exports.getAssetFieldValues = async (req, res) => {
try {
const { field } = req.query;
if (!field) return R.error(res, "Field is required.", 400);
const allowedFields = Object.keys(Asset.rawAttributes);
const dateFields = ["createdAt", "updatedAt", "deletedAt"];
if (!allowedFields.includes(field))
return R.error(res, "Invalid or restricted field.", 400);
if (auditByFields.includes(field)) {
const [rows] = await sequelize.query(`
SELECT DISTINCT u."personal_info"->'name'->>'full_name' AS value
FROM assets a
JOIN users u ON u.user_id = a."${field}"
WHERE a."${field}" IS NOT NULL
AND u."personal_info"->'name'->>'full_name' IS NOT NULL
ORDER BY value ASC
`);
return R.success(res, "Field values retrieved.", rows.map((r) => r.value).filter(Boolean));
}
if (dateFields.includes(field)) {
const results = await Asset.findAll({
attributes: [[Sequelize.fn("DISTINCT", Sequelize.fn("DATE", Sequelize.col(field))), "value"]],
where: { [field]: { [Op.ne]: null } },
order: [[Sequelize.fn("DATE", Sequelize.col(field)), "DESC"]],
raw: true,
});
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean));
}
const results = await Asset.findAll({
attributes: [[Sequelize.fn("DISTINCT", Sequelize.col(field)), "value"]],
where: { [field]: { [Op.ne]: null } },
raw: true,
});
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean).sort());
} catch (err) {
console.error("[ASSET][GET FIELD VALUES]", err);
return R.error(res, "Could not retrieve field values.", 500);
}
};
exports.getAssetFieldValues = getFieldValues(Asset, "ASSET");
+12 -55
View File
@@ -9,6 +9,7 @@ const { syncObjectivesCreate, syncObjectivesUpdate } = require("../../utils/cour
const { syncJunction } = require("../../utils/courses/junction.util");
const { archiveOne, archiveMany } = require("../../utils/courses/archive.util");
const { restoreOne, restoreMany } = require("../../utils/courses/restore.util");
const { getFieldValues } = require("../../utils/fieldValues.util");
// ── Models ────────────────────────────────────────────────────────────────────
@@ -408,11 +409,11 @@ exports.updateUnit = async (req, res) => {
}
};
exports.deleteUnit = async (req, res) => {
exports.archiveUnit = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId, unitId } = req.params;
const record = await archiveOne(Unit, { unit_id: unitId, course_id: courseId, ...notDeleted }, req.body.deletedBy, t);
const record = await archiveOne(Unit, { unit_id: unitId, course_id: courseId, ...notDeleted }, req.user.user_id, t);
if (!record) return R.error(res, "Unit not found.", 404);
await t.commit();
return R.success(res, "Unit archived.");
@@ -427,13 +428,13 @@ exports.bulkArchiveUnits = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId } = req.params;
const { ids = [], deletedBy } = req.body;
const { ids = [] } = req.body;
if (!ids.length) return R.error(res, "No IDs provided.", 400);
const units = await Unit.findAll({ where: { unit_id: ids, course_id: courseId, ...notDeleted } });
const validIds = units.map((u) => u.unit_id);
const count = await archiveMany(Unit, "unit_id", validIds, deletedBy, t);
const count = await archiveMany(Unit, "unit_id", validIds, req.user.user_id, t);
await t.commit();
return R.success(res, `${count} unit${count !== 1 ? "s" : ""} archived.`);
} catch (err) {
@@ -453,7 +454,7 @@ exports.getArchivedUnits = async (req, res) => {
const result = await paginate(Unit, req, {
excludeAttributes: adminExclude,
auditOptions: { mdl_Users, parentAlias: "Unit" },
context: "list",
context: "archived",
findOptions: {
where: { course_id: courseId, ...onlyDeleted },
paranoid: false,
@@ -487,7 +488,7 @@ exports.restoreUnit = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId, unitId } = req.params;
const record = await restoreOne(Unit, { unit_id: unitId, course_id: courseId, ...onlyDeleted }, req.body.restoredBy, t);
const record = await restoreOne(Unit, { unit_id: unitId, course_id: courseId, ...onlyDeleted }, req.user.user_id, t);
if (!record) return R.error(res, "Archived unit not found.", 404);
await t.commit();
return R.success(res, "Unit restored.", { data: record });
@@ -502,13 +503,13 @@ exports.bulkRestoreUnits = async (req, res) => {
const t = await sequelize.transaction();
try {
const { courseId } = req.params;
const { ids = [], restoredBy } = req.body;
const { ids = [] } = req.body;
if (!ids.length) return R.error(res, "No IDs provided.", 400);
const units = await Unit.findAll({ where: { unit_id: ids, course_id: courseId, ...onlyDeleted }, paranoid: false });
const validIds = units.map((u) => u.unit_id);
const count = await restoreMany(Unit, "unit_id", validIds, restoredBy, t);
const count = await restoreMany(Unit, "unit_id", validIds, req.user.user_id, t);
await t.commit();
return R.success(res, `${count} unit${count !== 1 ? "s" : ""} restored.`);
} catch (err) {
@@ -1315,50 +1316,6 @@ exports.restoreAssessment = async (req, res) => {
}
};
exports.getCourseFieldValues = async (req, res) => {
try {
const { field } = req.query;
if (!field) return R.error(res, "Field is required.", 400);
const allowedFields = Object.keys(Course.rawAttributes).filter(
(key) => Course.rawAttributes[key].filterable === true
);
const dateFields = ["createdAt", "updatedAt", "deletedAt"];
if (!allowedFields.includes(field) && !dateFields.includes(field))
return R.error(res, "Invalid or restricted field.", 400);
if (auditByFields.includes(field)) {
const [rows] = await sequelize.query(`
SELECT DISTINCT u."personal_info"->'name'->>'full_name' AS value
FROM courses c
JOIN users u ON u.user_id = c."${field}"
WHERE c."${field}" IS NOT NULL
AND u."personal_info"->'name'->>'full_name' IS NOT NULL
ORDER BY value ASC
`);
return R.success(res, "Field values retrieved.", rows.map((r) => r.value).filter(Boolean));
}
if (dateFields.includes(field)) {
const results = await Course.findAll({
attributes: [[Sequelize.fn("DISTINCT", Sequelize.fn("DATE", Sequelize.col(field))), "value"]],
where: { [field]: { [Op.ne]: null } },
order: [[Sequelize.fn("DATE", Sequelize.col(field)), "DESC"]],
raw: true,
});
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean));
}
const results = await Course.findAll({
attributes: [[Sequelize.fn("DISTINCT", Sequelize.col(field)), "value"]],
where: { [field]: { [Op.ne]: null } },
raw: true,
});
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean).sort());
} catch (err) {
console.error("[COURSE][GET FIELD VALUES]", err);
return R.error(res, "Could not retrieve field values.", 500);
}
};
exports.getCourseFieldValues = getFieldValues(Course, "COURSE");
exports.getUnitFieldValues = getFieldValues(Unit, "UNIT");
exports.getLessonFieldValues = getFieldValues(Lesson, "LESSON");
+5 -52
View File
@@ -14,6 +14,7 @@ const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/use
const R = require('../../utils/response.util');
const { paginate } = require('../../utils/paginate.util');
const { getFieldValues } = require("../../utils/fieldValues.util");
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed } = require('../../models/users/user_groups.attributes');
@@ -236,58 +237,10 @@ exports.getArchivedGroups = async (req, res) => {
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
exports.getGroupFieldValues = async (req, res) => {
try {
const { field } = req.query;
if (!field) return R.error(res, 'Field is required.', 400);
const allowedFields = Object.keys(mdl_UserGroups.rawAttributes).filter(
(f) => !['deletedAt'].includes(f)
);
const dateFields = ['createdAt', 'updatedAt', 'deletedAt'];
if (!field.includes('.')) {
if (!allowedFields.includes(field))
return R.error(res, 'Invalid or restricted field.', 400);
if (auditByFields.includes(field)) {
const [rows] = await sequelize.query(`
SELECT DISTINCT u."personal_info"->'name'->>'full_name' AS value
FROM user_groups g
JOIN users u ON u.user_id = g."${field}"
WHERE g."${field}" IS NOT NULL
AND u."personal_info"->'name'->>'full_name' IS NOT NULL
ORDER BY value ASC
`);
return R.success(res, 'Field values retrieved.', rows.map((r) => r.value).filter(Boolean));
}
if (dateFields.includes(field)) {
const results = await mdl_UserGroups.findAll({
attributes: [[Sequelize.fn('DISTINCT', Sequelize.fn('DATE', Sequelize.col(field))), 'value']],
where: { [field]: { [Op.ne]: null } },
order: [[Sequelize.fn('DATE', Sequelize.col(field)), 'DESC']],
paranoid: false,
raw: true,
});
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean));
}
const results = await mdl_UserGroups.findAll({
attributes: [[Sequelize.fn('DISTINCT', Sequelize.col(field)), 'value']],
where: { [field]: { [Op.ne]: null } },
paranoid: false,
raw: true,
});
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean).sort());
}
return R.error(res, 'JSONB fields are not supported for groups.', 400);
} catch (err) {
console.error('[ADMIN][GET GROUP FIELD VALUES]', err);
return R.error(res, 'Could not retrieve field values.', 500);
}
};
exports.getGroupFieldValues = getFieldValues(mdl_UserGroups, "GROUP", {
blockedFields: ["deletedAt"],
paranoid: false,
});
// ─── MEMBERSHIP ───────────────────────────────────────────────────────────────
+7 -64
View File
@@ -19,6 +19,7 @@ const sendEmail = require('../../services/email.service');
const R = require('../../utils/response.util');
const { paginate } = require('../../utils/paginate.util');
const { enrichPersonalInfo } = require('../../utils/personalInfo.util');
const { getFieldValues } = require("../../utils/fieldValues.util");
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
@@ -302,70 +303,12 @@ exports.terminateSession = async (req, res) => {
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
exports.getUserFieldValues = async (req, res) => {
try {
const { field } = req.query;
if (!field) return R.error(res, 'Field is required.', 400);
const allowedFields = Object.keys(mdl_Users.rawAttributes).filter(
(f) => !['password', 'otp_code', 'otp_expires_at', 'personal_info'].includes(f)
);
const dateFields = ['createdAt', 'updatedAt', 'deletedAt', 'modifiedAt'];
if (!field.includes('.')) {
if (!allowedFields.includes(field))
return R.error(res, 'Invalid or restricted field.', 400);
if (auditByFields.includes(field)) {
const [rows] = await sequelize.query(`
SELECT DISTINCT u2."personal_info"->'name'->>'full_name' AS value
FROM users u1
JOIN users u2 ON u2.user_id = u1."${field}"
WHERE u1."${field}" IS NOT NULL
AND u2."personal_info"->'name'->>'full_name' IS NOT NULL
ORDER BY value ASC
`);
return R.success(res, 'Field values retrieved.', rows.map((r) => r.value).filter(Boolean));
}
if (dateFields.includes(field)) {
const results = await mdl_Users.findAll({
attributes: [[Sequelize.fn('DISTINCT', Sequelize.fn('DATE', Sequelize.col(field))), 'value']],
where: { [field]: { [Op.ne]: null } },
order: [[Sequelize.fn('DATE', Sequelize.col(field)), 'DESC']],
raw: true,
});
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean));
}
const results = await mdl_Users.findAll({
attributes: [[Sequelize.fn('DISTINCT', Sequelize.col(field)), 'value']],
where: { [field]: { [Op.ne]: null } },
raw: true,
});
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean).sort());
}
// ─── JSONB dot-notation ───────────────────────────────────────────────
const [column, ...pathParts] = field.split('.');
const keys = [...pathParts];
const lastKey = keys.pop();
const jsonbPath = keys.length
? `"${column}"->${keys.map((k) => `'${k}'`).join('->')}->>'${lastKey}'`
: `"${column}"->>'${lastKey}'`;
const results = await mdl_Users.findAll({
attributes: [[Sequelize.literal(`DISTINCT ${jsonbPath}`), 'value']],
where: Sequelize.literal(`${jsonbPath} IS NOT NULL`),
raw: true,
});
return R.success(res, 'Field values retrieved.', results.map((r) => r.value).filter(Boolean).sort());
} catch (err) {
console.error('[ADMIN][GET USER FIELD VALUES]', err);
return R.error(res, 'Could not retrieve field values.', 500);
}
};
exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", {
blockedFields: ["password", "otp_code", "otp_expires_at", "personal_info"],
extraDateFields: ["modifiedAt"],
allowJsonb: true,
selfJoin: true,
});
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────