From 9ede77aead40db516480ae2c0145521a1178ca27 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 23 May 2020 23:44:55 +0200 Subject: [PATCH] added ancestor --- spec/search.spec.js | 68 ++++++++++++++++++- src/services/note_cache/entities/note.js | 27 ++++++++ .../search/expressions/descendant_of.js | 30 ++++++++ src/services/search/note_set.js | 1 + src/services/search/parser.js | 7 ++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/services/search/expressions/descendant_of.js diff --git a/spec/search.spec.js b/spec/search.spec.js index b194c53d7..25a812216 100644 --- a/spec/search.spec.js +++ b/spec/search.spec.js @@ -3,6 +3,7 @@ const Note = require('../src/services/note_cache/entities/note'); const Branch = require('../src/services/note_cache/entities/branch'); const Attribute = require('../src/services/note_cache/entities/attribute'); const ParsingContext = require('../src/services/search/parsing_context'); +const dateUtils = require('../src/services/date_utils'); const noteCache = require('../src/services/note_cache/note_cache'); const randtoken = require('rand-token').generator({source: 'crypto'}); @@ -131,6 +132,48 @@ describe("Search", () => { expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy(); }); + it("smart date comparisons", async () => { + // dates should not be coerced into numbers which would then give wrong numbers + + rootNote + .child(note("My note") + .label('year', new Date().getFullYear().toString()) + .label('month', dateUtils.localNowDate().substr(0, 7)) + .label('date', dateUtils.localNowDate()) + .label('dateTime', dateUtils.localNowDateTime()) + ); + + const parsingContext = new ParsingContext(); + + async function test(query, expectedResultCount) { + const searchResults = await searchService.findNotesWithQuery(query, parsingContext); + expect(searchResults.length).toEqual(expectedResultCount); + + if (expectedResultCount === 1) { + expect(findNoteByTitle(searchResults, "My note")).toBeTruthy(); + } + } + + await test("#year = YEAR", 1); + await test("#year >= YEAR", 1); + await test("#year <= YEAR", 1); + await test("#year < YEAR+1", 1); + await test("#year > YEAR+1", 0); + + await test("#month = MONTH", 1); + + await test("#date = TODAY", 1); + await test("#date > TODAY", 0); + await test("#date > TODAY-1", 1); + await test("#date < TODAY+1", 1); + await test("#date < 'TODAY + 1'", 1); + + await test("#dateTime <= NOW+10", 1); + await test("#dateTime < NOW-10", 0); + await test("#dateTime >= NOW-10", 1); + await test("#dateTime < NOW-10", 0); + }); + it("logical or", async () => { rootNote .child(note("Europe") @@ -217,6 +260,29 @@ describe("Search", () => { expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy(); }); + it("filter by note's ancestor", async () => { + rootNote + .child(note("Europe") + .child(note("Austria")) + .child(note("Czech Republic") + .child(note("Prague").label('city'))) + ) + .child(note("Asia") + .child(note('Taiwan') + .child(note('Taipei').label('city'))) + ); + + const parsingContext = new ParsingContext(); + + let searchResults = await searchService.findNotesWithQuery('#city AND note.ancestors.title = Europe', parsingContext); + expect(searchResults.length).toEqual(1); + expect(findNoteByTitle(searchResults, "Prague")).toBeTruthy(); + + searchResults = await searchService.findNotesWithQuery('#city AND note.ancestors.title = Asia', parsingContext); + expect(searchResults.length).toEqual(1); + expect(findNoteByTitle(searchResults, "Taipei")).toBeTruthy(); + }); + it("filter by note's child", async () => { rootNote .child(note("Europe") @@ -411,7 +477,7 @@ class NoteBuilder { this.note = note; } - label(name, value, isInheritable = false) { + label(name, value = '', isInheritable = false) { new Attribute(noteCache, { attributeId: id(), noteId: this.note.noteId, diff --git a/src/services/note_cache/entities/note.js b/src/services/note_cache/entities/note.js index b5a47c576..ec869d0d1 100644 --- a/src/services/note_cache/entities/note.js +++ b/src/services/note_cache/entities/note.js @@ -53,6 +53,9 @@ class Note { if (protectedSessionService.isProtectedSessionAvailable()) { this.decrypt(); } + + /** @param {Note[]|null} */ + this.ancestorCache = null; } /** @return {Attribute[]} */ @@ -164,6 +167,7 @@ class Note { this.attributeCache = null; this.inheritableAttributeCache = null; + this.ancestorCache = null; } invalidateSubtreeCaches() { @@ -258,6 +262,29 @@ class Note { return this.attributes.length; } + get ancestors() { + if (!this.ancestorCache) { + const noteIds = new Set(); + this.ancestorCache = []; + + for (const parent of this.parents) { + if (!noteIds.has(parent.noteId)) { + this.ancestorCache.push(parent); + noteIds.add(parent.noteId); + } + + for (const ancestorNote of parent.ancestors) { + if (!noteIds.has(ancestorNote.noteId)) { + this.ancestorCache.push(ancestorNote); + noteIds.add(ancestorNote.noteId); + } + } + } + } + + return this.ancestorCache; + } + /** @return {Note[]} - returns only notes which are templated, does not include their subtrees * in effect returns notes which are influenced by note's non-inheritable attributes */ get templatedNotes() { diff --git a/src/services/search/expressions/descendant_of.js b/src/services/search/expressions/descendant_of.js new file mode 100644 index 000000000..0d7298077 --- /dev/null +++ b/src/services/search/expressions/descendant_of.js @@ -0,0 +1,30 @@ +"use strict"; + +const Expression = require('./expression'); +const NoteSet = require('../note_set'); + +class DescendantOfExp extends Expression { + constructor(subExpression) { + super(); + + this.subExpression = subExpression; + } + + execute(inputNoteSet, searchContext) { + const resNoteSet = new NoteSet(); + + for (const note of inputNoteSet.notes) { + const subInputNoteSet = new NoteSet(note.ancestors); + + const subResNoteSet = this.subExpression.execute(subInputNoteSet, searchContext); + + if (subResNoteSet.notes.length > 0) { + resNoteSet.add(note); + } + } + + return resNoteSet; + } +} + +module.exports = DescendantOfExp; diff --git a/src/services/search/note_set.js b/src/services/search/note_set.js index 85c402a29..8488692c2 100644 --- a/src/services/search/note_set.js +++ b/src/services/search/note_set.js @@ -2,6 +2,7 @@ class NoteSet { constructor(notes = []) { + /** @type {Note[]} */ this.notes = notes; } diff --git a/src/services/search/parser.js b/src/services/search/parser.js index 552707c82..adad9cece 100644 --- a/src/services/search/parser.js +++ b/src/services/search/parser.js @@ -4,6 +4,7 @@ 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'); @@ -64,6 +65,12 @@ function getExpression(tokens, parsingContext) { return new ParentOfExp(parseNoteProperty()); } + if (tokens[i] === 'ancestors') { + i += 1; + + return new DescendantOfExp(parseNoteProperty()); + } + if (tokens[i] === 'labels') { if (tokens[i + 1] !== '.') { parsingContext.addError(`Expected "." to separate field path, god "${tokens[i + 1]}"`);