Files
starr-philproperties/apps/api/utils/fieldValues.util.js
T

129 lines
5.8 KiB
JavaScript

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 mdl_TierCategories = require("../models/tiers/tier_categories.mdl");
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"]);
}
// ─── Tier gate fields — same reasoning as ENUM above: the picklist should
// reflect every tier category that exists, not just whichever ones a
// DISTINCT scan happens to find already assigned to a row.
if (field === "subscription") {
const categories = await mdl_TierCategories.findAll({
where: { is_active: true },
order: [["rank", "ASC"]],
raw: true,
});
return R.success(res, "Field values retrieved.", categories.map((c) => ({ value: c.slug, label: c.name })));
}
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 };