Add boolean field handling to filtering logic in utils.js
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good
This update introduces functions to manage boolean fields in the filtering process, including parsing boolean operands and building equality/comparison conditions. The `buildCondition`, `parseLeafCondition`, and `parseFilter` functions have been modified to accommodate boolean field checks, enhancing the overall filtering capabilities across the application.
This commit is contained in:
parent
c043ad7a86
commit
6c694d7d48
92
src/utils.js
92
src/utils.js
@ -10,6 +10,7 @@ import config from './config.js';
|
|||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import canonicalize from 'canonical-json';
|
import canonicalize from 'canonical-json';
|
||||||
import { getModelByName } from './services/misc/model.js';
|
import { getModelByName } from './services/misc/model.js';
|
||||||
|
import { models } from './database/schemas/models.js';
|
||||||
import { createEmailRenderAuthCode } from './services/misc/emailRenderAuth.js';
|
import { createEmailRenderAuthCode } from './services/misc/emailRenderAuth.js';
|
||||||
import { Worker } from 'worker_threads';
|
import { Worker } from 'worker_threads';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@ -63,6 +64,50 @@ function looksLikeDateField(property) {
|
|||||||
return /[a-z](?:At|Date|Time)$/.test(last) || /^(?:date|time|datetime)$/i.test(last);
|
return /[a-z](?:At|Date|Time)$/.test(last) || /^(?:date|time|datetime)$/i.test(last);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NO_MATCH_CONDITION = { op: { $in: [] } };
|
||||||
|
|
||||||
|
function isBooleanField(property, model = null) {
|
||||||
|
const schema = model?.schema;
|
||||||
|
if (schema) {
|
||||||
|
const path = schema.path(property);
|
||||||
|
return path?.instance === 'Boolean';
|
||||||
|
}
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
for (const entry of Object.values(models)) {
|
||||||
|
const path = entry.model?.schema?.path(property);
|
||||||
|
if (!path) continue;
|
||||||
|
found = true;
|
||||||
|
if (path.instance !== 'Boolean') return false;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBooleanOperand(value) {
|
||||||
|
const lower = String(value).trim().toLowerCase();
|
||||||
|
if (lower === 'yes') return true;
|
||||||
|
if (lower === 'no') return false;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBooleanEquality(value) {
|
||||||
|
const bool = parseBooleanOperand(value);
|
||||||
|
if (bool === undefined) return NO_MATCH_CONDITION;
|
||||||
|
return { value: bool };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBooleanComparison(name, value) {
|
||||||
|
if (name === 'eq') return buildBooleanEquality(value);
|
||||||
|
|
||||||
|
if (name === 'ne') {
|
||||||
|
const bool = parseBooleanOperand(value);
|
||||||
|
if (bool === undefined) return NO_MATCH_CONDITION;
|
||||||
|
return { op: { $ne: bool } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return NO_MATCH_CONDITION;
|
||||||
|
}
|
||||||
|
|
||||||
function isObjectIdString(value) {
|
function isObjectIdString(value) {
|
||||||
return /^[a-f\d]{24}$/i.test(value);
|
return /^[a-f\d]{24}$/i.test(value);
|
||||||
}
|
}
|
||||||
@ -197,7 +242,8 @@ function stripIgnoreCase(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Builds an equality condition (value, date range, or wildcard regex).
|
// Builds an equality condition (value, date range, or wildcard regex).
|
||||||
function buildEquality(value, isDateField) {
|
function buildEquality(value, isDateField, isBooleanField = false) {
|
||||||
|
if (isBooleanField) return buildBooleanEquality(value);
|
||||||
if (isDateField) {
|
if (isDateField) {
|
||||||
const start = parseDateOperand(value, 'start');
|
const start = parseDateOperand(value, 'start');
|
||||||
const end = parseDateOperand(value, 'end');
|
const end = parseDateOperand(value, 'end');
|
||||||
@ -212,7 +258,8 @@ function buildEquality(value, isDateField) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Builds a comparison condition for a single operator.
|
// Builds a comparison condition for a single operator.
|
||||||
function buildComparison(name, value, isDateField) {
|
function buildComparison(name, value, isDateField, isBooleanField = false) {
|
||||||
|
if (isBooleanField) return buildBooleanComparison(name, value);
|
||||||
if (name === 'eq') return buildEquality(value, isDateField);
|
if (name === 'eq') return buildEquality(value, isDateField);
|
||||||
|
|
||||||
if (name === 'ne') {
|
if (name === 'ne') {
|
||||||
@ -233,15 +280,21 @@ function buildComparison(name, value, isDateField) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parses a leaf token (range, comparison, or plain value) into a condition.
|
// Parses a leaf token (range, comparison, or plain value) into a condition.
|
||||||
function parseLeafCondition(rawToken, isDateField) {
|
function parseLeafCondition(rawToken, isDateField, isBooleanField = false) {
|
||||||
const token = rawToken.trim();
|
const token = rawToken.trim();
|
||||||
if (token === '') {
|
if (token === '') {
|
||||||
|
if (isBooleanField) return NO_MATCH_CONDITION;
|
||||||
return { op: { $regex: '^$', $options: 'i' } };
|
return { op: { $regex: '^$', $options: 'i' } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interval: a..b, ..b, a..
|
// Interval: a..b, ..b, a..
|
||||||
const rangeIdx = token.indexOf('..');
|
const rangeIdx = token.indexOf('..');
|
||||||
if (rangeIdx !== -1) {
|
if (rangeIdx !== -1) {
|
||||||
|
if (isBooleanField) {
|
||||||
|
const lo = stripIgnoreCase(token.slice(0, rangeIdx).trim());
|
||||||
|
const hi = stripIgnoreCase(token.slice(rangeIdx + 2).trim());
|
||||||
|
if (lo !== '' || hi !== '') return NO_MATCH_CONDITION;
|
||||||
|
}
|
||||||
const lo = stripIgnoreCase(token.slice(0, rangeIdx).trim());
|
const lo = stripIgnoreCase(token.slice(0, rangeIdx).trim());
|
||||||
const hi = stripIgnoreCase(token.slice(rangeIdx + 2).trim());
|
const hi = stripIgnoreCase(token.slice(rangeIdx + 2).trim());
|
||||||
const op = {};
|
const op = {};
|
||||||
@ -263,11 +316,16 @@ function parseLeafCondition(rawToken, isDateField) {
|
|||||||
];
|
];
|
||||||
for (const [symbol, name] of operators) {
|
for (const [symbol, name] of operators) {
|
||||||
if (token.startsWith(symbol)) {
|
if (token.startsWith(symbol)) {
|
||||||
return buildComparison(name, stripIgnoreCase(token.slice(symbol.length).trim()), isDateField);
|
return buildComparison(
|
||||||
|
name,
|
||||||
|
stripIgnoreCase(token.slice(symbol.length).trim()),
|
||||||
|
isDateField,
|
||||||
|
isBooleanField
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildEquality(stripIgnoreCase(token), isDateField);
|
return buildEquality(stripIgnoreCase(token), isDateField, isBooleanField);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Converts a condition descriptor into a MongoDB query object for `property`.
|
// Converts a condition descriptor into a MongoDB query object for `property`.
|
||||||
@ -309,26 +367,33 @@ function combineOr(children, property) {
|
|||||||
return { query: { $or: children.map((child) => conditionToQuery(child, property)) } };
|
return { query: { $or: children.map((child) => conditionToQuery(child, property)) } };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCondition(node, property, isDateField) {
|
function buildCondition(node, property, isDateField, isBooleanField = false) {
|
||||||
if (node.type === 'leaf') {
|
if (node.type === 'leaf') {
|
||||||
return parseLeafCondition(node.token, isDateField);
|
return parseLeafCondition(node.token, isDateField, isBooleanField);
|
||||||
}
|
}
|
||||||
const children = node.items.map((item) => buildCondition(item, property, isDateField));
|
const children = node.items.map((item) =>
|
||||||
|
buildCondition(item, property, isDateField, isBooleanField)
|
||||||
|
);
|
||||||
return node.type === 'and' ? combineAnd(children, property) : combineOr(children, property);
|
return node.type === 'and' ? combineAnd(children, property) : combineOr(children, property);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFilter(property, value) {
|
function parseFilter(property, value, model = null) {
|
||||||
// Normalize state filter to state.type for schemas with state: { type }
|
// Normalize state filter to state.type for schemas with state: { type }
|
||||||
if (property === 'state') {
|
if (property === 'state') {
|
||||||
property = 'state.type';
|
property = 'state.type';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isBoolField = isBooleanField(property, model);
|
||||||
|
|
||||||
if (value?._id !== undefined && value?._id !== null) {
|
if (value?._id !== undefined && value?._id !== null) {
|
||||||
return { [property]: { _id: new mongoose.Types.ObjectId(value._id) } };
|
return { [property]: { _id: new mongoose.Types.ObjectId(value._id) } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-string values (actual booleans, numbers, objects, etc.) pass through.
|
// Non-string values pass through; boolean fields only accept actual booleans.
|
||||||
if (typeof value !== 'string') {
|
if (typeof value !== 'string') {
|
||||||
|
if (isBoolField && typeof value !== 'boolean') {
|
||||||
|
return { [property]: { $in: [] } };
|
||||||
|
}
|
||||||
return { [property]: value };
|
return { [property]: value };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,7 +405,7 @@ function parseFilter(property, value) {
|
|||||||
|
|
||||||
const isDateField = looksLikeDateField(property);
|
const isDateField = looksLikeDateField(property);
|
||||||
const tree = parseExpression(trimmed);
|
const tree = parseExpression(trimmed);
|
||||||
const condition = buildCondition(tree, property, isDateField);
|
const condition = buildCondition(tree, property, isDateField, isBoolField);
|
||||||
return conditionToQuery(condition, property);
|
return conditionToQuery(condition, property);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1136,14 +1201,13 @@ function mergeFilterClauses(clauses) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Returns a filter object based on allowed filters and req.query
|
// Returns a filter object based on allowed filters and req.query
|
||||||
function getFilter(query, allowedFilters, parse = true) {
|
function getFilter(query, allowedFilters, parse = true, model = null) {
|
||||||
const clauses = [];
|
const clauses = [];
|
||||||
for (const [key, value] of Object.entries(query)) {
|
for (const [key, value] of Object.entries(query)) {
|
||||||
if (allowedFilters.includes(key)) {
|
if (allowedFilters.includes(key)) {
|
||||||
clauses.push(parse ? parseFilter(key, value) : { [key]: value });
|
clauses.push(parse ? parseFilter(key, value, model) : { [key]: value });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('clauses', clauses);
|
|
||||||
return mergeFilterClauses(clauses);
|
return mergeFilterClauses(clauses);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user