From 15169289f00693d48eb903d979420123797a192e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sun, 18 Feb 2024 02:27:04 +0200 Subject: [PATCH] server-ts: Port services/search/services/parse --- spec/search/parser.spec.js | 2 +- src/services/search/expressions/ancestor.ts | 8 +- src/services/search/expressions/and.ts | 4 +- .../search/expressions/attribute_exists.ts | 4 +- .../expressions/note_content_fulltext.ts | 4 +- .../search/expressions/order_by_and_limit.ts | 8 +- src/services/search/search_context.ts | 4 +- src/services/search/services/handle_parens.ts | 8 +- src/services/search/services/lex.ts | 7 +- .../search/services/{parse.js => parse.ts} | 138 +++++++++++------- src/services/search/services/search.js | 2 +- src/services/search/services/types.ts | 6 + src/services/search/value_extractor.ts | 2 +- 13 files changed, 114 insertions(+), 83 deletions(-) rename src/services/search/services/{parse.js => parse.ts} (73%) create mode 100644 src/services/search/services/types.ts diff --git a/spec/search/parser.spec.js b/spec/search/parser.spec.js index fa573c549..be2e2bcaf 100644 --- a/spec/search/parser.spec.js +++ b/spec/search/parser.spec.js @@ -1,5 +1,5 @@ const SearchContext = require('../../src/services/search/search_context'); -const parse = require('../../src/services/search/services/parse.js'); +const parse = require('../../src/services/search/services/parse'); function tokens(toks, cur = 0) { return toks.map(arg => { diff --git a/src/services/search/expressions/ancestor.ts b/src/services/search/expressions/ancestor.ts index 6f169e33a..4e7380795 100644 --- a/src/services/search/expressions/ancestor.ts +++ b/src/services/search/expressions/ancestor.ts @@ -11,9 +11,9 @@ class AncestorExp extends Expression { private ancestorNoteId: string; private ancestorDepthComparator; - ancestorDepth: string; + ancestorDepth?: string; - constructor(ancestorNoteId: string, ancestorDepth: string) { + constructor(ancestorNoteId: string, ancestorDepth?: string) { super(); this.ancestorNoteId = ancestorNoteId; @@ -51,7 +51,7 @@ class AncestorExp extends Expression { return depthConformingNoteSet; } - getComparator(depthCondition: string): ((depth: number) => boolean) | null { + getComparator(depthCondition?: string): ((depth: number) => boolean) | null { if (!depthCondition) { return null; } @@ -74,4 +74,4 @@ class AncestorExp extends Expression { } } -module.exports = AncestorExp; +export = AncestorExp; diff --git a/src/services/search/expressions/and.ts b/src/services/search/expressions/and.ts index 4457dfc21..82c73fe70 100644 --- a/src/services/search/expressions/and.ts +++ b/src/services/search/expressions/and.ts @@ -8,8 +8,8 @@ import TrueExp = require('./true'); class AndExp extends Expression { private subExpressions: Expression[]; - static of(subExpressions: Expression[]) { - subExpressions = subExpressions.filter(exp => !!exp); + static of(_subExpressions: (Expression | null | undefined)[]) { + const subExpressions = _subExpressions.filter((exp) => !!exp) as Expression[]; if (subExpressions.length === 1) { return subExpressions[0]; diff --git a/src/services/search/expressions/attribute_exists.ts b/src/services/search/expressions/attribute_exists.ts index 7a17897bf..b1f01a1c0 100644 --- a/src/services/search/expressions/attribute_exists.ts +++ b/src/services/search/expressions/attribute_exists.ts @@ -11,9 +11,9 @@ class AttributeExistsExp extends Expression { private attributeType: string; private attributeName: string; private isTemplateLabel: boolean; - private prefixMatch: string; + private prefixMatch: boolean; - constructor(attributeType: string, attributeName: string, prefixMatch: string) { + constructor(attributeType: string, attributeName: string, prefixMatch: boolean) { super(); this.attributeType = attributeType; diff --git a/src/services/search/expressions/note_content_fulltext.ts b/src/services/search/expressions/note_content_fulltext.ts index ab80e21eb..c063c2692 100644 --- a/src/services/search/expressions/note_content_fulltext.ts +++ b/src/services/search/expressions/note_content_fulltext.ts @@ -26,8 +26,8 @@ function getRegex(str: string): RegExp { interface ConstructorOpts { tokens: string[]; - raw: boolean; - flatText: boolean; + raw?: boolean; + flatText?: boolean; } type SearchRow = Pick; diff --git a/src/services/search/expressions/order_by_and_limit.ts b/src/services/search/expressions/order_by_and_limit.ts index cc7512303..8f1f0826a 100644 --- a/src/services/search/expressions/order_by_and_limit.ts +++ b/src/services/search/expressions/order_by_and_limit.ts @@ -5,14 +5,12 @@ import NoteSet = require("../note_set"); import SearchContext = require("../search_context"); import Expression = require("./expression"); -type Direction = "asc"; - interface ValueExtractor { extract: (note: BNote) => number | string | null; } interface OrderDefinition { - direction: Direction; + direction?: string; smaller: number; larger: number; valueExtractor: ValueExtractor; @@ -22,9 +20,9 @@ class OrderByAndLimitExp extends Expression { private orderDefinitions: OrderDefinition[]; private limit: number; - private subExpression: Expression | null; + subExpression: Expression | null; - constructor(orderDefinitions: Pick[], limit: number) { + constructor(orderDefinitions: Pick[], limit?: number) { super(); this.orderDefinitions = orderDefinitions as unknown as OrderDefinition[]; diff --git a/src/services/search/search_context.ts b/src/services/search/search_context.ts index 93ae006b4..d9eb5945e 100644 --- a/src/services/search/search_context.ts +++ b/src/services/search/search_context.ts @@ -8,7 +8,7 @@ interface SearchParams { includeHiddenNotes?: boolean; ignoreHoistedNote?: boolean; ancestorNoteId?: string; - ancestorDepth?: number; + ancestorDepth?: string; orderBy?: string; orderDirection?: string; limit?: number; @@ -23,7 +23,7 @@ class SearchContext { includeHiddenNotes: boolean; ignoreHoistedNote: boolean; ancestorNoteId?: string; - ancestorDepth?: number; + ancestorDepth?: string; orderBy?: string; orderDirection?: string; limit?: number; diff --git a/src/services/search/services/handle_parens.ts b/src/services/search/services/handle_parens.ts index 3be25bf46..30d7e03fc 100644 --- a/src/services/search/services/handle_parens.ts +++ b/src/services/search/services/handle_parens.ts @@ -1,11 +1,9 @@ -interface Token { - token: string; -} +import { TokenData } from "./types"; /** * This will create a recursive object from a list of tokens - tokens between parenthesis are grouped in a single array */ -function handleParens(tokens: (Token | Token[])[]) { +function handleParens(tokens: (TokenData | TokenData[])[]) { if (tokens.length === 0) { return []; } @@ -45,7 +43,7 @@ function handleParens(tokens: (Token | Token[])[]) { ...tokens.slice(0, leftIdx), handleParens(tokens.slice(leftIdx + 1, rightIdx)), ...tokens.slice(rightIdx + 1) - ] as (Token | Token[])[]; + ] as (TokenData | TokenData[])[]; } } diff --git a/src/services/search/services/lex.ts b/src/services/search/services/lex.ts index 046fe6870..516cf71df 100644 --- a/src/services/search/services/lex.ts +++ b/src/services/search/services/lex.ts @@ -1,9 +1,4 @@ -interface TokenData { - token: string; - inQuotes: boolean; - startIndex: number; - endIndex: number; -} +import { TokenData } from "./types"; function lex(str: string) { str = str.toLowerCase(); diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.ts similarity index 73% rename from src/services/search/services/parse.js rename to src/services/search/services/parse.ts index 568a00227..2e24ed8ac 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.ts @@ -1,28 +1,31 @@ "use strict"; -const dayjs = require("dayjs"); -const AndExp = require('../expressions/and'); -const OrExp = require('../expressions/or'); -const NotExp = require('../expressions/not'); -const ChildOfExp = require('../expressions/child_of'); -const DescendantOfExp = require('../expressions/descendant_of'); -const ParentOfExp = require('../expressions/parent_of'); -const RelationWhereExp = require('../expressions/relation_where'); -const PropertyComparisonExp = require('../expressions/property_comparison'); -const AttributeExistsExp = require('../expressions/attribute_exists'); -const LabelComparisonExp = require('../expressions/label_comparison'); -const NoteFlatTextExp = require('../expressions/note_flat_text'); -const NoteContentFulltextExp = require('../expressions/note_content_fulltext'); -const OrderByAndLimitExp = require('../expressions/order_by_and_limit'); -const AncestorExp = require('../expressions/ancestor'); -const buildComparator = require('./build_comparator'); -const ValueExtractor = require('../value_extractor'); -const utils = require('../../utils'); -const TrueExp = require('../expressions/true'); -const IsHiddenExp = require('../expressions/is_hidden'); +import dayjs = require("dayjs"); +import AndExp = require('../expressions/and'); +import OrExp = require('../expressions/or'); +import NotExp = require('../expressions/not'); +import ChildOfExp = require('../expressions/child_of'); +import DescendantOfExp = require('../expressions/descendant_of'); +import ParentOfExp = require('../expressions/parent_of'); +import RelationWhereExp = require('../expressions/relation_where'); +import PropertyComparisonExp = require('../expressions/property_comparison'); +import AttributeExistsExp = require('../expressions/attribute_exists'); +import LabelComparisonExp = require('../expressions/label_comparison'); +import NoteFlatTextExp = require('../expressions/note_flat_text'); +import NoteContentFulltextExp = require('../expressions/note_content_fulltext'); +import OrderByAndLimitExp = require('../expressions/order_by_and_limit'); +import AncestorExp = require('../expressions/ancestor'); +import buildComparator = require('./build_comparator'); +import ValueExtractor = require('../value_extractor'); +import utils = require('../../utils'); +import TrueExp = require('../expressions/true'); +import IsHiddenExp = require('../expressions/is_hidden'); +import SearchContext = require("../search_context"); +import { TokenData } from "./types"; +import Expression = require("../expressions/expression"); -function getFulltext(tokens, searchContext) { - tokens = tokens.map(t => utils.removeDiacritic(t.token)); +function getFulltext(_tokens: TokenData[], searchContext: SearchContext) { + const tokens: string[] = _tokens.map(t => utils.removeDiacritic(t.token)); searchContext.highlightedTokens.push(...tokens); @@ -54,7 +57,7 @@ const OPERATORS = [ "%=" ]; -function isOperator(token) { +function isOperator(token: TokenData) { if (Array.isArray(token)) { return false; } @@ -62,20 +65,20 @@ function isOperator(token) { return OPERATORS.includes(token.token); } -function getExpression(tokens, searchContext, level = 0) { +function getExpression(tokens: TokenData[], searchContext: SearchContext, level = 0) { if (tokens.length === 0) { return null; } - const expressions = []; - let op = null; + const expressions: Expression[] = []; + let op: string | null = null; - let i; + let i: number; - function context(i) { + function context(i: number) { let {startIndex, endIndex} = tokens[i]; - startIndex = Math.max(0, startIndex - 20); - endIndex = Math.min(searchContext.originalQuery.length, endIndex + 20); + startIndex = Math.max(0, (startIndex || 0) - 20); + endIndex = Math.min(searchContext.originalQuery.length, (endIndex || Number.MAX_SAFE_INTEGER) + 20); return `"${startIndex !== 0 ? "..." : ""}${searchContext.originalQuery.substr(startIndex, endIndex - startIndex)}${endIndex !== searchContext.originalQuery.length ? "..." : ""}"`; } @@ -133,7 +136,7 @@ function getExpression(tokens, searchContext, level = 0) { return date.format(format); } - function parseNoteProperty() { + function parseNoteProperty(): Expression | undefined | null { if (tokens[i].token !== '.') { searchContext.addError('Expected "." to separate field path'); return; @@ -161,19 +164,25 @@ function getExpression(tokens, searchContext, level = 0) { if (tokens[i].token === 'parents') { i += 1; - return new ChildOfExp(parseNoteProperty()); + const expression = parseNoteProperty(); + if (!expression) { return; } + return new ChildOfExp(expression); } if (tokens[i].token === 'children') { i += 1; - return new ParentOfExp(parseNoteProperty()); + const expression = parseNoteProperty(); + if (!expression) { return; } + return new ParentOfExp(expression); } if (tokens[i].token === 'ancestors') { i += 1; - return new DescendantOfExp(parseNoteProperty()); + const expression = parseNoteProperty(); + if (!expression) { return; } + return new DescendantOfExp(expression); } if (tokens[i].token === 'labels') { @@ -219,6 +228,10 @@ function getExpression(tokens, searchContext, level = 0) { i += 2; const comparedValue = resolveConstantOperand(); + if (!comparedValue) { + searchContext.addError(`Unresolved constant operand.`); + return; + } return new PropertyComparisonExp(searchContext, propertyName, operator, comparedValue); } @@ -226,7 +239,7 @@ function getExpression(tokens, searchContext, level = 0) { searchContext.addError(`Unrecognized note property "${tokens[i].token}" in ${context(i)}`); } - function parseAttribute(name) { + function parseAttribute(name: string) { const isLabel = name.startsWith('#'); name = name.substr(1); @@ -239,10 +252,10 @@ function getExpression(tokens, searchContext, level = 0) { const subExp = isLabel ? parseLabel(name) : parseRelation(name); - return isNegated ? new NotExp(subExp) : subExp; + return subExp && isNegated ? new NotExp(subExp) : subExp; } - function parseLabel(labelName) { + function parseLabel(labelName: string) { searchContext.highlightedTokens.push(labelName); if (i < tokens.length - 2 && isOperator(tokens[i + 1])) { @@ -274,13 +287,15 @@ function getExpression(tokens, searchContext, level = 0) { } } - function parseRelation(relationName) { + function parseRelation(relationName: string) { searchContext.highlightedTokens.push(relationName); if (i < tokens.length - 2 && tokens[i + 1].token === '.') { i += 1; - return new RelationWhereExp(relationName, parseNoteProperty()); + const expression = parseNoteProperty(); + if (!expression) { return; } + return new RelationWhereExp(relationName, expression); } else if (i < tokens.length - 2 && isOperator(tokens[i + 1])) { searchContext.addError(`Relation can be compared only with property, e.g. ~relation.title=hello in ${context(i)}`); @@ -293,7 +308,10 @@ function getExpression(tokens, searchContext, level = 0) { } function parseOrderByAndLimit() { - const orderDefinitions = []; + const orderDefinitions: { + valueExtractor: ValueExtractor, + direction: string + }[] = []; let limit; if (tokens[i].token === 'orderby') { @@ -316,8 +334,9 @@ function getExpression(tokens, searchContext, level = 0) { const valueExtractor = new ValueExtractor(searchContext, propertyPath); - if (valueExtractor.validate()) { - searchContext.addError(valueExtractor.validate()); + const validationError = valueExtractor.validate(); + if (validationError) { + searchContext.addError(validationError); } orderDefinitions.push({ @@ -348,7 +367,10 @@ function getExpression(tokens, searchContext, level = 0) { for (i = 0; i < tokens.length; i++) { if (Array.isArray(tokens[i])) { - expressions.push(getExpression(tokens[i], searchContext, level++)); + const expression = getExpression(tokens[i] as unknown as TokenData[], searchContext, level++); + if (expression) { + expressions.push(expression); + } continue; } @@ -359,7 +381,10 @@ function getExpression(tokens, searchContext, level = 0) { } if (token.startsWith('#') || token.startsWith('~')) { - expressions.push(parseAttribute(token)); + const attribute = parseAttribute(token); + if (attribute) { + expressions.push(attribute); + } } else if (['orderby', 'limit'].includes(token)) { if (level !== 0) { @@ -384,12 +409,17 @@ function getExpression(tokens, searchContext, level = 0) { continue; } - expressions.push(new NotExp(getExpression(tokens[i], searchContext, level++))); + const tokenArray = tokens[i] as unknown as TokenData[]; + const expression = getExpression(tokenArray, searchContext, level++); + if (!expression) { return; } + expressions.push(new NotExp(expression)); } else if (token === 'note') { i++; - expressions.push(parseNoteProperty()); + const expression = parseNoteProperty(); + if (!expression) { return; } + expressions.push(expression); continue; } @@ -416,13 +446,17 @@ function getExpression(tokens, searchContext, level = 0) { return getAggregateExpression(); } -function parse({fulltextTokens, expressionTokens, searchContext}) { - let expression; +function parse({fulltextTokens, expressionTokens, searchContext}: { + fulltextTokens: TokenData[], + expressionTokens: TokenData[], + searchContext: SearchContext +}) { + let expression: Expression | undefined | null; try { expression = getExpression(expressionTokens, searchContext); } - catch (e) { + catch (e: any) { searchContext.addError(e.message); expression = new TrueExp(); @@ -443,13 +477,13 @@ function parse({fulltextTokens, expressionTokens, searchContext}) { direction: searchContext.orderDirection }], searchContext.limit); - exp.subExpression = filterExp; + (exp as any).subExpression = filterExp; } return exp; } -function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}) { +function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}: SearchContext) { if (ancestorNoteId && ancestorNoteId !== 'root') { return new AncestorExp(ancestorNoteId, ancestorDepth); } else if (!includeHiddenNotes) { @@ -459,4 +493,4 @@ function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}) { } } -module.exports = parse; +export = parse; diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js index 0ca74eb26..67b7d4f1f 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.js @@ -3,7 +3,7 @@ const normalizeString = require("normalize-strings"); const lex = require('./lex'); const handleParens = require('./handle_parens'); -const parse = require('./parse.js'); +const parse = require('./parse'); const SearchResult = require('../search_result'); const SearchContext = require('../search_context'); const becca = require('../../../becca/becca'); diff --git a/src/services/search/services/types.ts b/src/services/search/services/types.ts new file mode 100644 index 000000000..c383bdc2d --- /dev/null +++ b/src/services/search/services/types.ts @@ -0,0 +1,6 @@ +export interface TokenData { + token: string; + inQuotes?: boolean; + startIndex?: number; + endIndex?: number; +} \ No newline at end of file diff --git a/src/services/search/value_extractor.ts b/src/services/search/value_extractor.ts index c602e5f49..a32eb5f6d 100644 --- a/src/services/search/value_extractor.ts +++ b/src/services/search/value_extractor.ts @@ -134,4 +134,4 @@ class ValueExtractor { } } -module.exports = ValueExtractor; +export = ValueExtractor;