implemented property based access + parent

This commit is contained in:
zadam 2020-05-23 10:25:22 +02:00
parent 714881ad99
commit 4ea934509e
11 changed files with 229 additions and 38 deletions

View File

@ -18,8 +18,8 @@ describe("Lexer fulltext", () => {
});
it("you can use different quotes and other special characters inside quotes", () => {
expect(lexer("'i can use \" or ` or #@=*' without problem").fulltextTokens)
.toEqual(["i can use \" or ` or #@=*", "without", "problem"]);
expect(lexer("'i can use \" or ` or #~=*' without problem").fulltextTokens)
.toEqual(["i can use \" or ` or #~=*", "without", "problem"]);
});
it("if quote is not ended then it's just one long token", () => {
@ -33,15 +33,15 @@ describe("Lexer fulltext", () => {
});
it("escaping special characters", () => {
expect(lexer("hello \\#\\@\\'").fulltextTokens)
.toEqual(["hello", "#@'"]);
expect(lexer("hello \\#\\~\\'").fulltextTokens)
.toEqual(["hello", "#~'"]);
});
});
describe("Lexer expression", () => {
it("simple attribute existence", () => {
expect(lexer("#label @relation").expressionTokens)
.toEqual(["#label", "@relation"]);
expect(lexer("#label ~relation").expressionTokens)
.toEqual(["#label", "~relation"]);
});
it("simple label operators", () => {
@ -50,12 +50,17 @@ describe("Lexer expression", () => {
});
it("spaces in attribute names and values", () => {
expect(lexer(`#'long label'="hello o' world" @'long relation'`).expressionTokens)
.toEqual(["#long label", "=", "hello o' world", "@long relation"]);
expect(lexer(`#'long label'="hello o' world" ~'long relation'`).expressionTokens)
.toEqual(["#long label", "=", "hello o' world", "~long relation"]);
});
it("complex expressions with and, or and parenthesis", () => {
expect(lexer(`# (#label=text OR #second=text) AND @relation`).expressionTokens)
.toEqual(["#", "(", "#label", "=", "text", "or", "#second", "=", "text", ")", "and", "@relation"]);
expect(lexer(`# (#label=text OR #second=text) AND ~relation`).expressionTokens)
.toEqual(["#", "(", "#label", "=", "text", "or", "#second", "=", "text", ")", "and", "~relation"]);
});
it("dot separated properties", () => {
expect(lexer(`# ~author.title = 'Hugh Howey' AND note.title = 'Silo'`).expressionTokens)
.toEqual(["#", "~author", ".", "title", "=", "hugh howey", "and", "note", ".", "title", "=", "silo"]);
});
});

View File

@ -37,7 +37,7 @@ describe("Parser", () => {
parsingContext: new ParsingContext()
});
expect(rootExp.constructor.name).toEqual("FieldComparisonExp");
expect(rootExp.constructor.name).toEqual("LabelComparisonExp");
expect(rootExp.attributeType).toEqual("label");
expect(rootExp.attributeName).toEqual("mylabel");
expect(rootExp.comparator).toBeTruthy();
@ -53,10 +53,10 @@ describe("Parser", () => {
expect(rootExp.constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first");
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSub.attributeName).toEqual("second");
});
@ -70,27 +70,27 @@ describe("Parser", () => {
expect(rootExp.constructor.name).toEqual("AndExp");
const [firstSub, secondSub] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first");
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSub.attributeName).toEqual("second");
});
it("simple label OR", () => {
const rootExp = parser({
fulltextTokens: [],
expressionTokens: ["#first", "=", "text", "OR", "#second", "=", "text"],
expressionTokens: ["#first", "=", "text", "or", "#second", "=", "text"],
parsingContext: new ParsingContext()
});
expect(rootExp.constructor.name).toEqual("OrExp");
const [firstSub, secondSub] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
expect(firstSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSub.attributeName).toEqual("first");
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSub.attributeName).toEqual("second");
});
@ -107,30 +107,30 @@ describe("Parser", () => {
expect(firstSub.constructor.name).toEqual("NoteCacheFulltextExp");
expect(firstSub.tokens).toEqual(["hello"]);
expect(secondSub.constructor.name).toEqual("FieldComparisonExp");
expect(secondSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSub.attributeName).toEqual("mylabel");
});
it("label sub-expression", () => {
const rootExp = parser({
fulltextTokens: [],
expressionTokens: ["#first", "=", "text", "OR", ["#second", "=", "text", "AND", "#third", "=", "text"]],
expressionTokens: ["#first", "=", "text", "or", ["#second", "=", "text", "and", "#third", "=", "text"]],
parsingContext: new ParsingContext()
});
expect(rootExp.constructor.name).toEqual("OrExp");
const [firstSub, secondSub] = rootExp.subExpressions;
expect(firstSub.constructor.name).toEqual("FieldComparisonExp");
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("FieldComparisonExp");
expect(firstSubSub.constructor.name).toEqual("LabelComparisonExp");
expect(firstSubSub.attributeName).toEqual("second");
expect(secondSubSub.constructor.name).toEqual("FieldComparisonExp");
expect(secondSubSub.constructor.name).toEqual("LabelComparisonExp");
expect(secondSubSub.attributeName).toEqual("third");
});
});

