UNPKG

@underpostnet/underpost

Version:

Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc

462 lines (412 loc) 14 kB
/** * Module for parsing data query parameters into Mongoose query options. * Supports AG Grid filterModel/sortModel as well as simple legacy parameters. * @module src/server/data-query.js * @namespace DataQuery */ /** * @class DataQuery * @description Utility class for parsing request query parameters into Mongoose query options, * including support for AG Grid filterModel and sortModel. Provides a static `parse` * method that takes in request query parameters and returns an object containing the MongoDB * query, sort options, pagination skip/limit, and page number. Designed to be used in API * controllers to handle complex querying needs from the frontend. * @memberof DataQuery */ class DataQuery { /** * Parse request query parameters into Mongoose query options * @param {Object} params - The request query parameters (req.query) * @param {string|Object} [params.filterModel] - AG Grid filterModel as JSON string or object * @param {string|Object} [params.sortModel] - AG Grid sortModel as JSON string or object * @param {number|string} [params.page=1] - Page number for pagination * @param {number|string} [params.limit=10] - Items per page for pagination * @param {string} [params.sort] - Simple sort field (legacy) * @param {string|boolean} [params.asc] - Simple sort direction (legacy, '1'/'true' for asc) * @param {string} [params.order] - Simple order string, e.g. "field1:asc,field2:desc" (legacy) * @param {Object} [params.query] - Default query object to merge with filters * @memberof DataQuery * @returns {Object} { query, sort, skip, limit, page } */ static parse(params = {}) { let { filterModel, sortModel, page, limit, sort: sortParam, asc, order, query: defaultQuery } = params; // === 1. Pagination === page = parseInt(page, 10) || 1; limit = parseInt(limit, 10) || 10; const skip = (page - 1) * limit; // === 2. Sorting === const sort = DataQuery._parseSort(sortModel, sortParam, asc, order); // === 3. Filtering === const query = DataQuery._parseFilter(filterModel, defaultQuery); return { query, sort, skip, limit, page }; } /** * Parse sort parameters from AG Grid sortModel or simple sort params * @method * @param {string|Object} sortModel - AG Grid sortModel as JSON string or object * @param {string} sortParam - Simple sort field (legacy) * @param {string|boolean} asc - Simple sort direction (legacy) * @param {string} order - Simple order string (legacy) * @return {Object} sort object for Mongoose * @memberof DataQuery */ static _parseSort(sortModel, sortParam, asc, order) { const sort = {}; // Parse sortModel from string if needed if (typeof sortModel === 'string' && sortModel.trim()) { try { sortModel = JSON.parse(sortModel); } catch (e) { console.warn('DataQuery: Failed to parse sortModel JSON:', e.message); sortModel = null; } } // AG Grid sortModel format: [{ colId: 'field', sort: 'asc' | 'desc' }] if (Array.isArray(sortModel) && sortModel.length > 0) { sortModel.forEach((sortItem) => { if (sortItem && sortItem.colId && sortItem.sort) { sort[sortItem.colId] = sortItem.sort === 'asc' ? 1 : -1; } }); return sort; } // Simple sort params (legacy support) if (sortParam && typeof sortParam === 'string') { const direction = asc === '1' || asc === 'true' || asc === true ? 1 : -1; sort[sortParam] = direction; return sort; } // Order param format: "field1:asc,field2:desc" if (order && typeof order === 'string') { const orderParts = order.split(','); orderParts.forEach((part) => { const [field, dir] = part.split(':'); if (field && field.trim()) { sort[field.trim()] = dir === 'desc' ? -1 : 1; } }); return sort; } return sort; } /** * Parse filter parameters from AG Grid filterModel * @method * @param {string|Object} filterModel - AG Grid filterModel as JSON string or object * @param {Object} defaultQuery - Default query object to merge with filters * @return {Object} query object for Mongoose * @memberof DataQuery */ static _parseFilter(filterModel, defaultQuery) { let query = defaultQuery ? { ...defaultQuery } : {}; // Parse filterModel from string if needed if (typeof filterModel === 'string' && filterModel.trim()) { try { filterModel = JSON.parse(filterModel); } catch (e) { console.warn('DataQuery: Failed to parse filterModel JSON:', e.message); filterModel = null; } } if (!filterModel || typeof filterModel !== 'object' || Array.isArray(filterModel)) { return query; } // Process each filter in the filterModel Object.entries(filterModel).forEach(([field, filter]) => { if (!field || !filter) return; const fieldQuery = DataQuery._parseFieldFilter(field, filter); if (fieldQuery) { query = { ...query, ...fieldQuery }; } }); return query; } /** * Parse a single field filter * @method * @param {string} field - The field name * @param {Object} filter - The filter object * @return {Object|null} query condition for the field or null if invalid * @memberof DataQuery */ static _parseFieldFilter(field, filter) { if (!filter || !filter.filterType) { return null; } const { filterType } = filter; switch (filterType) { case 'text': return DataQuery._parseTextFilter(field, filter); case 'number': return DataQuery._parseNumberFilter(field, filter); case 'date': return DataQuery._parseDateFilter(field, filter); case 'set': return DataQuery._parseSetFilter(field, filter); case 'multi': return DataQuery._parseMultiFilter(field, filter); default: return null; } } /** * Parse text filter * @method * @param {string} field - The field name * @param {Object} filter - The filter object * @return {Object|null} query condition for the text field or null if invalid * @memberof DataQuery */ static _parseTextFilter(field, filter) { const { type, filter: filterValue } = filter; if (filterValue === null || filterValue === undefined || filterValue === '') { // Handle blank/notBlank without a filter value if (type === 'blank' || type === 'notBlank') { const query = {}; if (type === 'blank') { query[field] = { $in: [null, ''] }; } else { query[field] = { $nin: [null, ''], $exists: true }; } return query; } return null; } const query = {}; // Escape special regex characters for safety const escapeRegex = (str) => String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); switch (type) { case 'equals': query[field] = filterValue; break; case 'notEqual': query[field] = { $ne: filterValue }; break; case 'contains': query[field] = { $regex: escapeRegex(filterValue), $options: 'i' }; break; case 'notContains': query[field] = { $not: { $regex: escapeRegex(filterValue), $options: 'i' } }; break; case 'startsWith': query[field] = { $regex: `^${escapeRegex(filterValue)}`, $options: 'i' }; break; case 'endsWith': query[field] = { $regex: `${escapeRegex(filterValue)}$`, $options: 'i' }; break; case 'blank': query[field] = { $in: [null, ''] }; break; case 'notBlank': query[field] = { $nin: [null, ''], $exists: true }; break; default: query[field] = { $regex: escapeRegex(filterValue), $options: 'i' }; } return query; } /** * Parse number filter * @method * @param {string} field - The field name * @param {Object} filter - The filter object * @return {Object|null} query condition for the number field or null if invalid * @memberof DataQuery */ static _parseNumberFilter(field, filter) { const { type, filter: filterValue, filterTo } = filter; if (filterValue === null || filterValue === undefined) { return null; } const query = {}; const numValue = parseFloat(filterValue); if (isNaN(numValue)) { return null; } switch (type) { case 'equals': query[field] = numValue; break; case 'notEqual': query[field] = { $ne: numValue }; break; case 'lessThan': query[field] = { $lt: numValue }; break; case 'lessThanOrEqual': query[field] = { $lte: numValue }; break; case 'greaterThan': query[field] = { $gt: numValue }; break; case 'greaterThanOrEqual': query[field] = { $gte: numValue }; break; case 'inRange': if (filterTo !== null && filterTo !== undefined) { const numTo = parseFloat(filterTo); if (!isNaN(numTo)) { query[field] = { $gte: numValue, $lte: numTo }; } } break; case 'blank': query[field] = { $in: [null, undefined] }; break; case 'notBlank': query[field] = { $nin: [null, undefined], $exists: true }; break; default: query[field] = numValue; } return query; } /** * Parse date filter * @method * @param {string} field - The field name * @param {Object} filter - The filter object * @return {Object|null} query condition for the date field or null if invalid * @memberof DataQuery */ static _parseDateFilter(field, filter) { const { type, dateFrom, dateTo } = filter; // Handle blank/notBlank without dates if (type === 'blank' || type === 'notBlank') { const query = {}; if (type === 'blank') { query[field] = { $in: [null, undefined] }; } else { query[field] = { $nin: [null, undefined], $exists: true }; } return query; } if (!dateFrom && !dateTo) { return null; } const query = {}; const parseDate = (dateStr) => { if (!dateStr) return null; const date = new Date(dateStr); return isNaN(date.getTime()) ? null : date; }; const fromDate = parseDate(dateFrom); const toDate = parseDate(dateTo); if (!fromDate && !toDate) { return null; } switch (type) { case 'equals': if (fromDate) { // Match the entire day const startOfDay = new Date(fromDate); startOfDay.setHours(0, 0, 0, 0); const endOfDay = new Date(fromDate); endOfDay.setHours(23, 59, 59, 999); query[field] = { $gte: startOfDay, $lte: endOfDay }; } break; case 'notEqual': if (fromDate) { const startOfDay = new Date(fromDate); startOfDay.setHours(0, 0, 0, 0); const endOfDay = new Date(fromDate); endOfDay.setHours(23, 59, 59, 999); query[field] = { $not: { $gte: startOfDay, $lte: endOfDay } }; } break; case 'lessThan': if (fromDate) { query[field] = { $lt: fromDate }; } break; case 'lessThanOrEqual': if (fromDate) { query[field] = { $lte: fromDate }; } break; case 'greaterThan': if (fromDate) { query[field] = { $gt: fromDate }; } break; case 'greaterThanOrEqual': if (fromDate) { query[field] = { $gte: fromDate }; } break; case 'inRange': if (fromDate && toDate) { // For inRange, set toDate to end of day const endOfToDate = new Date(toDate); endOfToDate.setHours(23, 59, 59, 999); query[field] = { $gte: fromDate, $lte: endOfToDate }; } else if (fromDate) { query[field] = { $gte: fromDate }; } else if (toDate) { const endOfToDate = new Date(toDate); endOfToDate.setHours(23, 59, 59, 999); query[field] = { $lte: endOfToDate }; } break; case 'blank': query[field] = { $in: [null, undefined] }; break; case 'notBlank': query[field] = { $nin: [null, undefined], $exists: true }; break; default: if (fromDate) { query[field] = fromDate; } } return query; } /** * Parse set filter * @method * @param {string} field - The field name * @param {Object} filter - The filter object * @return {Object|null} query condition for the set field or null if invalid * @memberof DataQuery */ static _parseSetFilter(field, filter) { const { values } = filter; if (!Array.isArray(values) || values.length === 0) { return null; } return { [field]: { $in: values } }; } /** * Parse multi filter (combines multiple filters with AND/OR) * @method * @param {string} field - The field name * @param {Object} filter - The multi filter object * @return {Object|null} query condition for the multi filter or null if invalid * @memberof DataQuery */ static _parseMultiFilter(field, filter) { const { filterModels, operator } = filter; if (!Array.isArray(filterModels) || filterModels.length === 0) { return null; } const conditions = filterModels .map((subFilter) => DataQuery._parseFieldFilter(field, subFilter)) .filter((condition) => condition !== null); if (conditions.length === 0) { return null; } if (conditions.length === 1) { return conditions[0]; } // Combine conditions with AND or OR if (operator === 'OR') { return { $or: conditions }; } else { // AND operator (default) return { $and: conditions }; } } } export { DataQuery }; export default DataQuery;