mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
182 lines
5.8 KiB
JavaScript
182 lines
5.8 KiB
JavaScript
// utils/paginate.util.js
|
|
const { Sequelize } = require('sequelize');
|
|
const { modelToAttributes } = require('./modelToAttributes.util');
|
|
const { excludeJsonbPaths } = require('./excludeJSONBPaths.util');
|
|
const { buildQuery } = require('./buildQuery.util');
|
|
|
|
const PAGE_START = 1;
|
|
const PAGE_SIZE = 10;
|
|
const MAX_LIMIT = 1000;
|
|
|
|
function safeParseJSON(value, fallback = []) {
|
|
try {
|
|
const parsed = JSON.parse(value);
|
|
return Array.isArray(parsed) ? parsed : fallback;
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates audit subquery attributes for createdBy, updatedBy, deletedBy
|
|
*
|
|
* @param {Object} mdl_Users - Users model
|
|
* @param {string} parentAlias - Sequelize model alias e.g. 'User', 'UserGroup'
|
|
* @returns {Array} - Sequelize attribute include array
|
|
*/
|
|
function auditInclude(mdl_Users, parentAlias = 'User') {
|
|
const tableName = mdl_Users.getTableName();
|
|
|
|
const fullNameSubquery = (foreignKey) =>
|
|
Sequelize.literal(`(
|
|
SELECT (u."personal_info"->>'name')::jsonb->>'full_name'
|
|
FROM "${tableName}" AS u
|
|
WHERE u."user_id" = "${parentAlias}"."${foreignKey}"
|
|
LIMIT 1
|
|
)`);
|
|
|
|
return {
|
|
attributes: [
|
|
[fullNameSubquery('createdBy'), 'createdByName'],
|
|
[fullNameSubquery('updatedBy'), 'updatedByName'],
|
|
[fullNameSubquery('deletedBy'), 'deletedByName'],
|
|
],
|
|
};
|
|
}
|
|
|
|
const auditIdToName = (row) => {
|
|
const map = {
|
|
createdBy: 'createdByName',
|
|
updatedBy: 'updatedByName',
|
|
deletedBy: 'deletedByName',
|
|
};
|
|
|
|
const result = { ...row };
|
|
|
|
for (const [idField, nameField] of Object.entries(map)) {
|
|
if (result[idField] !== null && result[idField] !== undefined) {
|
|
result[idField] = result[nameField] ?? result[idField]; // ← use name if available, fallback to id
|
|
}
|
|
delete result[nameField]; // ← remove the redundant *ByName fields
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
/**
|
|
* Reusable paginated findAndCountAll
|
|
*
|
|
* @param {Object} model - Sequelize model
|
|
* @param {Object} req - Express request
|
|
* @param {Object} options
|
|
* @param {string[]} options.excludeAttributes - fields to exclude
|
|
* @param {Object} options.jsonbSchemas - JSONB schema map
|
|
* @param {string} options.jsonbColumn - JSONB column name e.g. "personal_info"
|
|
* @param {Object} options.findOptions - extra Sequelize options (include, where, etc.)
|
|
* @param {Object} options.auditOptions - { mdl_Users, parentAlias } to auto-include audit subqueries
|
|
*/
|
|
async function paginate(model, req, {
|
|
excludeAttributes = [],
|
|
jsonbSchemas = {},
|
|
jsonbColumn = null,
|
|
findOptions = {},
|
|
auditOptions = null, // ← { mdl_Users, parentAlias },
|
|
computedAttributes = [],
|
|
context = "list",
|
|
} = {}) {
|
|
const page = Math.max(PAGE_START, parseInt(req.query.page, 10) || PAGE_START);
|
|
const limit = Math.min(parseInt(req.query.limit, 10) || PAGE_SIZE, MAX_LIMIT);
|
|
const offset = (page - PAGE_START) * limit;
|
|
|
|
const filters = safeParseJSON(req.query.filters);
|
|
const sort = safeParseJSON(req.query.sort);
|
|
|
|
const topLevelExclude = excludeAttributes.filter((f) => !f.includes('.'));
|
|
const jsonbExclude = excludeAttributes.filter((f) => f.includes('.'));
|
|
const jsonbAttr = jsonbColumn ? excludeJsonbPaths(jsonbColumn, jsonbExclude) : null;
|
|
|
|
const attributes = modelToAttributes(model, { exclude: excludeAttributes, jsonbSchemas, context });
|
|
const ALLOWED_FIELDS = attributes.map((a) => a.field);
|
|
|
|
const { where, order } = buildQuery(filters, sort, ALLOWED_FIELDS);
|
|
|
|
// Build attribute includes: jsonb + audit subqueries + any extra from findOptions
|
|
const baseIncludes = jsonbAttr ? [jsonbAttr] : [];
|
|
const auditAttrs = auditOptions
|
|
? auditInclude(auditOptions.mdl_Users, auditOptions.parentAlias).attributes
|
|
: [];
|
|
const extraIncludes = findOptions.attributes?.include ?? [];
|
|
|
|
// ← Convert { key, literal } objects into Sequelize [literal, alias] tuples
|
|
const computedIncludes = computedAttributes
|
|
.filter((c) => c.literal)
|
|
.map((c) => [Sequelize.literal(c.literal), c.key]);
|
|
|
|
const mergedAttributeIncludes = [
|
|
...baseIncludes,
|
|
...auditAttrs,
|
|
...extraIncludes,
|
|
...computedIncludes, // ← now proper tuples
|
|
];
|
|
|
|
const { attributes: _attr, ...restFindOptions } = findOptions;
|
|
|
|
const { count, rows } = await model.findAndCountAll({
|
|
...restFindOptions,
|
|
where: { ...where, ...(restFindOptions.where ?? {}) },
|
|
order,
|
|
limit,
|
|
offset,
|
|
attributes: {
|
|
exclude: topLevelExclude,
|
|
include: mergedAttributeIncludes,
|
|
},
|
|
});
|
|
|
|
const data = rows
|
|
.map((row) => row.toJSON?.() ?? row)
|
|
.map(auditIdToName)
|
|
.map((row) => {
|
|
// ← Coerce computed types after toJSON
|
|
const result = { ...row };
|
|
for (const { key, type } of computedAttributes) {
|
|
if (result[key] !== undefined && result[key] !== null) {
|
|
if (type === 'number') result[key] = parseInt(result[key], 10);
|
|
if (type === 'float') result[key] = parseFloat(result[key]);
|
|
}
|
|
}
|
|
return result;
|
|
});
|
|
|
|
const totalPages = Math.ceil(count / limit);
|
|
|
|
// ← Append computed metadata
|
|
const computedMeta = computedAttributes.map(({ key, label, type, order: ord, filterable }) => ({
|
|
name: label ?? key,
|
|
type: type ?? 'text',
|
|
field: key,
|
|
order: ord ?? Infinity,
|
|
options: {},
|
|
filterable: filterable
|
|
}));
|
|
|
|
// ← Merge, sort, THEN strip order
|
|
const mergedAttributes = [...attributes, ...computedMeta]
|
|
.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
|
|
.map(({ order: _, ...rest }) => rest);
|
|
|
|
return {
|
|
data,
|
|
pagination: {
|
|
page,
|
|
limit,
|
|
totalRecords: count,
|
|
totalPages,
|
|
hasPrevPage: page > PAGE_START,
|
|
hasNextPage: page < totalPages,
|
|
},
|
|
attributes: mergedAttributes,
|
|
};
|
|
}
|
|
|
|
module.exports = { paginate, auditInclude, safeParseJSON }; |