From f026706e25358e86eaeee487f73ccaf5c421aaea Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 10 Jan 2025 21:20:46 +0200 Subject: [PATCH] chore(test): fix typing errors in parser --- spec/search/parser.spec.ts | 636 +++++++++--------- src/services/search/expressions/and.ts | 2 +- .../search/expressions/attribute_exists.ts | 4 +- .../search/expressions/label_comparison.ts | 6 +- src/services/search/expressions/not.ts | 2 +- .../expressions/note_content_fulltext.ts | 2 +- .../search/expressions/note_flat_text.ts | 2 +- src/services/search/expressions/or.ts | 2 +- .../search/expressions/order_by_and_limit.ts | 2 +- .../search/expressions/property_comparison.ts | 6 +- src/services/search/search_context.ts | 2 +- src/services/search/services/parse.ts | 9 +- 12 files changed, 353 insertions(+), 322 deletions(-) diff --git a/spec/search/parser.spec.ts b/spec/search/parser.spec.ts index 7774a8d1e..09c7ce06b 100644 --- a/spec/search/parser.spec.ts +++ b/spec/search/parser.spec.ts @@ -1,13 +1,274 @@ -// @ts-nocheck -// There are many issues with the types of the parser e.g. "parse" function returns "Expression" -// but we access properties like "subExpressions" which is not defined in the "Expression" class. - +import AndExp from "../../src/services/search/expressions/and.js"; +import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js"; import Expression from "../../src/services/search/expressions/expression.js"; +import LabelComparisonExp from "../../src/services/search/expressions/label_comparison.js"; +import NotExp from "../../src/services/search/expressions/not.js"; +import NoteContentFulltextExp from "../../src/services/search/expressions/note_content_fulltext.js"; +import NoteFlatTextExp from "../../src/services/search/expressions/note_flat_text.js"; +import OrExp from "../../src/services/search/expressions/or.js"; import OrderByAndLimitExp from "../../src/services/search/expressions/order_by_and_limit.js"; +import PropertyComparisonExp from "../../src/services/search/expressions/property_comparison.js"; import SearchContext from "../../src/services/search/search_context.js"; -import parse from "../../src/services/search/services/parse.js"; +import { default as parseInternal, type ParseOpts } from "../../src/services/search/services/parse.js"; -function tokens(toks: Array, cur = 0): Array { +describe("Parser", () => { + it("fulltext parser without content", () => { + const rootExp = parse({ + fulltextTokens: tokens(["hello", "hi"]), + expressionTokens: [], + searchContext: new SearchContext() + }, AndExp); + + expectExpression(rootExp.subExpressions[0], PropertyComparisonExp); + const orExp = expectExpression(rootExp.subExpressions[2], OrExp); + const flatTextExp = expectExpression(orExp.subExpressions[0], NoteFlatTextExp); + expect(flatTextExp.tokens).toEqual(["hello", "hi"]); + }); + + it("fulltext parser with content", () => { + const rootExp = parse({ + fulltextTokens: tokens(["hello", "hi"]), + expressionTokens: [], + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + + const orExp = expectExpression(rootExp.subExpressions[2], OrExp); + + const firstSub = expectExpression(orExp.subExpressions[0], NoteFlatTextExp); + expect(firstSub.tokens).toEqual(["hello", "hi"]); + + const secondSub = expectExpression(orExp.subExpressions[1], NoteContentFulltextExp); + expect(secondSub.tokens).toEqual(["hello", "hi"]); + }); + + it("simple label comparison", () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(["#mylabel", "=", "text"]), + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + const labelComparisonExp = expectExpression(rootExp.subExpressions[2], LabelComparisonExp); + expect(labelComparisonExp.attributeType).toEqual("label"); + expect(labelComparisonExp.attributeName).toEqual("mylabel"); + expect(labelComparisonExp.comparator).toBeTruthy(); + }); + + it("simple attribute negation", () => { + let rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(["#!mylabel"]), + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + let notExp = expectExpression(rootExp.subExpressions[2], NotExp); + let attributeExistsExp = expectExpression(notExp.subExpression, AttributeExistsExp); + expect(attributeExistsExp.attributeType).toEqual("label"); + expect(attributeExistsExp.attributeName).toEqual("mylabel"); + + rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(["~!myrelation"]), + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + notExp = expectExpression(rootExp.subExpressions[2], NotExp); + attributeExistsExp = expectExpression(notExp.subExpression, AttributeExistsExp); + expect(attributeExistsExp.attributeType).toEqual("relation"); + expect(attributeExistsExp.attributeName).toEqual("myrelation"); + }); + + it("simple label AND", () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]), + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + + const andExp = expectExpression(rootExp.subExpressions[2], AndExp); + const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp); + + expect(firstSub.attributeName).toEqual("first"); + expect(secondSub.attributeName).toEqual("second"); + }); + + it("simple label AND without explicit AND", () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]), + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + + const andExp = expectExpression(rootExp.subExpressions[2], AndExp); + const [firstSub, secondSub] = expectSubexpressions(andExp, LabelComparisonExp, LabelComparisonExp); + + expect(firstSub.attributeName).toEqual("first"); + expect(secondSub.attributeName).toEqual("second"); + }); + + it("simple label OR", () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]), + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + + const orExp = expectExpression(rootExp.subExpressions[2], OrExp); + const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, LabelComparisonExp); + expect(firstSub.attributeName).toEqual("first"); + expect(secondSub.attributeName).toEqual("second"); + }); + + it("fulltext and simple label", () => { + const rootExp = parse({ + fulltextTokens: tokens(["hello"]), + expressionTokens: tokens(["#mylabel", "=", "text"]), + searchContext: new SearchContext() + }, AndExp); + + const [firstSub, _, thirdSub, fourth] = expectSubexpressions(rootExp, PropertyComparisonExp, undefined, OrExp, LabelComparisonExp); + + expect(firstSub.propertyName).toEqual("isArchived"); + + const noteFlatTextExp = expectExpression(thirdSub.subExpressions[0], NoteFlatTextExp); + expect(noteFlatTextExp.tokens).toEqual(["hello"]); + + expect(fourth.attributeName).toEqual("mylabel"); + }); + + it("label sub-expression", () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]), + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + + const orExp = expectExpression(rootExp.subExpressions[2], OrExp); + const [firstSub, secondSub] = expectSubexpressions(orExp, LabelComparisonExp, AndExp); + + expect(firstSub.attributeName).toEqual("first"); + + const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, LabelComparisonExp, LabelComparisonExp); + expect(firstSubSub.attributeName).toEqual("second"); + expect(secondSubSub.attributeName).toEqual("third"); + }); + + it("label sub-expression without explicit operator", () => { + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]), + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + + const andExp = expectExpression(rootExp.subExpressions[2], AndExp); + const [firstSub, secondSub, thirdSub] = expectSubexpressions(andExp, AttributeExistsExp, OrExp, AttributeExistsExp); + + expect(firstSub.attributeName).toEqual("first"); + + const [firstSubSub, secondSubSub] = expectSubexpressions(secondSub, AttributeExistsExp, AttributeExistsExp); + expect(firstSubSub.attributeName).toEqual("second"); + expect(secondSubSub.attributeName).toEqual("third"); + + expect(thirdSub.attributeName).toEqual("fourth"); + }); + + it("parses limit without order by", () => { + const rootExp = parse({ + fulltextTokens: tokens(["hello", "hi"]), + expressionTokens: [], + searchContext: new SearchContext({ limit: 2 }) + }, OrderByAndLimitExp); + + expect(rootExp.limit).toBe(2); + expect(rootExp.subExpression).toBeInstanceOf(AndExp); + }); +}); + +describe("Invalid expressions", () => { + it("incomplete comparison", () => { + const searchContext = new SearchContext(); + + parseInternal({ + fulltextTokens: [], + expressionTokens: tokens(["#first", "="]), + searchContext + }); + + expect(searchContext.error).toEqual('Misplaced or incomplete expression "="'); + }); + + it("comparison between labels is impossible", () => { + let searchContext = new SearchContext(); + searchContext.originalQuery = "#first = #second"; + + parseInternal({ + fulltextTokens: [], + expressionTokens: tokens(["#first", "=", "#second"]), + searchContext + }); + + expect(searchContext.error).toEqual(`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`); + + searchContext = new SearchContext(); + searchContext.originalQuery = "#first = note.relations.second"; + + parseInternal({ + fulltextTokens: [], + expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]), + searchContext + }); + + expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`); + + const rootExp = parse({ + fulltextTokens: [], + expressionTokens: [ + { token: "#first", inQuotes: false }, + { token: "=", inQuotes: false }, + { token: "#second", inQuotes: true } + ], + searchContext: new SearchContext() + }, AndExp); + + assertIsArchived(rootExp.subExpressions[0]); + + const labelComparisonExp = expectExpression(rootExp.subExpressions[2], LabelComparisonExp); + expect(labelComparisonExp.attributeType).toEqual("label"); + expect(labelComparisonExp.attributeName).toEqual("first"); + expect(labelComparisonExp.comparator).toBeTruthy(); + }); + + it("searching by relation without note property", () => { + const searchContext = new SearchContext(); + + parseInternal({ + fulltextTokens: [], + expressionTokens: tokens(["~first", "=", "text", "-", "abc"]), + searchContext + }); + + expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""'); + }); +}); + +type ClassType = new (...args: any[]) => T; + +function tokens(toks: (string | string[])[], cur = 0): Array { return toks.map((arg) => { if (Array.isArray(arg)) { return tokens(arg, cur); @@ -24,308 +285,71 @@ function tokens(toks: Array, cur = 0): Array { }); } -function assertIsArchived(exp: Expression) { - expect(exp.constructor.name).toEqual("PropertyComparisonExp"); +function assertIsArchived(_exp: Expression) { + const exp = expectExpression(_exp, PropertyComparisonExp); expect(exp.propertyName).toEqual("isArchived"); expect(exp.operator).toEqual("="); expect(exp.comparedValue).toEqual("false"); } -describe("Parser", () => { - it("fulltext parser without content", () => { - const rootExp = parse({ - fulltextTokens: tokens(["hello", "hi"]), - expressionTokens: [], - searchContext: new SearchContext({ excludeArchived: true }) - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); - expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); - expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); - expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(["hello", "hi"]); - }); - - it("fulltext parser with content", () => { - const rootExp = parse({ - fulltextTokens: tokens(["hello", "hi"]), - expressionTokens: [], - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); - - const subs = rootExp.subExpressions[2].subExpressions; - - expect(subs[0].constructor.name).toEqual("NoteFlatTextExp"); - expect(subs[0].tokens).toEqual(["hello", "hi"]); - - expect(subs[1].constructor.name).toEqual("NoteContentFulltextExp"); - expect(subs[1].tokens).toEqual(["hello", "hi"]); - }); - - it("simple label comparison", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#mylabel", "=", "text"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); - expect(rootExp.subExpressions[2].attributeType).toEqual("label"); - expect(rootExp.subExpressions[2].attributeName).toEqual("mylabel"); - expect(rootExp.subExpressions[2].comparator).toBeTruthy(); - }); - - it("simple attribute negation", () => { - let rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#!mylabel"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); - expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); - expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("label"); - expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("mylabel"); - - rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["~!myrelation"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - expect(rootExp.subExpressions[2].constructor.name).toEqual("NotExp"); - expect(rootExp.subExpressions[2].subExpression.constructor.name).toEqual("AttributeExistsExp"); - expect(rootExp.subExpressions[2].subExpression.attributeType).toEqual("relation"); - expect(rootExp.subExpressions[2].subExpression.attributeName).toEqual("myrelation"); - }); - - it("simple label AND", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "text", "and", "#second", "=", "text"]), - searchContext: new SearchContext(true) - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); - const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); - expect(secondSub.attributeName).toEqual("second"); - }); - - it("simple label AND without explicit AND", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "text", "#second", "=", "text"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); - const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); - expect(secondSub.attributeName).toEqual("second"); - }); - - it("simple label OR", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "text", "or", "#second", "=", "text"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); - const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("LabelComparisonExp"); - expect(secondSub.attributeName).toEqual("second"); - }); - - it("fulltext and simple label", () => { - const rootExp = parse({ - fulltextTokens: tokens(["hello"]), - expressionTokens: tokens(["#mylabel", "=", "text"]), - searchContext: new SearchContext({ excludeArchived: true }) - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - const [firstSub, secondSub, thirdSub, fourth] = rootExp.subExpressions; - - expect(firstSub.constructor.name).toEqual("PropertyComparisonExp"); - expect(firstSub.propertyName).toEqual("isArchived"); - - expect(thirdSub.constructor.name).toEqual("OrExp"); - expect(thirdSub.subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); - expect(thirdSub.subExpressions[0].tokens).toEqual(["hello"]); - - expect(fourth.constructor.name).toEqual("LabelComparisonExp"); - expect(fourth.attributeName).toEqual("mylabel"); - }); - - it("label sub-expression", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); - const [firstSub, secondSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("AndExp"); - const [firstSubSub, secondSubSub] = secondSub.subExpressions; - - expect(firstSubSub.constructor.name).toEqual("LabelComparisonExp"); - expect(firstSubSub.attributeName).toEqual("second"); - - expect(secondSubSub.constructor.name).toEqual("LabelComparisonExp"); - expect(secondSubSub.attributeName).toEqual("third"); - }); - - it("label sub-expression without explicit operator", () => { - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", ["#second", "or", "#third"], "#fourth"]), - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("AndExp"); - const [firstSub, secondSub, thirdSub] = rootExp.subExpressions[2].subExpressions; - - expect(firstSub.constructor.name).toEqual("AttributeExistsExp"); - expect(firstSub.attributeName).toEqual("first"); - - expect(secondSub.constructor.name).toEqual("OrExp"); - const [firstSubSub, secondSubSub] = secondSub.subExpressions; - - expect(firstSubSub.constructor.name).toEqual("AttributeExistsExp"); - expect(firstSubSub.attributeName).toEqual("second"); - - expect(secondSubSub.constructor.name).toEqual("AttributeExistsExp"); - expect(secondSubSub.attributeName).toEqual("third"); - - expect(thirdSub.constructor.name).toEqual("AttributeExistsExp"); - expect(thirdSub.attributeName).toEqual("fourth"); - }); - - it("parses limit without order by", () => { - const rootExp = parse({ - fulltextTokens: tokens(["hello", "hi"]), - expressionTokens: [], - searchContext: new SearchContext({ - excludeArchived: true, - limit: 2 - }) - }); - - expect(rootExp).toBeInstanceOf(OrderByAndLimitExp); - expect(rootExp.limit).toBe(2); - expect(rootExp.subExpression).toBeInstanceOf(AndExp); - }); -}); - -describe("Invalid expressions", () => { - it("incomplete comparison", () => { - const searchContext = new SearchContext(); - - parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "="]), - searchContext - }); - - expect(searchContext.error).toEqual('Misplaced or incomplete expression "="'); - }); - - it("comparison between labels is impossible", () => { - let searchContext = new SearchContext(); - searchContext.originalQuery = "#first = #second"; - - parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "#second"]), - searchContext - }); - - expect(searchContext.error).toEqual(`Error near token "#second" in "#first = #second", it's possible to compare with constant only.`); - - searchContext = new SearchContext(); - searchContext.originalQuery = "#first = note.relations.second"; - - parse({ - fulltextTokens: [], - expressionTokens: tokens(["#first", "=", "note", ".", "relations", "second"]), - searchContext - }); - - expect(searchContext.error).toEqual(`Error near token "note" in "#first = note.relations.second", it's possible to compare with constant only.`); - - const rootExp = parse({ - fulltextTokens: [], - expressionTokens: [ - { token: "#first", inQuotes: false }, - { token: "=", inQuotes: false }, - { token: "#second", inQuotes: true } - ], - searchContext: new SearchContext() - }); - - expect(rootExp.constructor.name).toEqual("AndExp"); - assertIsArchived(rootExp.subExpressions[0]); - - expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); - expect(rootExp.subExpressions[2].attributeType).toEqual("label"); - expect(rootExp.subExpressions[2].attributeName).toEqual("first"); - expect(rootExp.subExpressions[2].comparator).toBeTruthy(); - }); - - it("searching by relation without note property", () => { - const searchContext = new SearchContext(); - - parse({ - fulltextTokens: [], - expressionTokens: tokens(["~first", "=", "text", "-", "abc"]), - searchContext - }); - - expect(searchContext.error).toEqual('Relation can be compared only with property, e.g. ~relation.title=hello in ""'); - }); -}); +/** + * Parses the corresponding {@link Expression} from plain text, while also expecting the resulting expression to be of the given type. + * + * @param opts the options for parsing. + * @param type the expected type of the expression. + * @returns the expression typecasted to the expected type. + */ +function parse(opts: ParseOpts, type: ClassType) { + return expectExpression(parseInternal(opts), type); +} + +/** + * Expects the given {@link Expression} to be of the given type. + * + * @param exp an instance of an {@link Expression}. + * @param type a type class such as {@link AndExp}, {@link OrExp}, etc. + * @returns the same expression typecasted to the expected type. + */ +function expectExpression(exp: Expression, type: ClassType) { + expect(exp).toBeInstanceOf(type); + return exp as T; +} + +/** + * For an {@link AndExp}, it goes through all its subexpressions (up to fourth) and checks their type and returns them as a typecasted array. + * Each subexpression can have their own type. + * + * @param exp the expression containing one or more subexpressions. + * @param firstType the type of the first subexpression. + * @param secondType the type of the second subexpression. + * @param thirdType the type of the third subexpression. + * @param fourthType the type of the fourth subexpression. + * @returns an array of all the subexpressions (in order) typecasted to their expected type. + */ +function expectSubexpressions( + exp: AndExp, + firstType: ClassType, + secondType?: ClassType, + thirdType?: ClassType, + fourthType?: ClassType): [ FirstT, SecondT, ThirdT, FourthT ] +{ + expectExpression(exp.subExpressions[0], firstType); + if (secondType) { + expectExpression(exp.subExpressions[1], secondType); + } + if (thirdType) { + expectExpression(exp.subExpressions[2], thirdType); + } + if (fourthType) { + expectExpression(exp.subExpressions[3], fourthType); + } + return [ + exp.subExpressions[0] as FirstT, + exp.subExpressions[1] as SecondT, + exp.subExpressions[2] as ThirdT, + exp.subExpressions[3] as FourthT + ] +} diff --git a/src/services/search/expressions/and.ts b/src/services/search/expressions/and.ts index 2a6e07877..72918a74f 100644 --- a/src/services/search/expressions/and.ts +++ b/src/services/search/expressions/and.ts @@ -6,7 +6,7 @@ import Expression from "./expression.js"; import TrueExp from "./true.js"; class AndExp extends Expression { - private subExpressions: Expression[]; + subExpressions: Expression[]; static of(_subExpressions: (Expression | null | undefined)[]) { const subExpressions = _subExpressions.filter((exp) => !!exp) as Expression[]; diff --git a/src/services/search/expressions/attribute_exists.ts b/src/services/search/expressions/attribute_exists.ts index 1b0dc397b..52c866001 100644 --- a/src/services/search/expressions/attribute_exists.ts +++ b/src/services/search/expressions/attribute_exists.ts @@ -7,8 +7,8 @@ import becca from "../../../becca/becca.js"; import Expression from "./expression.js"; class AttributeExistsExp extends Expression { - private attributeType: string; - private attributeName: string; + attributeType: string; + attributeName: string; private isTemplateLabel: boolean; private prefixMatch: boolean; diff --git a/src/services/search/expressions/label_comparison.ts b/src/services/search/expressions/label_comparison.ts index ade1fed6c..c162aa0a2 100644 --- a/src/services/search/expressions/label_comparison.ts +++ b/src/services/search/expressions/label_comparison.ts @@ -8,9 +8,9 @@ import SearchContext from "../search_context.js"; type Comparator = (value: string) => boolean; class LabelComparisonExp extends Expression { - private attributeType: string; - private attributeName: string; - private comparator: Comparator; + attributeType: string; + attributeName: string; + comparator: Comparator; constructor(attributeType: string, attributeName: string, comparator: Comparator) { super(); diff --git a/src/services/search/expressions/not.ts b/src/services/search/expressions/not.ts index 25f61f6db..80f2adfe3 100644 --- a/src/services/search/expressions/not.ts +++ b/src/services/search/expressions/not.ts @@ -5,7 +5,7 @@ import SearchContext from "../search_context.js"; import Expression from "./expression.js"; class NotExp extends Expression { - private subExpression: Expression; + subExpression: Expression; constructor(subExpression: Expression) { super(); diff --git a/src/services/search/expressions/note_content_fulltext.ts b/src/services/search/expressions/note_content_fulltext.ts index 741a54a03..cd92e5178 100644 --- a/src/services/search/expressions/note_content_fulltext.ts +++ b/src/services/search/expressions/note_content_fulltext.ts @@ -34,7 +34,7 @@ type SearchRow = Pick !!exp); diff --git a/src/services/search/expressions/order_by_and_limit.ts b/src/services/search/expressions/order_by_and_limit.ts index c0cb676a7..3bbc53a67 100644 --- a/src/services/search/expressions/order_by_and_limit.ts +++ b/src/services/search/expressions/order_by_and_limit.ts @@ -18,7 +18,7 @@ interface OrderDefinition { class OrderByAndLimitExp extends Expression { private orderDefinitions: OrderDefinition[]; - private limit: number; + limit: number; subExpression: Expression | null; constructor(orderDefinitions: Pick[], limit?: number) { diff --git a/src/services/search/expressions/property_comparison.ts b/src/services/search/expressions/property_comparison.ts index a29661c47..02906f995 100644 --- a/src/services/search/expressions/property_comparison.ts +++ b/src/services/search/expressions/property_comparison.ts @@ -41,9 +41,9 @@ interface SearchContext { } class PropertyComparisonExp extends Expression { - private propertyName: string; - private operator: string; - private comparedValue: string; + propertyName: string; + operator: string; + comparedValue: string; private comparator; static isProperty(name: string) { diff --git a/src/services/search/search_context.ts b/src/services/search/search_context.ts index 5efc890a8..29fb7dbda 100644 --- a/src/services/search/search_context.ts +++ b/src/services/search/search_context.ts @@ -22,7 +22,7 @@ class SearchContext { originalQuery: string; fulltextQuery: string; dbLoadNeeded: boolean; - private error: string | null; + error: string | null; constructor(params: SearchParams = {}) { this.fastSearch = !!params.fastSearch; diff --git a/src/services/search/services/parse.ts b/src/services/search/services/parse.ts index 1a0fcce23..34acbc7ad 100644 --- a/src/services/search/services/parse.ts +++ b/src/services/search/services/parse.ts @@ -423,7 +423,14 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level return getAggregateExpression(); } -function parse({ fulltextTokens, expressionTokens, searchContext }: { fulltextTokens: TokenData[]; expressionTokens: TokenStructure; searchContext: SearchContext; originalQuery: string }) { +export interface ParseOpts { + fulltextTokens: TokenData[]; + expressionTokens: TokenStructure; + searchContext: SearchContext; + originalQuery?: string +} + +function parse({ fulltextTokens, expressionTokens, searchContext }: ParseOpts) { let expression: Expression | undefined | null; try {