mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/develop' into renovate/jsdom-26.x
This commit is contained in:
		| @@ -1,12 +1,274 @@ | |||||||
| // @ts-nocheck | import AndExp from "../../src/services/search/expressions/and.js"; | ||||||
| // There are many issues with the types of the parser e.g. "parse" function returns "Expression" | import AttributeExistsExp from "../../src/services/search/expressions/attribute_exists.js"; | ||||||
| // but we access properties like "subExpressions" which is not defined in the "Expression" class. |  | ||||||
|  |  | ||||||
| import Expression from "../../src/services/search/expressions/expression.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 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<string>, cur = 0): Array<any> { | 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<T extends Expression> = new (...args: any[]) => T; | ||||||
|  |  | ||||||
|  | function tokens(toks: (string | string[])[], cur = 0): Array<any> { | ||||||
|     return toks.map((arg) => { |     return toks.map((arg) => { | ||||||
|         if (Array.isArray(arg)) { |         if (Array.isArray(arg)) { | ||||||
|             return tokens(arg, cur); |             return tokens(arg, cur); | ||||||
| @@ -23,293 +285,71 @@ function tokens(toks: Array<string>, cur = 0): Array<any> { | |||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function assertIsArchived(exp: Expression) { | function assertIsArchived(_exp: Expression) { | ||||||
|     expect(exp.constructor.name).toEqual("PropertyComparisonExp"); |     const exp = expectExpression(_exp, PropertyComparisonExp); | ||||||
|     expect(exp.propertyName).toEqual("isArchived"); |     expect(exp.propertyName).toEqual("isArchived"); | ||||||
|     expect(exp.operator).toEqual("="); |     expect(exp.operator).toEqual("="); | ||||||
|     expect(exp.comparedValue).toEqual("false"); |     expect(exp.comparedValue).toEqual("false"); | ||||||
| } | } | ||||||
|  |  | ||||||
| describe("Parser", () => { | /** | ||||||
|     it("fulltext parser without content", () => { |  * Parses the corresponding {@link Expression} from plain text, while also expecting the resulting expression to be of the given type. | ||||||
|         const rootExp = parse({ |  * | ||||||
|             fulltextTokens: tokens(["hello", "hi"]), |  * @param opts the options for parsing. | ||||||
|             expressionTokens: [], |  * @param type the expected type of the expression. | ||||||
|             searchContext: new SearchContext({ excludeArchived: true }) |  * @returns the expression typecasted to the expected type. | ||||||
|         }); |  */ | ||||||
|  | function parse<T extends Expression>(opts: ParseOpts, type: ClassType<T>) { | ||||||
|  |     return expectExpression(parseInternal(opts), type); | ||||||
|  | } | ||||||
|  |  | ||||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); | /** | ||||||
|         expect(rootExp.subExpressions[0].constructor.name).toEqual("PropertyComparisonExp"); |  * Expects the given {@link Expression} to be of the given type. | ||||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); |  * | ||||||
|         expect(rootExp.subExpressions[2].subExpressions[0].constructor.name).toEqual("NoteFlatTextExp"); |  * @param exp an instance of an {@link Expression}. | ||||||
|         expect(rootExp.subExpressions[2].subExpressions[0].tokens).toEqual(["hello", "hi"]); |  * @param type a type class such as {@link AndExp}, {@link OrExp}, etc. | ||||||
|     }); |  * @returns the same expression typecasted to the expected type. | ||||||
|  |  */ | ||||||
|  | function expectExpression<T extends Expression>(exp: Expression, type: ClassType<T>) { | ||||||
|  |     expect(exp).toBeInstanceOf(type); | ||||||
|  |     return exp as T; | ||||||
|  | } | ||||||
|  |  | ||||||
|     it("fulltext parser with content", () => { | /** | ||||||
|         const rootExp = parse({ |  * For an {@link AndExp}, it goes through all its subexpressions (up to fourth) and checks their type and returns them as a typecasted array. | ||||||
|             fulltextTokens: tokens(["hello", "hi"]), |  * Each subexpression can have their own type. | ||||||
|             expressionTokens: [], |  * | ||||||
|             searchContext: new SearchContext() |  * @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. | ||||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); |  * @param thirdType the type of the third subexpression. | ||||||
|         assertIsArchived(rootExp.subExpressions[0]); |  * @param fourthType the type of the fourth subexpression. | ||||||
|  |  * @returns an array of all the subexpressions (in order) typecasted to their expected type. | ||||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("OrExp"); |  */ | ||||||
|  | function expectSubexpressions<FirstT extends Expression, | ||||||
|         const subs = rootExp.subExpressions[2].subExpressions; |                               SecondT extends Expression, | ||||||
|  |                               ThirdT extends Expression, | ||||||
|         expect(subs[0].constructor.name).toEqual("NoteFlatTextExp"); |                               FourthT extends Expression>( | ||||||
|         expect(subs[0].tokens).toEqual(["hello", "hi"]); |                                 exp: AndExp, | ||||||
|  |                                 firstType: ClassType<FirstT>, | ||||||
|         expect(subs[1].constructor.name).toEqual("NoteContentFulltextExp"); |                                 secondType?: ClassType<SecondT>, | ||||||
|         expect(subs[1].tokens).toEqual(["hello", "hi"]); |                                 thirdType?: ClassType<ThirdT>, | ||||||
|     }); |                                 fourthType?: ClassType<FourthT>): [ FirstT, SecondT, ThirdT, FourthT ] | ||||||
|  | { | ||||||
|     it("simple label comparison", () => { |     expectExpression(exp.subExpressions[0], firstType); | ||||||
|         const rootExp = parse({ |     if (secondType) { | ||||||
|             fulltextTokens: [], |         expectExpression(exp.subExpressions[1], secondType); | ||||||
|             expressionTokens: tokens(["#mylabel", "=", "text"]), |     } | ||||||
|             searchContext: new SearchContext() |     if (thirdType) { | ||||||
|         }); |         expectExpression(exp.subExpressions[2], thirdType); | ||||||
|  |     } | ||||||
|         expect(rootExp.constructor.name).toEqual("AndExp"); |     if (fourthType) { | ||||||
|         assertIsArchived(rootExp.subExpressions[0]); |         expectExpression(exp.subExpressions[3], fourthType); | ||||||
|         expect(rootExp.subExpressions[2].constructor.name).toEqual("LabelComparisonExp"); |     } | ||||||
|         expect(rootExp.subExpressions[2].attributeType).toEqual("label"); |     return [ | ||||||
|         expect(rootExp.subExpressions[2].attributeName).toEqual("mylabel"); |         exp.subExpressions[0] as FirstT, | ||||||
|         expect(rootExp.subExpressions[2].comparator).toBeTruthy(); |         exp.subExpressions[1] as SecondT, | ||||||
|     }); |         exp.subExpressions[2] as ThirdT, | ||||||
|  |         exp.subExpressions[3] as FourthT | ||||||
|     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"); |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| 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 ""'); |  | ||||||
|     }); |  | ||||||
| }); |  | ||||||
|   | |||||||
| @@ -704,7 +704,7 @@ body.layout-horizontal .tab-row-widget-container { | |||||||
|     overflow: hidden; |     overflow: hidden; | ||||||
| } | } | ||||||
|  |  | ||||||
| body.desktop #root-widget.horizontal-layout { | body.desktop:not(.background-effects.platform-win32) #root-widget.horizontal-layout { | ||||||
|     background-color: var(--root-background) !important; |     background-color: var(--root-background) !important; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import Expression from "./expression.js"; | |||||||
| import TrueExp from "./true.js"; | import TrueExp from "./true.js"; | ||||||
|  |  | ||||||
| class AndExp extends Expression { | class AndExp extends Expression { | ||||||
|     private subExpressions: Expression[]; |     subExpressions: Expression[]; | ||||||
|  |  | ||||||
|     static of(_subExpressions: (Expression | null | undefined)[]) { |     static of(_subExpressions: (Expression | null | undefined)[]) { | ||||||
|         const subExpressions = _subExpressions.filter((exp) => !!exp) as Expression[]; |         const subExpressions = _subExpressions.filter((exp) => !!exp) as Expression[]; | ||||||
|   | |||||||
| @@ -7,8 +7,8 @@ import becca from "../../../becca/becca.js"; | |||||||
| import Expression from "./expression.js"; | import Expression from "./expression.js"; | ||||||
|  |  | ||||||
| class AttributeExistsExp extends Expression { | class AttributeExistsExp extends Expression { | ||||||
|     private attributeType: string; |     attributeType: string; | ||||||
|     private attributeName: string; |     attributeName: string; | ||||||
|     private isTemplateLabel: boolean; |     private isTemplateLabel: boolean; | ||||||
|     private prefixMatch: boolean; |     private prefixMatch: boolean; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| import NoteSet from "../note_set.js"; | import type NoteSet from "../note_set.js"; | ||||||
| import SearchContext from "../search_context.js"; | import type SearchContext from "../search_context.js"; | ||||||
|  |  | ||||||
| abstract class Expression { | export default abstract class Expression { | ||||||
|     name: string; |     name: string; | ||||||
|  |  | ||||||
|     constructor() { |     constructor() { | ||||||
| @@ -12,5 +12,3 @@ abstract class Expression { | |||||||
|  |  | ||||||
|     abstract execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext): NoteSet; |     abstract execute(inputNoteSet: NoteSet, executionContext: {}, searchContext: SearchContext): NoteSet; | ||||||
| } | } | ||||||
|  |  | ||||||
| export default Expression; |  | ||||||
|   | |||||||
| @@ -8,9 +8,9 @@ import SearchContext from "../search_context.js"; | |||||||
| type Comparator = (value: string) => boolean; | type Comparator = (value: string) => boolean; | ||||||
|  |  | ||||||
| class LabelComparisonExp extends Expression { | class LabelComparisonExp extends Expression { | ||||||
|     private attributeType: string; |     attributeType: string; | ||||||
|     private attributeName: string; |     attributeName: string; | ||||||
|     private comparator: Comparator; |     comparator: Comparator; | ||||||
|  |  | ||||||
|     constructor(attributeType: string, attributeName: string, comparator: Comparator) { |     constructor(attributeType: string, attributeName: string, comparator: Comparator) { | ||||||
|         super(); |         super(); | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ import SearchContext from "../search_context.js"; | |||||||
| import Expression from "./expression.js"; | import Expression from "./expression.js"; | ||||||
|  |  | ||||||
| class NotExp extends Expression { | class NotExp extends Expression { | ||||||
|     private subExpression: Expression; |     subExpression: Expression; | ||||||
|  |  | ||||||
|     constructor(subExpression: Expression) { |     constructor(subExpression: Expression) { | ||||||
|         super(); |         super(); | ||||||
|   | |||||||
| @@ -34,7 +34,7 @@ type SearchRow = Pick<NoteRow, "noteId" | "type" | "mime" | "content" | "isProte | |||||||
|  |  | ||||||
| class NoteContentFulltextExp extends Expression { | class NoteContentFulltextExp extends Expression { | ||||||
|     private operator: string; |     private operator: string; | ||||||
|     private tokens: string[]; |     tokens: string[]; | ||||||
|     private raw: boolean; |     private raw: boolean; | ||||||
|     private flatText: boolean; |     private flatText: boolean; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ import { normalize } from "../../utils.js"; | |||||||
| import beccaService from "../../../becca/becca_service.js"; | import beccaService from "../../../becca/becca_service.js"; | ||||||
|  |  | ||||||
| class NoteFlatTextExp extends Expression { | class NoteFlatTextExp extends Expression { | ||||||
|     private tokens: string[]; |     tokens: string[]; | ||||||
|  |  | ||||||
|     constructor(tokens: string[]) { |     constructor(tokens: string[]) { | ||||||
|         super(); |         super(); | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import TrueExp from "./true.js"; | |||||||
| import SearchContext from "../search_context.js"; | import SearchContext from "../search_context.js"; | ||||||
|  |  | ||||||
| class OrExp extends Expression { | class OrExp extends Expression { | ||||||
|     private subExpressions: Expression[]; |     subExpressions: Expression[]; | ||||||
|  |  | ||||||
|     static of(subExpressions: Expression[]) { |     static of(subExpressions: Expression[]) { | ||||||
|         subExpressions = subExpressions.filter((exp) => !!exp); |         subExpressions = subExpressions.filter((exp) => !!exp); | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ interface OrderDefinition { | |||||||
|  |  | ||||||
| class OrderByAndLimitExp extends Expression { | class OrderByAndLimitExp extends Expression { | ||||||
|     private orderDefinitions: OrderDefinition[]; |     private orderDefinitions: OrderDefinition[]; | ||||||
|     private limit: number; |     limit: number; | ||||||
|     subExpression: Expression | null; |     subExpression: Expression | null; | ||||||
|  |  | ||||||
|     constructor(orderDefinitions: Pick<OrderDefinition, "direction" | "valueExtractor">[], limit?: number) { |     constructor(orderDefinitions: Pick<OrderDefinition, "direction" | "valueExtractor">[], limit?: number) { | ||||||
|   | |||||||
| @@ -41,9 +41,9 @@ interface SearchContext { | |||||||
| } | } | ||||||
|  |  | ||||||
| class PropertyComparisonExp extends Expression { | class PropertyComparisonExp extends Expression { | ||||||
|     private propertyName: string; |     propertyName: string; | ||||||
|     private operator: string; |     operator: string; | ||||||
|     private comparedValue: string; |     comparedValue: string; | ||||||
|     private comparator; |     private comparator; | ||||||
|  |  | ||||||
|     static isProperty(name: string) { |     static isProperty(name: string) { | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ class SearchContext { | |||||||
|     originalQuery: string; |     originalQuery: string; | ||||||
|     fulltextQuery: string; |     fulltextQuery: string; | ||||||
|     dbLoadNeeded: boolean; |     dbLoadNeeded: boolean; | ||||||
|     private error: string | null; |     error: string | null; | ||||||
|  |  | ||||||
|     constructor(params: SearchParams = {}) { |     constructor(params: SearchParams = {}) { | ||||||
|         this.fastSearch = !!params.fastSearch; |         this.fastSearch = !!params.fastSearch; | ||||||
|   | |||||||
| @@ -423,7 +423,14 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level | |||||||
|     return getAggregateExpression(); |     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; |     let expression: Expression | undefined | null; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
| @@ -441,6 +448,12 @@ function parse({ fulltextTokens, expressionTokens, searchContext }: { fulltextTo | |||||||
|         expression |         expression | ||||||
|     ]); |     ]); | ||||||
|  |  | ||||||
|  |     if (searchContext.limit && !searchContext.orderBy) { | ||||||
|  |         const filterExp = exp; | ||||||
|  |         exp = new OrderByAndLimitExp([], searchContext.limit || undefined ); | ||||||
|  |         (exp as any).subExpression = filterExp; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (searchContext.orderBy && searchContext.orderBy !== "relevancy") { |     if (searchContext.orderBy && searchContext.orderBy !== "relevancy") { | ||||||
|         const filterExp = exp; |         const filterExp = exp; | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user