mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
Adjusted
This commit is contained in:
@@ -10,6 +10,7 @@ const R = require('../../utils/response.util');
|
|||||||
const { paginate } = require("../../utils/paginate.util");
|
const { paginate } = require("../../utils/paginate.util");
|
||||||
const { adminExclude, jsonbSchemas, computedAttributes, } = require("../../models/assets/assets.attributes");
|
const { adminExclude, jsonbSchemas, computedAttributes, } = require("../../models/assets/assets.attributes");
|
||||||
const mdl_Users = require('../../models/users/users.mdl');
|
const mdl_Users = require('../../models/users/users.mdl');
|
||||||
|
const { getFieldValues } = require("../../utils/fieldValues.util");
|
||||||
|
|
||||||
const { Op, Sequelize } = require('sequelize');
|
const { Op, Sequelize } = require('sequelize');
|
||||||
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
|
const auditByFields = ['createdBy', 'updatedBy', 'deletedBy'];
|
||||||
@@ -581,48 +582,4 @@ exports.getArchivedAssets = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getAssetFieldValues = async (req, res) => {
|
exports.getAssetFieldValues = getFieldValues(Asset, "ASSET");
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -9,6 +9,7 @@ const { syncObjectivesCreate, syncObjectivesUpdate } = require("../../utils/cour
|
|||||||
const { syncJunction } = require("../../utils/courses/junction.util");
|
const { syncJunction } = require("../../utils/courses/junction.util");
|
||||||
const { archiveOne, archiveMany } = require("../../utils/courses/archive.util");
|
const { archiveOne, archiveMany } = require("../../utils/courses/archive.util");
|
||||||
const { restoreOne, restoreMany } = require("../../utils/courses/restore.util");
|
const { restoreOne, restoreMany } = require("../../utils/courses/restore.util");
|
||||||
|
const { getFieldValues } = require("../../utils/fieldValues.util");
|
||||||
|
|
||||||
// ── Models ────────────────────────────────────────────────────────────────────
|
// ── 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();
|
const t = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const { courseId, unitId } = req.params;
|
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);
|
if (!record) return R.error(res, "Unit not found.", 404);
|
||||||
await t.commit();
|
await t.commit();
|
||||||
return R.success(res, "Unit archived.");
|
return R.success(res, "Unit archived.");
|
||||||
@@ -427,13 +428,13 @@ exports.bulkArchiveUnits = async (req, res) => {
|
|||||||
const t = await sequelize.transaction();
|
const t = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const { courseId } = req.params;
|
const { courseId } = req.params;
|
||||||
const { ids = [], deletedBy } = req.body;
|
const { ids = [] } = req.body;
|
||||||
if (!ids.length) return R.error(res, "No IDs provided.", 400);
|
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 units = await Unit.findAll({ where: { unit_id: ids, course_id: courseId, ...notDeleted } });
|
||||||
const validIds = units.map((u) => u.unit_id);
|
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();
|
await t.commit();
|
||||||
return R.success(res, `${count} unit${count !== 1 ? "s" : ""} archived.`);
|
return R.success(res, `${count} unit${count !== 1 ? "s" : ""} archived.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -453,7 +454,7 @@ exports.getArchivedUnits = async (req, res) => {
|
|||||||
const result = await paginate(Unit, req, {
|
const result = await paginate(Unit, req, {
|
||||||
excludeAttributes: adminExclude,
|
excludeAttributes: adminExclude,
|
||||||
auditOptions: { mdl_Users, parentAlias: "Unit" },
|
auditOptions: { mdl_Users, parentAlias: "Unit" },
|
||||||
context: "list",
|
context: "archived",
|
||||||
findOptions: {
|
findOptions: {
|
||||||
where: { course_id: courseId, ...onlyDeleted },
|
where: { course_id: courseId, ...onlyDeleted },
|
||||||
paranoid: false,
|
paranoid: false,
|
||||||
@@ -487,7 +488,7 @@ exports.restoreUnit = async (req, res) => {
|
|||||||
const t = await sequelize.transaction();
|
const t = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const { courseId, unitId } = req.params;
|
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);
|
if (!record) return R.error(res, "Archived unit not found.", 404);
|
||||||
await t.commit();
|
await t.commit();
|
||||||
return R.success(res, "Unit restored.", { data: record });
|
return R.success(res, "Unit restored.", { data: record });
|
||||||
@@ -502,13 +503,13 @@ exports.bulkRestoreUnits = async (req, res) => {
|
|||||||
const t = await sequelize.transaction();
|
const t = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const { courseId } = req.params;
|
const { courseId } = req.params;
|
||||||
const { ids = [], restoredBy } = req.body;
|
const { ids = [] } = req.body;
|
||||||
if (!ids.length) return R.error(res, "No IDs provided.", 400);
|
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 units = await Unit.findAll({ where: { unit_id: ids, course_id: courseId, ...onlyDeleted }, paranoid: false });
|
||||||
const validIds = units.map((u) => u.unit_id);
|
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();
|
await t.commit();
|
||||||
return R.success(res, `${count} unit${count !== 1 ? "s" : ""} restored.`);
|
return R.success(res, `${count} unit${count !== 1 ? "s" : ""} restored.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1315,50 +1316,6 @@ exports.restoreAssessment = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.getCourseFieldValues = async (req, res) => {
|
exports.getCourseFieldValues = getFieldValues(Course, "COURSE");
|
||||||
try {
|
exports.getUnitFieldValues = getFieldValues(Unit, "UNIT");
|
||||||
const { field } = req.query;
|
exports.getLessonFieldValues = getFieldValues(Lesson, "LESSON");
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -14,6 +14,7 @@ const { mdl_UserGroups, mdl_UserGroupMembers } = require('../../models/users/use
|
|||||||
|
|
||||||
const R = require('../../utils/response.util');
|
const R = require('../../utils/response.util');
|
||||||
const { paginate } = require('../../utils/paginate.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: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
|
||||||
const { excludeAttributes: groupExclude, jsonbSchemas: groupSchemas, computedAttributes: groupComputed } = require('../../models/users/user_groups.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 ─────────────────────────────────────────────────────────────
|
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.getGroupFieldValues = async (req, res) => {
|
exports.getGroupFieldValues = getFieldValues(mdl_UserGroups, "GROUP", {
|
||||||
try {
|
blockedFields: ["deletedAt"],
|
||||||
const { field } = req.query;
|
paranoid: false,
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── MEMBERSHIP ───────────────────────────────────────────────────────────────
|
// ─── MEMBERSHIP ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const sendEmail = require('../../services/email.service');
|
|||||||
const R = require('../../utils/response.util');
|
const R = require('../../utils/response.util');
|
||||||
const { paginate } = require('../../utils/paginate.util');
|
const { paginate } = require('../../utils/paginate.util');
|
||||||
const { enrichPersonalInfo } = require('../../utils/personalInfo.util');
|
const { enrichPersonalInfo } = require('../../utils/personalInfo.util');
|
||||||
|
const { getFieldValues } = require("../../utils/fieldValues.util");
|
||||||
|
|
||||||
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
|
const { excludeAttributes: usersExclude, jsonbSchemas: usersSchemas } = require('../../models/users/users.attributes');
|
||||||
|
|
||||||
@@ -302,70 +303,12 @@ exports.terminateSession = async (req, res) => {
|
|||||||
|
|
||||||
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
|
// ─── FIELD VALUES ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
exports.getUserFieldValues = async (req, res) => {
|
exports.getUserFieldValues = getFieldValues(mdl_Users, "USER", {
|
||||||
try {
|
blockedFields: ["password", "otp_code", "otp_expires_at", "personal_info"],
|
||||||
const { field } = req.query;
|
extraDateFields: ["modifiedAt"],
|
||||||
if (!field) return R.error(res, 'Field is required.', 400);
|
allowJsonb: true,
|
||||||
|
selfJoin: true,
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
|
// ─── ARCHIVED ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
+10
-13
@@ -2,19 +2,16 @@ const { DataTypes } = require("sequelize");
|
|||||||
const sequelize = require("../../config/db.config");
|
const sequelize = require("../../config/db.config");
|
||||||
|
|
||||||
const Unit = sequelize.define("Unit", {
|
const Unit = sequelize.define("Unit", {
|
||||||
unit_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, hidden: true, order: 0 },
|
unit_id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true, hidden: true, order: 0, filterable: false },
|
||||||
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, hidden: true, order: 0 },
|
uuid: { type: DataTypes.UUID, defaultValue: DataTypes.UUIDV4, allowNull: false, unique: true, hidden: true, order: 0, filterable: false },
|
||||||
course_id: { type: DataTypes.BIGINT, allowNull: false, hidden: true, order: 0 },
|
course_id: { type: DataTypes.BIGINT, allowNull: false, hidden: true, order: 0, filterable: false },
|
||||||
|
title: { type: DataTypes.STRING(255), allowNull: false, label: "Title", hidden: false, order: 1, filterable: true },
|
||||||
title: { type: DataTypes.STRING(255), allowNull: false, label: "Title", hidden: false, order: 1 },
|
description: { type: DataTypes.TEXT, allowNull: true, label: "Description", hidden: true, order: 0, filterable: false },
|
||||||
description: { type: DataTypes.TEXT, allowNull: true, label: "Description", hidden: false, order: 2 },
|
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Order", hidden: false, order: 2, filterable: false },
|
||||||
order_index: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, label: "Order", hidden: false, order: 3 },
|
duration_seconds: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 0, hidden: false, order: 3, filterable: false },
|
||||||
|
createdBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
||||||
duration_seconds: { type: DataTypes.INTEGER, allowNull: true, defaultValue: 0 },
|
updatedBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
||||||
|
deletedBy: { type: DataTypes.BIGINT, allowNull: true, filterable: true },
|
||||||
createdBy: { type: DataTypes.BIGINT, allowNull: true, label: "Created By" },
|
|
||||||
updatedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Updated By" },
|
|
||||||
deletedBy: { type: DataTypes.BIGINT, allowNull: true, label: "Deleted By" },
|
|
||||||
}, {
|
}, {
|
||||||
tableName: "units",
|
tableName: "units",
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ router.patch("/:courseId/assessment/:assessmentId/questions/:questionId/restore"
|
|||||||
// UNITS
|
// UNITS
|
||||||
// ══════════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
router.get("/:courseId/field-values", ctrl.getUnitFieldValues);
|
||||||
router.get("/:courseId/units", ctrl.getUnits);
|
router.get("/:courseId/units", ctrl.getUnits);
|
||||||
router.post("/:courseId/units", ctrl.createUnit);
|
router.post("/:courseId/units", ctrl.createUnit);
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ router.get("/:courseId/units/archives/:unitId", ctrl.getArchivedUnit);
|
|||||||
router.patch("/:courseId/units/:unitId/restore", ctrl.restoreUnit);
|
router.patch("/:courseId/units/:unitId/restore", ctrl.restoreUnit);
|
||||||
router.get("/:courseId/units/:unitId", ctrl.getUnit);
|
router.get("/:courseId/units/:unitId", ctrl.getUnit);
|
||||||
router.put("/:courseId/units/:unitId", ctrl.updateUnit);
|
router.put("/:courseId/units/:unitId", ctrl.updateUnit);
|
||||||
router.delete("/:courseId/units/:unitId", ctrl.deleteUnit);
|
router.delete("/:courseId/units/:unitId", ctrl.archiveUnit);
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
// UNIT QUIZ
|
// UNIT QUIZ
|
||||||
@@ -111,6 +112,7 @@ router.patch("/:courseId/units/:unitId/quiz/:quizId/questions/:questionId/restor
|
|||||||
// LESSONS
|
// LESSONS
|
||||||
// ══════════════════════════════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
router.get("/:courseId/units/:unitId/field-values", ctrl.getLessonFieldValues);
|
||||||
router.get("/:courseId/units/:unitId/lessons", ctrl.getLessons);
|
router.get("/:courseId/units/:unitId/lessons", ctrl.getLessons);
|
||||||
router.post("/:courseId/units/:unitId/lessons", ctrl.createLesson);
|
router.post("/:courseId/units/:unitId/lessons", ctrl.createLesson);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
const { Op } = require("sequelize");
|
||||||
|
const sequelize = require("../config/db.config");
|
||||||
|
const Sequelize = require("sequelize");
|
||||||
|
const R = require("../utils/response.util"); // adjust path as needed
|
||||||
|
|
||||||
|
const auditByFields = ["createdBy", "updatedBy", "deletedBy"];
|
||||||
|
|
||||||
|
const getFieldValues = (Model, logTag, options = {}) => async (req, res) => {
|
||||||
|
const {
|
||||||
|
blockedFields = [],
|
||||||
|
extraDateFields = [],
|
||||||
|
allowJsonb = false,
|
||||||
|
paranoid = true,
|
||||||
|
selfJoin = false, // for users auditing users
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { field } = req.query;
|
||||||
|
if (!field) return R.error(res, "Field is required.", 400);
|
||||||
|
|
||||||
|
const dateFields = ["createdAt", "updatedAt", "deletedAt", ...extraDateFields];
|
||||||
|
|
||||||
|
// ─── Non-JSONB path ───────────────────────────────────────────────────
|
||||||
|
if (!field.includes(".")) {
|
||||||
|
const allowedFields = blockedFields.length
|
||||||
|
? Object.keys(Model.rawAttributes).filter((f) => !blockedFields.includes(f))
|
||||||
|
: Object.keys(Model.rawAttributes).filter((f) => Model.rawAttributes[f].filterable === true);
|
||||||
|
|
||||||
|
if (!allowedFields.includes(field) && !dateFields.includes(field))
|
||||||
|
return R.error(res, "Invalid or restricted field.", 400);
|
||||||
|
|
||||||
|
if (auditByFields.includes(field)) {
|
||||||
|
const tableName = Model.getTableName();
|
||||||
|
const [rows] = selfJoin
|
||||||
|
? await sequelize.query(`
|
||||||
|
SELECT DISTINCT u2."personal_info"->'name'->>'full_name' AS value
|
||||||
|
FROM "${tableName}" u1
|
||||||
|
JOIN "${tableName}" 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
|
||||||
|
`)
|
||||||
|
: await sequelize.query(`
|
||||||
|
SELECT DISTINCT u."personal_info"->'name'->>'full_name' AS value
|
||||||
|
FROM "${tableName}" t
|
||||||
|
JOIN users u ON u.user_id = t."${field}"
|
||||||
|
WHERE t."${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 Model.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,
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await Model.findAll({
|
||||||
|
attributes: [[Sequelize.fn("DISTINCT", Sequelize.col(field)), "value"]],
|
||||||
|
where: { [field]: { [Op.ne]: null } },
|
||||||
|
paranoid,
|
||||||
|
raw: true,
|
||||||
|
});
|
||||||
|
return R.success(res, "Field values retrieved.", results.map((r) => r.value).filter(Boolean).sort());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── JSONB dot-notation path ──────────────────────────────────────────
|
||||||
|
if (!allowJsonb)
|
||||||
|
return R.error(res, "JSONB fields are not supported.", 400);
|
||||||
|
|
||||||
|
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 Model.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(`[${logTag}][GET FIELD VALUES]`, err);
|
||||||
|
return R.error(res, "Could not retrieve field values.", 500);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { getFieldValues };
|
||||||
Reference in New Issue
Block a user