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); // ─── ENUM / BOOLEAN fields — return the canonical value set, not a DISTINCT // scan of current rows. A DISTINCT query silently omits valid values // that just don't happen to exist yet (e.g. is_banned when nobody is // currently banned), which produces an incomplete filter picklist. const rawType = Model.rawAttributes[field]?.type; const typeKey = rawType?.constructor?.key || rawType?.key; if (typeKey === "ENUM") { return R.success(res, "Field values retrieved.", rawType.values ?? []); } if (typeKey === "BOOLEAN") { return R.success(res, "Field values retrieved.", ["true", "false"]); } if (auditByFields.includes(field)) { // Filtering must match the audit column's real (bigint) id — returning // just the display name here previously made buildWhere() compare a // name string against the raw id column via a text cast, which can // never match. Carry both: `value` (id) is what gets filtered on, // `label` (full_name) is what the filter sheet displays. const tableName = Model.getTableName(); const [rows] = selfJoin ? await sequelize.query(` SELECT DISTINCT u1."${field}" AS value, u2."personal_info"->'name'->>'full_name' AS label 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 label ASC `) : await sequelize.query(` SELECT DISTINCT t."${field}" AS value, u."personal_info"->'name'->>'full_name' AS label 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 label ASC `); return R.success(res, "Field values retrieved.", rows.filter((r) => r.label)); } 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 };