Files
starr-philproperties/utils/buildQuery.util.js
T
kennethobsequio ea3e82e54c added
Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
2026-07-12 12:39:17 +08:00

123 lines
4.4 KiB
JavaScript

// utils/queryBuilder.js
const { Sequelize, Op } = require("sequelize");
// ─── Custom sort order for ENUM fields ────────────────────────────────────────
const ENUM_SORT_ORDER = {
acc_type: ["admin", "staff", "user"],
reg_type: ["system", "google"],
};
// createdBy/updatedBy/deletedBy filter options come from getFieldValues()
// as { value: user_id, label: full_name } — the filter sheet selects by id,
// so these need an exact match against the bigint column, never the
// substring-on-text-cast path used for everything else (that path compares
// against the raw id and can never match a name-looking value).
const AUDIT_ID_FIELDS = new Set(["createdBy", "updatedBy", "deletedBy"]);
/**
* Builds a Sequelize `where` clause from an array of filters.
*
* @param {Array<{ id: string, value: any }>} filters
* @returns {Object} Sequelize where clause
*/
function buildWhere(filters = [], allowedFields = new Set()) {
const where = [];
for (const { id, value } of filters) {
if (!id || value === undefined || value === null || value === "") continue;
if (allowedFields.size && !allowedFields.has(id)) continue;
const values = Array.isArray(value) ? value : [value];
if (AUDIT_ID_FIELDS.has(id)) {
where.push({ [id]: { [Op.in]: values } });
continue;
}
const conditions = values.map((v) =>
id.startsWith("personal_info.")
? Sequelize.where(
Sequelize.json(`personal_info.${id.replace("personal_info.", "")}`),
{ [Op.iLike]: `%${v}%` }
)
: Sequelize.where(
Sequelize.cast(Sequelize.col(id), "TEXT"),
{ [Op.iLike]: `%${v}%` }
)
);
where.push({ [Op.or]: conditions });
}
return where.length ? { [Op.and]: where } : {};
}
/**
* Builds a Sequelize `order` clause from an array of sort descriptors.
* Falls back to [["createdAt", "DESC"]] if no valid sort entries.
*
* @param {Array<{ id: string, desc: boolean }>} sort
* @returns {Array} Sequelize order clause
*/
function buildOrder(sort = [], allowedFields = new Set(), computedFields = new Set()) {
const order = [];
for (const { id, desc } of sort) {
if (!id) continue;
if (allowedFields.size && !allowedFields.has(id)) continue;
// ─── Computed (subquery/literal) columns — order by the unqualified
// SELECT alias, since they aren't real columns on the model's table ──
if (computedFields.has(id)) {
order.push([Sequelize.literal(`"${id}"`), desc ? "DESC" : "ASC"]);
continue;
}
// ─── ENUM fields — use CASE for custom sort order ─────────────────────
if (ENUM_SORT_ORDER[id]) {
const sequence = desc
? [...ENUM_SORT_ORDER[id]].reverse()
: ENUM_SORT_ORDER[id];
const caseExpr = sequence
.map((val, i) => `WHEN '${val}' THEN ${i}`)
.join(" ");
order.push([Sequelize.literal(`CASE "${id}" ${caseExpr} END`)]);
continue;
}
// ─── JSONB dot-notation fields ─────────────────────────────────────────
if (id.startsWith("personal_info.")) {
order.push([Sequelize.json(id), desc ? "DESC" : "ASC"]);
continue;
}
// ─── Regular fields ───────────────────────────────────────────────────
order.push([id, desc ? "DESC" : "ASC"]);
}
return order.length ? order : [["createdAt", "DESC"]];
}
/**
* Convenience wrapper — returns both where and order in one call.
*
* @param {Array} filters
* @param {Array} sort
* @returns {{ where: Object, order: Array }}
*/
function buildQuery(filters = [], sort = [], allowedFields = [], computedFields = []) {
const fieldSet = new Set(allowedFields);
const computedSet = new Set(computedFields);
const orderFieldSet = new Set([...allowedFields, ...computedFields]);
return {
// Computed (subquery/literal) columns aren't real table columns — filtering
// via Sequelize.col() would error, so only allow them in ORDER BY, not WHERE.
where: buildWhere(filters, fieldSet),
order: buildOrder(sort, orderFieldSet, computedSet),
};
}
module.exports = { buildWhere, buildOrder, buildQuery };