From fa1f51f8028c230a5de9fd4acb6221f024e71851 Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Wed, 29 Apr 2026 10:46:13 +0000 Subject: [PATCH] fix(rest-api): handle mixed bare filters and conjunctions in filter parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../add-default-conjunction.util.spec.ts | 34 +++++++++++++++++++ .../add-default-conjunction.util.ts | 6 +++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/add-default-conjunction.util.spec.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/add-default-conjunction.util.spec.ts index e1a2c69af19..2a81670b97a 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/add-default-conjunction.util.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/__tests__/add-default-conjunction.util.spec.ts @@ -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)'); + }); }); diff --git a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts index 8fde997cd91..aecf7e800c7 100644 --- a/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts +++ b/packages/twenty-server/src/engine/api/rest/input-request-parsers/filter-parser-utils/add-default-conjunction.util.ts @@ -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})`; }