diff --git a/src/components/Search/types.ts b/src/components/Search/types.ts index b48afb75d0de..b22c8e58e122 100644 --- a/src/components/Search/types.ts +++ b/src/components/Search/types.ts @@ -42,7 +42,7 @@ type SearchContext = { type ASTNode = { operator: ValueOf; left: ValueOf | ASTNode; - right: string | ASTNode; + right: string | ASTNode | string[]; }; type QueryFilter = { diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 4c6c382d6224..32fd834c1346 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -3,6 +3,11 @@ // https://peggyjs.org/ + + function buildFilter(operator, left, right) { + return { operator, left, right }; + } + function peg$subclass(child, parent) { function C() { this.constructor = child; } C.prototype = parent.prototype; @@ -176,32 +181,32 @@ function peg$parse(input, options) { var peg$startRuleFunction = peg$parsequery; var peg$c0 = "!="; - var peg$c1 = ">"; - var peg$c2 = ">="; - var peg$c3 = "<"; - var peg$c4 = "<="; - var peg$c5 = "type"; - var peg$c6 = "status"; - var peg$c7 = "date"; - var peg$c8 = "amount"; - var peg$c9 = "expenseType"; - var peg$c10 = "in"; - var peg$c11 = "currency"; - var peg$c12 = "merchant"; - var peg$c13 = "description"; - var peg$c14 = "from"; + var peg$c1 = ">="; + var peg$c2 = ">"; + var peg$c3 = "<="; + var peg$c4 = "<"; + var peg$c5 = "date"; + var peg$c6 = "amount"; + var peg$c7 = "merchant"; + var peg$c8 = "description"; + var peg$c9 = "reportID"; + var peg$c10 = "keyword"; + var peg$c11 = "in"; + var peg$c12 = "currency"; + var peg$c13 = "tag"; + var peg$c14 = "category"; var peg$c15 = "to"; - var peg$c16 = "category"; - var peg$c17 = "tag"; - var peg$c18 = "taxRate"; - var peg$c19 = "cardID"; - var peg$c20 = "reportID"; - var peg$c21 = "keyword"; - var peg$c22 = "sortBy"; - var peg$c23 = "sortOrder"; - var peg$c24 = "policyID"; - var peg$c25 = "has"; - var peg$c26 = "is"; + var peg$c16 = "taxRate"; + var peg$c17 = "cardID"; + var peg$c18 = "from"; + var peg$c19 = "expenseType"; + var peg$c20 = "has"; + var peg$c21 = "is"; + var peg$c22 = "type"; + var peg$c23 = "status"; + var peg$c24 = "sortBy"; + var peg$c25 = "sortOrder"; + var peg$c26 = "policyID"; var peg$c27 = "\""; var peg$r0 = /^[:=]/; @@ -209,118 +214,100 @@ function peg$parse(input, options) { var peg$r2 = /^[A-Za-z0-9_@.\/#&+\-\\',;]/; var peg$r3 = /^[ \t\r\n]/; - var peg$e0 = peg$classExpectation([":", "="], false, false); - var peg$e1 = peg$literalExpectation("!=", false); - var peg$e2 = peg$literalExpectation(">", false); + var peg$e0 = peg$otherExpectation("operator"); + var peg$e1 = peg$classExpectation([":", "="], false, false); + var peg$e2 = peg$literalExpectation("!=", false); var peg$e3 = peg$literalExpectation(">=", false); - var peg$e4 = peg$literalExpectation("<", false); + var peg$e4 = peg$literalExpectation(">", false); var peg$e5 = peg$literalExpectation("<=", false); - var peg$e6 = peg$literalExpectation("type", false); - var peg$e7 = peg$literalExpectation("status", false); + var peg$e6 = peg$literalExpectation("<", false); + var peg$e7 = peg$otherExpectation("key"); var peg$e8 = peg$literalExpectation("date", false); var peg$e9 = peg$literalExpectation("amount", false); - var peg$e10 = peg$literalExpectation("expenseType", false); - var peg$e11 = peg$literalExpectation("in", false); - var peg$e12 = peg$literalExpectation("currency", false); - var peg$e13 = peg$literalExpectation("merchant", false); - var peg$e14 = peg$literalExpectation("description", false); - var peg$e15 = peg$literalExpectation("from", false); - var peg$e16 = peg$literalExpectation("to", false); + var peg$e10 = peg$literalExpectation("merchant", false); + var peg$e11 = peg$literalExpectation("description", false); + var peg$e12 = peg$literalExpectation("reportID", false); + var peg$e13 = peg$literalExpectation("keyword", false); + var peg$e14 = peg$literalExpectation("in", false); + var peg$e15 = peg$literalExpectation("currency", false); + var peg$e16 = peg$literalExpectation("tag", false); var peg$e17 = peg$literalExpectation("category", false); - var peg$e18 = peg$literalExpectation("tag", false); + var peg$e18 = peg$literalExpectation("to", false); var peg$e19 = peg$literalExpectation("taxRate", false); var peg$e20 = peg$literalExpectation("cardID", false); - var peg$e21 = peg$literalExpectation("reportID", false); - var peg$e22 = peg$literalExpectation("keyword", false); - var peg$e23 = peg$literalExpectation("sortBy", false); - var peg$e24 = peg$literalExpectation("sortOrder", false); - var peg$e25 = peg$literalExpectation("policyID", false); - var peg$e26 = peg$literalExpectation("has", false); - var peg$e27 = peg$literalExpectation("is", false); - var peg$e28 = peg$literalExpectation("\"", false); - var peg$e29 = peg$classExpectation(["\"", "\r", "\n"], true, false); - var peg$e30 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_", "@", ".", "/", "#", "&", "+", "-", "\\", "'", ",", ";"], false, false); - var peg$e31 = peg$otherExpectation("whitespace"); - var peg$e32 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); - - var peg$f0 = function(filters) { - const withDefaults = applyDefaults(filters); - if (defaultValues.policyID) { - return applyPolicyID(withDefaults); - } - - return withDefaults; - }; + var peg$e21 = peg$literalExpectation("from", false); + var peg$e22 = peg$literalExpectation("expenseType", false); + var peg$e23 = peg$literalExpectation("has", false); + var peg$e24 = peg$literalExpectation("is", false); + var peg$e25 = peg$otherExpectation("default key"); + var peg$e26 = peg$literalExpectation("type", false); + var peg$e27 = peg$literalExpectation("status", false); + var peg$e28 = peg$literalExpectation("sortBy", false); + var peg$e29 = peg$literalExpectation("sortOrder", false); + var peg$e30 = peg$literalExpectation("policyID", false); + var peg$e31 = peg$otherExpectation("quote"); + var peg$e32 = peg$literalExpectation("\"", false); + var peg$e33 = peg$classExpectation(["\"", "\r", "\n"], true, false); + var peg$e34 = peg$otherExpectation("word"); + var peg$e35 = peg$classExpectation([["A", "Z"], ["a", "z"], ["0", "9"], "_", "@", ".", "/", "#", "&", "+", "-", "\\", "'", ",", ";"], false, false); + var peg$e36 = peg$otherExpectation("whitespace"); + var peg$e37 = peg$classExpectation([" ", "\t", "\r", "\n"], false, false); + + var peg$f0 = function(filters) { return applyDefaults(filters); }; var peg$f1 = function(head, tail) { - const allFilters = [head, ...tail.map(([_, filter]) => filter)].filter(filter => filter !== null); + const allFilters = [head, ...tail.map(([_, filter]) => filter)] + .filter(Boolean) + .filter((filter) => filter.right); if (!allFilters.length) { - return null; - } - const keywords = allFilters.filter((filter) => filter.left === "keyword" || filter.right?.left === "keyword") - const nonKeywords = allFilters.filter((filter) => filter.left !== "keyword" && filter.right?.left !== "keyword") - if(!nonKeywords.length){ - return keywords.reduce((result, filter) => buildFilter("or", result, filter)) - } - if(!keywords.length){ - return nonKeywords.reduce((result, filter) => buildFilter("and", result, filter)) - } - - return buildFilter("and", keywords.reduce((result, filter) => buildFilter("or", result, filter)), nonKeywords.reduce((result, filter) => buildFilter("and", result, filter))) - - return allFilters.reduce((result, filter) => buildFilter("and", result, filter)); - }; - var peg$f2 = function(field, op, value) { - if (isDefaultField(field)) { - updateDefaultValues(field, value.trim()); return null; } - if (isPolicyID(field)) { - updateDefaultValues(field, value.trim()); - return null; + const keywords = allFilters.filter( + (filter) => + filter.left === "keyword" || filter.right?.left === "keyword" + ); + const nonKeywords = allFilters.filter( + (filter) => + filter.left !== "keyword" && filter.right?.left !== "keyword" + ); + + const keywordFilter = buildFilter( + "eq", + "keyword", + keywords.map((filter) => filter.right).flat() + ); + if (keywordFilter.right.length > 0) { + nonKeywords.push(keywordFilter); } - - if (!field && !op) { - return buildFilter('eq', 'keyword', value.trim()); + return nonKeywords.reduce((result, filter) => + buildFilter("and", result, filter) + ); + }; + var peg$f2 = function(key, op, value) { + updateDefaultValues(key, value); + }; + var peg$f3 = function(value) { return buildFilter("eq", "keyword", value); }; + var peg$f4 = function(field, op, values) { + return buildFilter(op, field, values); + }; + var peg$f5 = function() { return "eq"; }; + var peg$f6 = function() { return "neq"; }; + var peg$f7 = function() { return "gte"; }; + var peg$f8 = function() { return "gt"; }; + var peg$f9 = function() { return "lte"; }; + var peg$f10 = function() { return "lt"; }; + var peg$f11 = function(parts) { + const value = parts.flat(); + if (value.length > 1) { + return value; } - - const values = value.split(','); - const operatorValue = op ?? 'eq'; - - return values.slice(1).reduce((acc, val) => buildFilter('or', acc, buildFilter(operatorValue, field, val.trim())), buildFilter(operatorValue, field, values[0])); + return value[0]; + }; + var peg$f12 = function(chars) { return chars.join(""); }; + var peg$f13 = function(chars) { + return chars.join("").trim().split(",").filter(Boolean); }; - var peg$f3 = function() { return "eq"; }; - var peg$f4 = function() { return "neq"; }; - var peg$f5 = function() { return "gt"; }; - var peg$f6 = function() { return "gte"; }; - var peg$f7 = function() { return "lt"; }; - var peg$f8 = function() { return "lte"; }; - var peg$f9 = function() { return "type"; }; - var peg$f10 = function() { return "status"; }; - var peg$f11 = function() { return "date"; }; - var peg$f12 = function() { return "amount"; }; - var peg$f13 = function() { return "expenseType"; }; - var peg$f14 = function() { return "in"; }; - var peg$f15 = function() { return "currency"; }; - var peg$f16 = function() { return "merchant"; }; - var peg$f17 = function() { return "description"; }; - var peg$f18 = function() { return "from"; }; - var peg$f19 = function() { return "to"; }; - var peg$f20 = function() { return "category"; }; - var peg$f21 = function() { return "tag"; }; - var peg$f22 = function() { return "taxRate"; }; - var peg$f23 = function() { return "cardID"; }; - var peg$f24 = function() { return "reportID"; }; - var peg$f25 = function() { return "keyword"; }; - var peg$f26 = function() { return "sortBy"; }; - var peg$f27 = function() { return "sortOrder"; }; - var peg$f28 = function() { return "policyID"; }; - var peg$f29 = function() { return "has"; }; - var peg$f30 = function() { return "is"; }; - var peg$f31 = function(parts) { return parts.join(''); }; - var peg$f32 = function(chars) { return chars.join(''); }; - var peg$f33 = function(chars) { return chars.join(''); }; - var peg$f34 = function() { return "and"; }; + var peg$f14 = function() { return "and"; }; var peg$currPos = options.peg$currPos | 0; var peg$savedPos = peg$currPos; var peg$posDetailsCache = [{ line: 1, column: 1 }]; @@ -540,24 +527,98 @@ function peg$parse(input, options) { } function peg$parsefilter() { + var s0, s1; + + s0 = peg$currPos; + s1 = peg$parsestandardFilter(); + if (s1 === peg$FAILED) { + s1 = peg$parsedefaultFilter(); + if (s1 === peg$FAILED) { + s1 = peg$parsefreeTextFilter(); + } + } + if (s1 !== peg$FAILED) { + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsedefaultFilter() { var s0, s1, s2, s3, s4, s5, s6; s0 = peg$currPos; s1 = peg$parse_(); - s2 = peg$parsekey(); - if (s2 === peg$FAILED) { - s2 = null; - } - s3 = peg$parse_(); - s4 = peg$parseoperator(); - if (s4 === peg$FAILED) { - s4 = null; + s2 = peg$parsedefaultKey(); + if (s2 !== peg$FAILED) { + s3 = peg$parse_(); + s4 = peg$parseoperator(); + if (s4 !== peg$FAILED) { + s5 = peg$parse_(); + s6 = peg$parseidentifier(); + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f2(s2, s4, s6); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; } - s5 = peg$parse_(); - s6 = peg$parseidentifier(); - if (s6 !== peg$FAILED) { + + return s0; + } + + function peg$parsefreeTextFilter() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = peg$parse_(); + s2 = peg$parseidentifier(); + if (s2 !== peg$FAILED) { + s3 = peg$parse_(); peg$savedPos = s0; - s0 = peg$f2(s2, s4, s6); + s0 = peg$f3(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsestandardFilter() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = peg$parse_(); + s2 = peg$parsekey(); + if (s2 !== peg$FAILED) { + s3 = peg$parse_(); + s4 = peg$parseoperator(); + if (s4 !== peg$FAILED) { + s5 = peg$parse_(); + s6 = peg$parseidentifier(); + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f4(s2, s4, s6); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } } else { peg$currPos = s0; s0 = peg$FAILED; @@ -569,17 +630,18 @@ function peg$parse(input, options) { function peg$parseoperator() { var s0, s1; + peg$silentFails++; s0 = peg$currPos; s1 = input.charAt(peg$currPos); if (peg$r0.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e0); } + if (peg$silentFails === 0) { peg$fail(peg$e1); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f3(); + s1 = peg$f5(); } s0 = s1; if (s0 === peg$FAILED) { @@ -589,67 +651,67 @@ function peg$parse(input, options) { peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e1); } + if (peg$silentFails === 0) { peg$fail(peg$e2); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f4(); + s1 = peg$f6(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 62) { + if (input.substr(peg$currPos, 2) === peg$c1) { s1 = peg$c1; - peg$currPos++; + peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e2); } + if (peg$silentFails === 0) { peg$fail(peg$e3); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f5(); + s1 = peg$f7(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c2) { + if (input.charCodeAt(peg$currPos) === 62) { s1 = peg$c2; - peg$currPos += 2; + peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e3); } + if (peg$silentFails === 0) { peg$fail(peg$e4); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f6(); + s1 = peg$f8(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.charCodeAt(peg$currPos) === 60) { + if (input.substr(peg$currPos, 2) === peg$c3) { s1 = peg$c3; - peg$currPos++; + peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e4); } + if (peg$silentFails === 0) { peg$fail(peg$e5); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f7(); + s1 = peg$f9(); } s0 = s1; if (s0 === peg$FAILED) { s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c4) { + if (input.charCodeAt(peg$currPos) === 60) { s1 = peg$c4; - peg$currPos += 2; + peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e5); } + if (peg$silentFails === 0) { peg$fail(peg$e6); } } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f8(); + s1 = peg$f10(); } s0 = s1; } @@ -657,6 +719,11 @@ function peg$parse(input, options) { } } } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } return s0; } @@ -664,316 +731,150 @@ function peg$parse(input, options) { function peg$parsekey() { var s0, s1; + peg$silentFails++; s0 = peg$currPos; if (input.substr(peg$currPos, 4) === peg$c5) { s1 = peg$c5; peg$currPos += 4; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e6); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f9(); + if (peg$silentFails === 0) { peg$fail(peg$e8); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; + if (s1 === peg$FAILED) { if (input.substr(peg$currPos, 6) === peg$c6) { s1 = peg$c6; peg$currPos += 6; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e7); } + if (peg$silentFails === 0) { peg$fail(peg$e9); } } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f10(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c7) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 8) === peg$c7) { s1 = peg$c7; - peg$currPos += 4; + peg$currPos += 8; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e8); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f11(); + if (peg$silentFails === 0) { peg$fail(peg$e10); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 6) === peg$c8) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 11) === peg$c8) { s1 = peg$c8; - peg$currPos += 6; + peg$currPos += 11; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e9); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f12(); + if (peg$silentFails === 0) { peg$fail(peg$e11); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 11) === peg$c9) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 8) === peg$c9) { s1 = peg$c9; - peg$currPos += 11; + peg$currPos += 8; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e10); } + if (peg$silentFails === 0) { peg$fail(peg$e12); } } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f13(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c10) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 7) === peg$c10) { s1 = peg$c10; - peg$currPos += 2; + peg$currPos += 7; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e11); } + if (peg$silentFails === 0) { peg$fail(peg$e13); } } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f14(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 8) === peg$c11) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c11) { s1 = peg$c11; - peg$currPos += 8; + peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e12); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f15(); + if (peg$silentFails === 0) { peg$fail(peg$e14); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; + if (s1 === peg$FAILED) { if (input.substr(peg$currPos, 8) === peg$c12) { s1 = peg$c12; peg$currPos += 8; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e13); } + if (peg$silentFails === 0) { peg$fail(peg$e15); } } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f16(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 11) === peg$c13) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c13) { s1 = peg$c13; - peg$currPos += 11; + peg$currPos += 3; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e14); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f17(); + if (peg$silentFails === 0) { peg$fail(peg$e16); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 4) === peg$c14) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 8) === peg$c14) { s1 = peg$c14; - peg$currPos += 4; + peg$currPos += 8; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e15); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f18(); + if (peg$silentFails === 0) { peg$fail(peg$e17); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; + if (s1 === peg$FAILED) { if (input.substr(peg$currPos, 2) === peg$c15) { s1 = peg$c15; peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e16); } + if (peg$silentFails === 0) { peg$fail(peg$e18); } } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f19(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 8) === peg$c16) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 7) === peg$c16) { s1 = peg$c16; - peg$currPos += 8; + peg$currPos += 7; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e17); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f20(); + if (peg$silentFails === 0) { peg$fail(peg$e19); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 3) === peg$c17) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 6) === peg$c17) { s1 = peg$c17; - peg$currPos += 3; + peg$currPos += 6; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e18); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f21(); + if (peg$silentFails === 0) { peg$fail(peg$e20); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 7) === peg$c18) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 4) === peg$c18) { s1 = peg$c18; - peg$currPos += 7; + peg$currPos += 4; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e19); } + if (peg$silentFails === 0) { peg$fail(peg$e21); } } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f22(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 6) === peg$c19) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 11) === peg$c19) { s1 = peg$c19; - peg$currPos += 6; + peg$currPos += 11; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e20); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f23(); + if (peg$silentFails === 0) { peg$fail(peg$e22); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 8) === peg$c20) { + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 3) === peg$c20) { s1 = peg$c20; - peg$currPos += 8; + peg$currPos += 3; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e21); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f24(); + if (peg$silentFails === 0) { peg$fail(peg$e23); } } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 7) === peg$c21) { - s1 = peg$c21; - peg$currPos += 7; + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c11) { + s1 = peg$c11; + peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e22); } + if (peg$silentFails === 0) { peg$fail(peg$e14); } } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f25(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 6) === peg$c22) { - s1 = peg$c22; - peg$currPos += 6; + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 2) === peg$c21) { + s1 = peg$c21; + peg$currPos += 2; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e23); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f26(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 9) === peg$c23) { - s1 = peg$c23; - peg$currPos += 9; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e24); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f27(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 8) === peg$c24) { - s1 = peg$c24; - peg$currPos += 8; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e25); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f28(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 3) === peg$c25) { - s1 = peg$c25; - peg$currPos += 3; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e26); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f29(); - } - s0 = s1; - if (s0 === peg$FAILED) { - s0 = peg$currPos; - if (input.substr(peg$currPos, 2) === peg$c26) { - s1 = peg$c26; - peg$currPos += 2; - } else { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e27); } - } - if (s1 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$f30(); - } - s0 = s1; - } - } - } + if (peg$silentFails === 0) { peg$fail(peg$e24); } } } } @@ -992,6 +893,80 @@ function peg$parse(input, options) { } } } + if (s1 !== peg$FAILED) { + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e7); } + } + + return s0; + } + + function peg$parsedefaultKey() { + var s0, s1; + + peg$silentFails++; + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c22) { + s1 = peg$c22; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e26); } + } + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 6) === peg$c23) { + s1 = peg$c23; + peg$currPos += 6; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e27); } + } + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 6) === peg$c24) { + s1 = peg$c24; + peg$currPos += 6; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e28); } + } + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 9) === peg$c25) { + s1 = peg$c25; + peg$currPos += 9; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e29); } + } + if (s1 === peg$FAILED) { + if (input.substr(peg$currPos, 8) === peg$c26) { + s1 = peg$c26; + peg$currPos += 8; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e30); } + } + } + } + } + } + if (s1 !== peg$FAILED) { + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e25); } + } return s0; } @@ -1018,7 +993,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f31(s1); + s1 = peg$f11(s1); } s0 = s1; @@ -1028,13 +1003,14 @@ function peg$parse(input, options) { function peg$parsequotedString() { var s0, s1, s2, s3; + peg$silentFails++; s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 34) { s1 = peg$c27; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e28); } + if (peg$silentFails === 0) { peg$fail(peg$e32); } } if (s1 !== peg$FAILED) { s2 = []; @@ -1043,7 +1019,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e29); } + if (peg$silentFails === 0) { peg$fail(peg$e33); } } while (s3 !== peg$FAILED) { s2.push(s3); @@ -1052,7 +1028,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e29); } + if (peg$silentFails === 0) { peg$fail(peg$e33); } } } if (input.charCodeAt(peg$currPos) === 34) { @@ -1060,11 +1036,11 @@ function peg$parse(input, options) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e28); } + if (peg$silentFails === 0) { peg$fail(peg$e32); } } if (s3 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f32(s2); + s0 = peg$f12(s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1073,6 +1049,11 @@ function peg$parse(input, options) { peg$currPos = s0; s0 = peg$FAILED; } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e31); } + } return s0; } @@ -1080,6 +1061,7 @@ function peg$parse(input, options) { function peg$parsealphanumeric() { var s0, s1, s2; + peg$silentFails++; s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); @@ -1087,7 +1069,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e30); } + if (peg$silentFails === 0) { peg$fail(peg$e35); } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { @@ -1097,7 +1079,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e30); } + if (peg$silentFails === 0) { peg$fail(peg$e35); } } } } else { @@ -1105,9 +1087,14 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f33(s1); + s1 = peg$f13(s1); } s0 = s1; + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e34); } + } return s0; } @@ -1118,7 +1105,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f34(); + s1 = peg$f14(); s0 = s1; return s0; @@ -1134,7 +1121,7 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e32); } + if (peg$silentFails === 0) { peg$fail(peg$e37); } } while (s1 !== peg$FAILED) { s0.push(s1); @@ -1143,39 +1130,28 @@ function peg$parse(input, options) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e32); } + if (peg$silentFails === 0) { peg$fail(peg$e37); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e31); } + if (peg$silentFails === 0) { peg$fail(peg$e36); } return s0; } const defaultValues = { - "type": "expense", - "status": "all", - "sortBy": "date", - "sortOrder": "desc", + type: "expense", + status: "all", + sortBy: "date", + sortOrder: "desc", }; - function buildFilter(operator, left, right) { - return { operator, left, right }; - } - function applyDefaults(filters) { return { ...defaultValues, - filters - }; - } - - function applyPolicyID(filtersWithDefaults) { - return { - ...filtersWithDefaults, - policyID: filtersWithDefaults.policyID + filters, }; } @@ -1183,14 +1159,6 @@ function peg$parse(input, options) { defaultValues[field] = value; } - function isDefaultField(field) { - return defaultValues.hasOwnProperty(field); - } - - function isPolicyID(field) { - return field === 'policyID'; - } - peg$result = peg$startRuleFunction(); if (options.peg$library) { diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index 805ee3e668de..bea1e5cfd6ff 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -5,149 +5,144 @@ // // query: entry point for the parser and rule to process the values returned by the filterList rule. Takes filters as an argument and returns the final AST output. // filterList: rule to process the array of filters returned by the filter rule. It takes head and tail as arguments, filters it for null values and builds the AST. -// filter: rule to build the filter object. It takes field, operator and value as input and returns {operator, left: field, right: value} or null if the left value is a defaultValues +// filter: abstract rule to simplify the filterList rule. It takes all filter types. +// defaultFilter: rule to process the default values returned by the defaultKey rule. It updates the default values object. +// freeTextFilter: rule to process the free text search values returned by the identifier rule. It builds filter Object. +// standardFilter: rule to process the values returned by the key rule. It builds filter Object. // operator: rule to match pre-defined search syntax operators, e.g. !=, >, etc // key: rule to match pre-defined search syntax fields, e.g. amount, merchant, etc +// defaultKey: rule to match pre-defined search syntax fields that are used to update default values, e.g. type, status, etc // identifier: composite rule to match patterns defined by the quotedString and alphanumeric rules // quotedString: rule to match a quoted string pattern, e.g. "this is a quoted string" // alphanumeric: rule to match unquoted alphanumeric characters, e.g. a-z, 0-9, _, @, etc // logicalAnd: rule to match whitespace and return it as a logical 'and' operator // whitespace: rule to match whitespaces -{ - const defaultValues = { - "type": "expense", - "status": "all", - "sortBy": "date", - "sortOrder": "desc", - }; - +// global initializer (code executed only once) +{{ function buildFilter(operator, left, right) { return { operator, left, right }; } +}} + +// per-parser initializer (code executed before every parse) +{ + const defaultValues = { + type: "expense", + status: "all", + sortBy: "date", + sortOrder: "desc", + }; function applyDefaults(filters) { return { ...defaultValues, - filters - }; - } - - function applyPolicyID(filtersWithDefaults) { - return { - ...filtersWithDefaults, - policyID: filtersWithDefaults.policyID + filters, }; } function updateDefaultValues(field, value) { defaultValues[field] = value; } - - function isDefaultField(field) { - return defaultValues.hasOwnProperty(field); - } - - function isPolicyID(field) { - return field === 'policyID'; - } } -query - = _ filters:filterList? _ { - const withDefaults = applyDefaults(filters); - if (defaultValues.policyID) { - return applyPolicyID(withDefaults); - } - - return withDefaults; - } +query = _ filters:filterList? _ { return applyDefaults(filters); } filterList = head:filter tail:(logicalAnd filter)* { - const allFilters = [head, ...tail.map(([_, filter]) => filter)].filter(filter => filter !== null); + const allFilters = [head, ...tail.map(([_, filter]) => filter)] + .filter(Boolean) + .filter((filter) => filter.right); if (!allFilters.length) { - return null; - } - const keywords = allFilters.filter((filter) => filter.left === "keyword" || filter.right?.left === "keyword") - const nonKeywords = allFilters.filter((filter) => filter.left !== "keyword" && filter.right?.left !== "keyword") - if(!nonKeywords.length){ - return keywords.reduce((result, filter) => buildFilter("or", result, filter)) - } - if(!keywords.length){ - return nonKeywords.reduce((result, filter) => buildFilter("and", result, filter)) - } - - return buildFilter("and", keywords.reduce((result, filter) => buildFilter("or", result, filter)), nonKeywords.reduce((result, filter) => buildFilter("and", result, filter))) - - return allFilters.reduce((result, filter) => buildFilter("and", result, filter)); - } - -filter - = _ field:key? _ op:operator? _ value:identifier { - if (isDefaultField(field)) { - updateDefaultValues(field, value.trim()); return null; } - if (isPolicyID(field)) { - updateDefaultValues(field, value.trim()); - return null; + const keywords = allFilters.filter( + (filter) => + filter.left === "keyword" || filter.right?.left === "keyword" + ); + const nonKeywords = allFilters.filter( + (filter) => + filter.left !== "keyword" && filter.right?.left !== "keyword" + ); + + const keywordFilter = buildFilter( + "eq", + "keyword", + keywords.map((filter) => filter.right).flat() + ); + if (keywordFilter.right.length > 0) { + nonKeywords.push(keywordFilter); } + return nonKeywords.reduce((result, filter) => + buildFilter("and", result, filter) + ); + } - if (!field && !op) { - return buildFilter('eq', 'keyword', value.trim()); - } +filter = @(standardFilter / defaultFilter / freeTextFilter) - const values = value.split(','); - const operatorValue = op ?? 'eq'; +defaultFilter + = _ key:defaultKey _ op:operator _ value:identifier { + updateDefaultValues(key, value); + } - return values.slice(1).reduce((acc, val) => buildFilter('or', acc, buildFilter(operatorValue, field, val.trim())), buildFilter(operatorValue, field, values[0])); +freeTextFilter + = _ value:identifier _ { return buildFilter("eq", "keyword", value); } + +standardFilter + = _ field:key _ op:operator _ values:identifier { + return buildFilter(op, field, values); } -operator +operator "operator" = (":" / "=") { return "eq"; } / "!=" { return "neq"; } - / ">" { return "gt"; } / ">=" { return "gte"; } - / "<" { return "lt"; } + / ">" { return "gt"; } / "<=" { return "lte"; } + / "<" { return "lt"; } -key - = "type" { return "type"; } - / "status" { return "status"; } - / "date" { return "date"; } - / "amount" { return "amount"; } - / "expenseType" { return "expenseType"; } - / "in" { return "in"; } - / "currency" { return "currency"; } - / "merchant" { return "merchant"; } - / "description" { return "description"; } - / "from" { return "from"; } - / "to" { return "to"; } - / "category" { return "category"; } - / "tag" { return "tag"; } - / "taxRate" { return "taxRate"; } - / "cardID" { return "cardID"; } - / "reportID" { return "reportID"; } - / "keyword" { return "keyword"; } - / "sortBy" { return "sortBy"; } - / "sortOrder" { return "sortOrder"; } - / "policyID" { return "policyID"; } - / "has" { return "has"; } - / "is" { return "is"; } +key "key" + = @( + "date" + / "amount" + / "merchant" + / "description" + / "reportID" + / "keyword" + / "in" + / "currency" + / "tag" + / "category" + / "to" + / "taxRate" + / "cardID" + / "from" + / "expenseType" + / "has" + / "in" + / "is" + ) + +defaultKey "default key" + = @("type" / "status" / "sortBy" / "sortOrder" / "policyID") identifier - = parts:(quotedString / alphanumeric)+ { return parts.join(''); } + = parts:(quotedString / alphanumeric)+ { + const value = parts.flat(); + if (value.length > 1) { + return value; + } + return value[0]; + } -quotedString - = '"' chars:[^"\r\n]* '"' { return chars.join(''); } +quotedString "quote" = "\"" chars:[^"\r\n]* "\"" { return chars.join(""); } -alphanumeric - = chars:[A-Za-z0-9_@./#&+\-\\',;]+ { return chars.join(''); } +alphanumeric "word" + = chars:[A-Za-z0-9_@./#&+\-\\',;]+ { + return chars.join("").trim().split(",").filter(Boolean); + } -logicalAnd - = _ { return "and"; } +logicalAnd = _ { return "and"; } -_ "whitespace" - = [ \t\r\n]* +_ "whitespace" = [ \t\r\n]* diff --git a/src/libs/SearchUtils.ts b/src/libs/SearchUtils.ts index e0832b6a1512..1b1fcaee8682 100644 --- a/src/libs/SearchUtils.ts +++ b/src/libs/SearchUtils.ts @@ -469,10 +469,11 @@ function buildAmountFilterQuery(filterValues: Partial } function sanitizeString(str: string) { - if (str.includes(' ') || str.includes(',')) { - return `"${str}"`; + const safeStr = str; + if (safeStr.includes(' ') || safeStr.includes(',')) { + return `"${safeStr}"`; } - return str; + return safeStr; } function getExpenseTypeTranslationKey(expenseType: ValueOf): TranslationPaths { @@ -523,7 +524,7 @@ function buildQueryStringFromFilters(filterValues: Partial CONST.SEARCH.SYNTAX_FILTER_KEYS[key] === filterKey); if (keyInCorrectForm) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS[keyInCorrectForm]}:${filterValue as string}`; + return `${filterValue as string}`; } } @@ -574,7 +575,7 @@ function getFilters(queryJSON: SearchQueryJSON) { traverse(node.left); } - if (typeof node?.right === 'object' && node.right) { + if (typeof node?.right === 'object' && node.right && !Array.isArray(node.right)) { traverse(node.right); } @@ -589,10 +590,19 @@ function getFilters(queryJSON: SearchQueryJSON) { // the "?? []" is added only for typescript because otherwise TS throws an error, in newer TS versions this should be fixed const filterArray = filters[nodeKey] ?? []; - filterArray.push({ - operator: node.operator, - value: node.right as string | number, - }); + if (!Array.isArray(node.right)) { + filterArray.push({ + operator: node.operator, + value: node.right as string | number, + }); + } else { + node.right.forEach((element) => { + filterArray.push({ + operator: node.operator, + value: element as string | number, + }); + }); + } } if (queryJSON.filters) { @@ -629,7 +639,7 @@ function buildFilterString(filterName: string, queryFilters: QueryFilter[]) { if ((queryFilter.operator === 'eq' && queryFilters[index - 1]?.operator === 'eq') || (queryFilter.operator === 'neq' && queryFilters[index - 1]?.operator === 'neq')) { filterValueString += ` ${sanitizeString(queryFilter.value.toString())}`; } else { - filterValueString += ` ${filterName}${operatorToSignMap[queryFilter.operator]}${queryFilter.value}`; + filterValueString += ` ${filterName}${operatorToSignMap[queryFilter.operator]}${sanitizeString(queryFilter.value.toString())}`; } }); diff --git a/tests/unit/SearchParserTest.ts b/tests/unit/SearchParserTest.ts index c8d3764f0546..1f2a97771bf3 100644 --- a/tests/unit/SearchParserTest.ts +++ b/tests/unit/SearchParserTest.ts @@ -193,17 +193,9 @@ const tests = [ right: '200', }, right: { - operator: 'or', - left: { - operator: 'eq', - left: 'expenseType', - right: 'cash', - }, - right: { - operator: 'eq', - left: 'expenseType', - right: 'card', - }, + operator: 'eq', + left: 'expenseType', + right: ['cash', 'card'], }, }, right: { @@ -219,25 +211,9 @@ const tests = [ }, }, right: { - operator: 'or', - left: { - operator: 'or', - left: { - operator: 'eq', - left: 'category', - right: 'travel', - }, - right: { - operator: 'eq', - left: 'category', - right: 'hotel', - }, - }, - right: { - operator: 'eq', - left: 'category', - right: 'meal & entertainment', - }, + operator: 'eq', + left: 'category', + right: ['travel', 'hotel', 'meal & entertainment'], }, }, }, @@ -252,23 +228,15 @@ const tests = [ filters: { operator: 'and', left: { - operator: 'or', - left: { - operator: 'eq', - left: 'keyword', - right: 'las', - }, - right: { - operator: 'eq', - left: 'keyword', - right: 'vegas', - }, - }, - right: { operator: 'gt', left: 'amount', right: '200', }, + right: { + operator: 'eq', + left: 'keyword', + right: ['las', 'vegas'], + }, }, }, }, @@ -282,19 +250,6 @@ const tests = [ filters: { operator: 'and', left: { - operator: 'or', - left: { - operator: 'eq', - left: 'keyword', - right: 'las', - }, - right: { - operator: 'eq', - left: 'keyword', - right: 'vegas', - }, - }, - right: { operator: 'and', left: { operator: 'gt', @@ -307,6 +262,11 @@ const tests = [ right: 'Hotel : Marriott', }, }, + right: { + operator: 'eq', + left: 'keyword', + right: ['las', 'vegas'], + }, }, }, }, @@ -320,19 +280,6 @@ const tests = [ filters: { operator: 'and', left: { - operator: 'or', - left: { - operator: 'eq', - left: 'keyword', - right: 'las', - }, - right: { - operator: 'eq', - left: 'keyword', - right: 'vegas', - }, - }, - right: { operator: 'and', left: { operator: 'and', @@ -351,55 +298,28 @@ const tests = [ right: 'Hotel : Marriott', }, }, - right: { - operator: 'or', - left: { - operator: 'eq', - left: 'date', - right: '2024-01-01', - }, - right: { - operator: 'eq', - left: 'date', - right: '2024-02-01', - }, - }, - }, - right: { - operator: 'or', - left: { - operator: 'eq', - left: 'merchant', - right: 'Expensify', - }, - right: { - operator: 'eq', - left: 'merchant', - right: 'Inc.', - }, - }, - }, - right: { - operator: 'or', - left: { - operator: 'or', - left: { - operator: 'eq', - left: 'tag', - right: 'hotel', - }, right: { operator: 'eq', - left: 'tag', - right: 'travel', + left: 'date', + right: ['2024-01-01', '2024-02-01'], }, }, right: { operator: 'eq', - left: 'tag', - right: 'meals & entertainment', + left: 'merchant', + right: 'Expensify, Inc.', }, }, + right: { + operator: 'eq', + left: 'tag', + right: ['hotel', 'travel', 'meals & entertainment'], + }, + }, + right: { + operator: 'eq', + left: 'keyword', + right: ['las', 'vegas'], }, }, },