mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
320 lines
12 KiB
TypeScript
320 lines
12 KiB
TypeScript
// @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 Expression from "../../src/services/search/expressions/expression.js";
|
|
import SearchContext from "../../src/services/search/search_context.js";
|
|
import parse from "../../src/services/search/services/parse.js";
|
|
|
|
function tokens(toks: Array<string>, cur = 0): Array<any> {
|
|
return toks.map((arg) => {
|
|
if (Array.isArray(arg)) {
|
|
return tokens(arg, cur);
|
|
} else {
|
|
cur += arg.length;
|
|
|
|
return {
|
|
token: arg,
|
|
inQuotes: false,
|
|
startIndex: cur - arg.length,
|
|
endIndex: cur - 1,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
function assertIsArchived(exp: Expression) {
|
|
expect(exp.constructor.name).toEqual('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');
|
|
});
|
|
});
|
|
|
|
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 ""');
|
|
});
|
|
});
|