mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -18,9 +18,15 @@ 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
|
||||
* @param {Set<string>} allowedFields
|
||||
* @param {string} [parentAlias] - main model's table alias (e.g. "User"). Needed
|
||||
* to qualify Sequelize.col() references — unqualified col() refs throw
|
||||
* "column reference is ambiguous" as soon as a query joins another table
|
||||
* that happens to share a column name (e.g. Users + UserGroups both have
|
||||
* is_active/createdAt/updatedAt/deletedAt).
|
||||
* @returns {Object} Sequelize where clause
|
||||
*/
|
||||
function buildWhere(filters = [], allowedFields = new Set()) {
|
||||
function buildWhere(filters = [], allowedFields = new Set(), parentAlias = null) {
|
||||
const where = [];
|
||||
|
||||
for (const { id, value } of filters) {
|
||||
@@ -34,6 +40,8 @@ function buildWhere(filters = [], allowedFields = new Set()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const qualifiedCol = parentAlias ? `${parentAlias}.${id}` : id;
|
||||
|
||||
const conditions = values.map((v) =>
|
||||
id.startsWith("personal_info.")
|
||||
? Sequelize.where(
|
||||
@@ -41,7 +49,7 @@ function buildWhere(filters = [], allowedFields = new Set()) {
|
||||
{ [Op.iLike]: `%${v}%` }
|
||||
)
|
||||
: Sequelize.where(
|
||||
Sequelize.cast(Sequelize.col(id), "TEXT"),
|
||||
Sequelize.cast(Sequelize.col(qualifiedCol), "TEXT"),
|
||||
{ [Op.iLike]: `%${v}%` }
|
||||
)
|
||||
);
|
||||
@@ -59,8 +67,9 @@ function buildWhere(filters = [], allowedFields = new Set()) {
|
||||
* @param {Array<{ id: string, desc: boolean }>} sort
|
||||
* @returns {Array} Sequelize order clause
|
||||
*/
|
||||
function buildOrder(sort = [], allowedFields = new Set(), computedFields = new Set()) {
|
||||
function buildOrder(sort = [], allowedFields = new Set(), computedFields = new Set(), parentAlias = null) {
|
||||
const order = [];
|
||||
const qualifiedId = (id) => (parentAlias ? `"${parentAlias}"."${id}"` : `"${id}"`);
|
||||
|
||||
for (const { id, desc } of sort) {
|
||||
if (!id) continue;
|
||||
@@ -83,7 +92,7 @@ function buildOrder(sort = [], allowedFields = new Set(), computedFields = new S
|
||||
.map((val, i) => `WHEN '${val}' THEN ${i}`)
|
||||
.join(" ");
|
||||
|
||||
order.push([Sequelize.literal(`CASE "${id}" ${caseExpr} END`)]);
|
||||
order.push([Sequelize.literal(`CASE ${qualifiedId(id)} ${caseExpr} END`)]);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -107,7 +116,7 @@ function buildOrder(sort = [], allowedFields = new Set(), computedFields = new S
|
||||
* @param {Array} sort
|
||||
* @returns {{ where: Object, order: Array }}
|
||||
*/
|
||||
function buildQuery(filters = [], sort = [], allowedFields = [], computedFields = []) {
|
||||
function buildQuery(filters = [], sort = [], allowedFields = [], computedFields = [], parentAlias = null) {
|
||||
const fieldSet = new Set(allowedFields);
|
||||
const computedSet = new Set(computedFields);
|
||||
const orderFieldSet = new Set([...allowedFields, ...computedFields]);
|
||||
@@ -115,8 +124,8 @@ function buildQuery(filters = [], sort = [], 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),
|
||||
where: buildWhere(filters, fieldSet, parentAlias),
|
||||
order: buildOrder(sort, orderFieldSet, computedSet, parentAlias),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+13
-3
@@ -1,5 +1,5 @@
|
||||
// utils/paginate.util.js
|
||||
const { Sequelize } = require('sequelize');
|
||||
const { Sequelize, Op } = require('sequelize');
|
||||
const { modelToAttributes } = require('./modelToAttributes.util');
|
||||
const { excludeJsonbPaths } = require('./excludeJSONBPaths.util');
|
||||
const { buildQuery } = require('./buildQuery.util');
|
||||
@@ -108,7 +108,10 @@ async function paginate(model, req, {
|
||||
const ALLOWED_FIELDS = attributes.map((a) => a.field);
|
||||
const computedFieldKeys = computedAttributes.map((c) => c.key);
|
||||
|
||||
const { where, order } = buildQuery(filters, sort, ALLOWED_FIELDS, computedFieldKeys);
|
||||
// Sequelize aliases the main model's table with the model's name by default
|
||||
// (e.g. `FROM "users" AS "User"`) — needed to qualify Sequelize.col()
|
||||
// references so they don't collide with same-named columns on joined tables.
|
||||
const { where, order } = buildQuery(filters, sort, ALLOWED_FIELDS, computedFieldKeys, model.name);
|
||||
|
||||
// Build attribute includes: jsonb + audit subqueries + any extra from findOptions
|
||||
const baseIncludes = jsonbAttr ? [jsonbAttr] : [];
|
||||
@@ -131,9 +134,16 @@ async function paginate(model, req, {
|
||||
|
||||
const { attributes: _attr, ...restFindOptions } = findOptions;
|
||||
|
||||
// Nest rather than shallow-spread — `where` and `restFindOptions.where` can
|
||||
// both carry an [Op.and] key (the same global Symbol), and spreading two
|
||||
// objects that share a Symbol key silently drops the first one's value.
|
||||
const combinedWhere = restFindOptions.where
|
||||
? { [Op.and]: [where, restFindOptions.where] }
|
||||
: where;
|
||||
|
||||
const { count, rows } = await model.findAndCountAll({
|
||||
...restFindOptions,
|
||||
where: { ...where, ...(restFindOptions.where ?? {}) },
|
||||
where: combinedWhere,
|
||||
order,
|
||||
limit,
|
||||
offset,
|
||||
|
||||
Reference in New Issue
Block a user