Add boolean field handling to filtering logic in utils.js
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:
Tom Butcher 2026-08-01 14:33:03 +01:00
parent c043ad7a86
commit 6c694d7d48

View File

@ -10,6 +10,7 @@ import config from './config.js';
import crypto from 'crypto';
import canonicalize from 'canonical-json';
import { getModelByName } from './services/misc/model.js';
import { models } from './database/schemas/models.js';
import { createEmailRenderAuthCode } from './services/misc/emailRenderAuth.js';
import { Worker } from 'worker_threads';
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);
}
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) {
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).
function buildEquality(value, isDateField) {
function buildEquality(value, isDateField, isBooleanField = false) {
if (isBooleanField) return buildBooleanEquality(value);
if (isDateField) {
const start = parseDateOperand(value, 'start');
const end = parseDateOperand(value, 'end');
@ -212,7 +258,8 @@ function buildEquality(value, isDateField) {
}
// 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 === 'ne') {
@ -233,15 +280,21 @@ function buildComparison(name, value, isDateField) {
}
// 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();
if (token === '') {
if (isBooleanField) return NO_MATCH_CONDITION;
return { op: { $regex: '^$', $options: 'i' } };
}
// Interval: a..b, ..b, a..
const rangeIdx = token.indexOf('..');
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 hi = stripIgnoreCase(token.slice(rangeIdx + 2).trim());
const op = {};
@ -263,11 +316,16 @@ function parseLeafCondition(rawToken, isDateField) {
];
for (const [symbol, name] of operators) {
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`.
@ -309,26 +367,33 @@ function combineOr(children, 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') {
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);
}
function parseFilter(property, value) {
function parseFilter(property, value, model = null) {
// Normalize state filter to state.type for schemas with state: { type }
if (property === 'state') {
property = 'state.type';
}
const isBoolField = isBooleanField(property, model);
if (value?._id !== undefined && value?._id !== null) {
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 (isBoolField && typeof value !== 'boolean') {
return { [property]: { $in: [] } };
}
return { [property]: value };
}
@ -340,7 +405,7 @@ function parseFilter(property, value) {
const isDateField = looksLikeDateField(property);
const tree = parseExpression(trimmed);
const condition = buildCondition(tree, property, isDateField);
const condition = buildCondition(tree, property, isDateField, isBoolField);
return conditionToQuery(condition, property);
}
@ -1136,14 +1201,13 @@ function mergeFilterClauses(clauses) {
}
// 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 = [];
for (const [key, value] of Object.entries(query)) {
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);
}