Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code fa1f51f802 fix(rest-api): handle mixed bare filters and conjunctions in filter parser
https://sonarly.com/issue/32412?type=bug

The REST API filter parser's `addDefaultConjunctionIfMissing` function uses a naive heuristic — checking for the presence of any parentheses — to decide whether to wrap the filter in a default `and()` conjunction. When a filter mixes bare conditions with explicit conjunctions (e.g., `status[eq]:'TODO',and(title[ilike]:'%test%')`), the parentheses from the nested conjunction trick the function into thinking a root conjunction already exists, causing the entire filter string to be parsed as a single base filter and producing a 400 error.

Fix: **What changed:** Replaced the naive parentheses-presence heuristic in `addDefaultConjunctionIfMissing` with the same regex pattern used by `parseFilter` to detect a root-level conjunction.

**Before:** The function checked `filterQuery.includes('(') && filterQuery.includes(')')`. If the filter contained *any* parentheses (e.g. from a nested `and(...)` or `or(...)`), it assumed a root conjunction already existed and returned the input unchanged. This meant `status[eq]:'TODO',and(title[ilike]:'%test%')` was NOT wrapped, causing a parse failure downstream.

**After:** The function checks `^(or|and|not)\((.+)\)$` — the exact same regex `parseFilter` uses to detect a root conjunction. This correctly identifies that `status[eq]:'TODO',and(title[ilike]:'%test%')` does NOT start with a root conjunction and wraps it in `and(...)`.

**Files changed:**
1. `add-default-conjunction.util.ts` — Replaced `includes('(')` check with `ROOT_CONJUNCTION_REGEX.test()` using the same regex pattern as `parseFilter`.
2. `__tests__/add-default-conjunction.util.spec.ts` — Added 5 test cases covering: mixed bare filters with nested conjunctions (the reported bug), `or` and `not` root conjunctions, and multiple bare filters without parentheses.
2026-04-29 10:46:13 +00:00
2 changed files with 39 additions and 1 deletions
@@ -12,4 +12,38 @@ describe('addDefaultConjunctionIfMissing', () => {
'and(field[eq]:1)',
);
});
it('should add default conjunction when bare filters are mixed with nested conjunctions', () => {
expect(
addDefaultConjunctionIfMissing(
"status[eq]:'TODO',and(title[ilike]:'%test%')",
),
).toEqual("and(status[eq]:'TODO',and(title[ilike]:'%test%'))");
});
it('should add default conjunction for multiple bare filters with nested or', () => {
expect(
addDefaultConjunctionIfMissing(
"field1[in]:['a','b'],or(field2[like]:'%x%')",
),
).toEqual("and(field1[in]:['a','b'],or(field2[like]:'%x%'))");
});
it('should not add default conjunction for or root conjunction', () => {
expect(
addDefaultConjunctionIfMissing('or(field[eq]:1,field[eq]:2)'),
).toEqual('or(field[eq]:1,field[eq]:2)');
});
it('should not add default conjunction for not root conjunction', () => {
expect(addDefaultConjunctionIfMissing('not(field[eq]:1)')).toEqual(
'not(field[eq]:1)',
);
});
it('should add default conjunction for multiple bare filters', () => {
expect(
addDefaultConjunctionIfMissing('field1[eq]:1,field2[gte]:10'),
).toEqual('and(field1[eq]:1,field2[gte]:10)');
});
});
@@ -2,8 +2,12 @@ import { Conjunctions } from 'src/engine/api/rest/input-request-parsers/filter-p
export const DEFAULT_CONJUNCTION = Conjunctions.and;
const ROOT_CONJUNCTION_REGEX = new RegExp(
`^(${Object.values(Conjunctions).join('|')})\\((.+)\\)$`,
);
export const addDefaultConjunctionIfMissing = (filterQuery: string): string => {
if (!(filterQuery.includes('(') && filterQuery.includes(')'))) {
if (!ROOT_CONJUNCTION_REGEX.test(filterQuery)) {
return `${DEFAULT_CONJUNCTION}(${filterQuery})`;
}