View File

@ -83,6 +83,31 @@ describe("Search", () => {
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("numeric label comparison fallback to string comparison", async () => {
rootNote.child(
note("Europe")
.label('country', '', true)
.child(
note("Austria")
.label('established', '1955-07-27')
)
.child(
note("Czech Republic")
.label('established', '1993-01-01')
)
.child(
note("Hungary")
.label('established', '..wrong..')
)
);
const parsingContext = new ParsingContext();
const searchResults = await searchService.findNotesWithQuery('#established < 1990', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("logical or", async () => {
rootNote.child(
note("Europe")
@ -140,6 +165,51 @@ describe("Search", () => {
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
});
it("filter by note property", async () => {
rootNote.child(
note("Europe")
.child(
note("Austria")
)
.child(
note("Czech Republic")
)
);
const parsingContext = new ParsingContext();
const searchResults = await searchService.findNotesWithQuery('# note.title =* czech', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
});
it("filter by note's parent", async () => {
rootNote.child(
note("Europe")
.child(
note("Austria")
)
.child(
note("Czech Republic")
)
)
.child(
note("Asia")
.child(note('Taiwan'))
);
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('# note.parent.title = Europe', parsingContext);
expect(searchResults.length).toEqual(2);
expect(findNoteByTitle(searchResults, "Austria")).toBeTruthy();
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = await searchService.findNotesWithQuery('# note.parent.title = Asia', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Taiwan")).toBeTruthy();
});
});
/** @return {Note} */

View File

@ -2,7 +2,7 @@
const Expression = require('./expression');
class AndExp extends Expression{
class AndExp extends Expression {
static of(subExpressions) {
subExpressions = subExpressions.filter(exp => !!exp);

View File

@ -0,0 +1,36 @@
"use strict";
const Expression = require('./expression');
const NoteSet = require('../note_set');
class ChildOfExp extends Expression {
constructor(subExpression) {
super();
this.subExpression = subExpression;
}
execute(inputNoteSet, searchContext) {
const subInputNoteSet = new NoteSet();
for (const note of inputNoteSet.notes) {
subInputNoteSet.addAll(note.parents);
}
const subResNoteSet = this.subExpression.execute(subInputNoteSet, searchContext);
const resNoteSet = new NoteSet();
for (const parentNote of subResNoteSet.notes) {
for (const childNote of parentNote.children) {
if (inputNoteSet.hasNote(childNote)) {
resNoteSet.add(childNote);
}
}
}
return resNoteSet;
}
}
module.exports = ChildOfExp;

View File

@ -4,6 +4,7 @@ class Expression {
/**
* @param {NoteSet} noteSet
* @param {object} searchContext
* @return {NoteSet}
*/
execute(noteSet, searchContext) {}
}

View File

@ -4,7 +4,7 @@ const Expression = require('./expression');
const NoteSet = require('../note_set');
const noteCache = require('../../note_cache/note_cache');
class FieldComparisonExp extends Expression {
class LabelComparisonExp extends Expression {
constructor(attributeType, attributeName, comparator) {
super();
@ -37,4 +37,4 @@ class FieldComparisonExp extends Expression {
}
}
module.exports = FieldComparisonExp;
module.exports = LabelComparisonExp;

View File

@ -0,0 +1,29 @@
"use strict";
const Expression = require('./expression');
const NoteSet = require('../note_set');
class PropertyComparisonExp extends Expression {
constructor(propertyName, comparator) {
super();
this.propertyName = propertyName;
this.comparator = comparator;
}
execute(noteSet, searchContext) {
const resNoteSet = new NoteSet();
for (const note of noteSet.notes) {
const value = note[this.propertyName].toLowerCase();
if (this.comparator(value)) {
resNoteSet.add(note);
}
}
return resNoteSet;
}
}
module.exports = PropertyComparisonExp;

View File

@ -77,7 +77,7 @@ function lexer(str) {
continue;
}
else if (!quotes) {
if (currentWord.length === 0 && (chr === '#' || chr === '@')) {
if (currentWord.length === 0 && (chr === '#' || chr === '~')) {
fulltextEnded = true;
currentWord = chr;
@ -87,7 +87,7 @@ function lexer(str) {
finishWord();
continue;
}
else if (fulltextEnded && ['(', ')'].includes(chr)) {
else if (fulltextEnded && ['(', ')', '.'].includes(chr)) {
finishWord();
currentWord += chr;
finishWord();

View File

@ -6,11 +6,19 @@ class NoteSet {
}
add(note) {
this.notes.push(note);
if (!this.hasNote(note)) {
this.notes.push(note);
}
}
addAll(notes) {
this.notes.push(...notes);
for (const note of notes) {
this.add(note);
}
}
hasNote(note) {
return this.hasNoteId(note.noteId);
}
hasNoteId(noteId) {

View File

@ -3,8 +3,10 @@
const AndExp = require('./expressions/and');
const OrExp = require('./expressions/or');
const NotExp = require('./expressions/not');
const ChildOfExp = require('./expressions/child_of');
const PropertyComparisonExp = require('./expressions/property_comparison');
const AttributeExistsExp = require('./expressions/attribute_exists');
const FieldComparisonExp = require('./expressions/field_comparison');
const LabelComparisonExp = require('./expressions/label_comparison');
const NoteCacheFulltextExp = require('./expressions/note_cache_fulltext');
const NoteContentFulltextExp = require('./expressions/note_content_fulltext');
const comparatorBuilder = require('./comparator_builder');
@ -38,17 +40,50 @@ function getExpression(tokens, parsingContext) {
const expressions = [];
let op = null;
for (let i = 0; i < tokens.length; i++) {
let i;
function parseNoteProperty() {
if (tokens[i] !== '.') {
parsingContext.addError('Expected "." to separate field path');
return;
}
i++;
if (tokens[i] === 'parent') {
i += 1;
return new ChildOfExp(parseNoteProperty());
}
if (tokens[i] === 'title') {
const propertyName = tokens[i];
const operator = tokens[i + 1];
const comparedValue = tokens[i + 2];
const comparator = comparatorBuilder(operator, comparedValue);
if (!comparator) {
parsingContext.addError(`Can't find operator '${operator}'`);
return;
}
i += 3;
return new PropertyComparisonExp(propertyName, comparator);
}
}
for (i = 0; i < tokens.length; i++) {
const token = tokens[i];
if (token === '#' || token === '@') {
if (token === '#' || token === '~') {
continue;
}
if (Array.isArray(token)) {
expressions.push(getExpression(token, parsingContext));
}
else if (token.startsWith('#') || token.startsWith('@')) {
else if (token.startsWith('#') || token.startsWith('~')) {
const type = token.startsWith('#') ? 'label' : 'relation';
parsingContext.highlightedTokens.push(token.substr(1));
@ -70,7 +105,7 @@ function getExpression(tokens, parsingContext) {
continue;
}
expressions.push(new FieldComparisonExp(type, token.substr(1), comparator));
expressions.push(new LabelComparisonExp(type, token.substr(1), comparator));
i += 2;
}
@ -78,11 +113,18 @@ function getExpression(tokens, parsingContext) {
expressions.push(new AttributeExistsExp(type, token.substr(1), parsingContext.fuzzyAttributeSearch));
}
}
else if (['and', 'or'].includes(token.toLowerCase())) {
else if (token === 'note') {
i++;
expressions.push(parseNoteProperty(tokens));
continue;
}
else if (['and', 'or'].includes(token)) {
if (!op) {
op = token.toLowerCase();
op = token;
}
else if (op !== token.toLowerCase()) {
else if (op !== token) {
parsingContext.addError('Mixed usage of AND/OR - always use parenthesis to group AND/OR expressions.');
}
}