added querying by relation's properties

This commit is contained in:
zadam 2020-05-23 12:27:44 +02:00
parent 3d12341ff1
commit 355ffd3d02
14 changed files with 120 additions and 29 deletions

View File

@ -191,6 +191,31 @@ describe("Search", () => {
expect(searchResults.length).toEqual(1); expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy(); expect(findNoteByTitle(searchResults, "Europe")).toBeTruthy();
}); });
it("filter by relation's note properties", async () => {
const austria = note("Austria");
const portugal = note("Portugal");
rootNote
.child(note("Europe")
.child(austria)
.child(note("Czech Republic")
.relation('neighbor', austria.note))
.child(portugal)
.child(note("Spain")
.relation('neighbor', portugal.note))
);
const parsingContext = new ParsingContext();
let searchResults = await searchService.findNotesWithQuery('# ~neighbor.title = Austria', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Czech Republic")).toBeTruthy();
searchResults = await searchService.findNotesWithQuery('# ~neighbor.title = Portugal', parsingContext);
expect(searchResults.length).toEqual(1);
expect(findNoteByTitle(searchResults, "Spain")).toBeTruthy();
});
}); });
/** @return {Note} */ /** @return {Note} */
@ -218,13 +243,13 @@ class NoteBuilder {
return this; return this;
} }
relation(name, note) { relation(name, targetNote) {
new Attribute(noteCache, { new Attribute(noteCache, {
attributeId: id(), attributeId: id(),
noteId: this.note.noteId, noteId: this.note.noteId,
type: 'relation', type: 'relation',
name, name,
value: note.noteId value: targetNote.noteId
}); });
return this; return this;

View File

@ -13,7 +13,7 @@ class Attribute {
/** @param {string} */ /** @param {string} */
this.name = row.name.toLowerCase(); this.name = row.name.toLowerCase();
/** @param {string} */ /** @param {string} */
this.value = row.value.toLowerCase(); this.value = row.type === 'label'? row.value.toLowerCase() : row.value;
/** @param {boolean} */ /** @param {boolean} */
this.isInheritable = !!row.isInheritable; this.isInheritable = !!row.isInheritable;

View File

@ -18,12 +18,12 @@ class AndExp extends Expression {
this.subExpressions = subExpressions; this.subExpressions = subExpressions;
} }
execute(noteSet, searchContext) { execute(inputNoteSet, searchContext) {
for (const subExpression of this.subExpressions) { for (const subExpression of this.subExpressions) {
noteSet = subExpression.execute(noteSet, searchContext); inputNoteSet = subExpression.execute(inputNoteSet, searchContext);
} }
return noteSet; return inputNoteSet;
} }
} }

View File

@ -13,7 +13,7 @@ class AttributeExistsExp extends Expression {
this.prefixMatch = prefixMatch; this.prefixMatch = prefixMatch;
} }
execute(noteSet) { execute(inputNoteSet) {
const attrs = this.prefixMatch const attrs = this.prefixMatch
? noteCache.findAttributesWithPrefix(this.attributeType, this.attributeName) ? noteCache.findAttributesWithPrefix(this.attributeType, this.attributeName)
: noteCache.findAttributes(this.attributeType, this.attributeName); : noteCache.findAttributes(this.attributeType, this.attributeName);
@ -23,7 +23,7 @@ class AttributeExistsExp extends Expression {
for (const attr of attrs) { for (const attr of attrs) {
const note = attr.note; const note = attr.note;
if (noteSet.hasNoteId(note.noteId)) { if (inputNoteSet.hasNoteId(note.noteId)) {
if (attr.isInheritable) { if (attr.isInheritable) {
resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); resultNoteSet.addAll(note.subtreeNotesIncludingTemplated);
} }

View File

@ -2,11 +2,11 @@
class Expression { class Expression {
/** /**
* @param {NoteSet} noteSet * @param {NoteSet} inputNoteSet
* @param {object} searchContext * @param {object} searchContext
* @return {NoteSet} * @return {NoteSet}
*/ */
execute(noteSet, searchContext) {} execute(inputNoteSet, searchContext) {}
} }
module.exports = Expression; module.exports = Expression;

View File

@ -13,14 +13,14 @@ class LabelComparisonExp extends Expression {
this.comparator = comparator; this.comparator = comparator;
} }
execute(noteSet) { execute(inputNoteSet) {
const attrs = noteCache.findAttributes(this.attributeType, this.attributeName); const attrs = noteCache.findAttributes(this.attributeType, this.attributeName);
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
for (const attr of attrs) { for (const attr of attrs) {
const note = attr.note; const note = attr.note;
if (noteSet.hasNoteId(note.noteId) && this.comparator(attr.value)) { if (inputNoteSet.hasNoteId(note.noteId) && this.comparator(attr.value)) {
if (attr.isInheritable) { if (attr.isInheritable) {
resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); resultNoteSet.addAll(note.subtreeNotesIncludingTemplated);
} }

View File

@ -9,10 +9,10 @@ class NotExp extends Expression {
this.subExpression = subExpression; this.subExpression = subExpression;
} }
execute(noteSet, searchContext) { execute(inputNoteSet, searchContext) {
const subNoteSet = this.subExpression.execute(noteSet, searchContext); const subNoteSet = this.subExpression.execute(inputNoteSet, searchContext);
return noteSet.minus(subNoteSet); return inputNoteSet.minus(subNoteSet);
} }
} }

View File

@ -11,7 +11,7 @@ class NoteCacheFulltextExp extends Expression {
this.tokens = tokens; this.tokens = tokens;
} }
execute(noteSet, searchContext) { execute(inputNoteSet, searchContext) {
// has deps on SQL which breaks unit test so needs to be dynamically required // has deps on SQL which breaks unit test so needs to be dynamically required
const noteCacheService = require('../../note_cache/note_cache_service'); const noteCacheService = require('../../note_cache/note_cache_service');
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
@ -66,7 +66,7 @@ class NoteCacheFulltextExp extends Expression {
} }
} }
const candidateNotes = this.getCandidateNotes(noteSet); const candidateNotes = this.getCandidateNotes(inputNoteSet);
for (const note of candidateNotes) { for (const note of candidateNotes) {
// autocomplete should be able to find notes by their noteIds as well (only leafs) // autocomplete should be able to find notes by their noteIds as well (only leafs)

View File

@ -11,7 +11,7 @@ class NoteContentFulltextExp extends Expression {
this.tokens = tokens; this.tokens = tokens;
} }
async execute(noteSet) { async execute(inputNoteSet) {
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
const wheres = this.tokens.map(token => "note_contents.content LIKE " + utils.prepareSqlForLike('%', token, '%')); const wheres = this.tokens.map(token => "note_contents.content LIKE " + utils.prepareSqlForLike('%', token, '%'));
@ -24,7 +24,7 @@ class NoteContentFulltextExp extends Expression {
WHERE isDeleted = 0 AND isProtected = 0 AND ${wheres.join(' AND ')}`); WHERE isDeleted = 0 AND isProtected = 0 AND ${wheres.join(' AND ')}`);
for (const noteId of noteIds) { for (const noteId of noteIds) {
if (noteSet.hasNoteId(noteId) && noteId in noteCache.notes) { if (inputNoteSet.hasNoteId(noteId) && noteId in noteCache.notes) {
resultNoteSet.add(noteCache.notes[noteId]); resultNoteSet.add(noteCache.notes[noteId]);
} }
} }

View File

@ -21,11 +21,11 @@ class OrExp extends Expression {
this.subExpressions = subExpressions; this.subExpressions = subExpressions;
} }
execute(noteSet, searchContext) { execute(inputNoteSet, searchContext) {
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
for (const subExpression of this.subExpressions) { for (const subExpression of this.subExpressions) {
resultNoteSet.mergeIn(subExpression.execute(noteSet, searchContext)); resultNoteSet.mergeIn(subExpression.execute(inputNoteSet, searchContext));
} }
return resultNoteSet; return resultNoteSet;

View File

@ -11,10 +11,10 @@ class PropertyComparisonExp extends Expression {
this.comparator = comparator; this.comparator = comparator;
} }
execute(noteSet, searchContext) { execute(inputNoteSet, searchContext) {
const resNoteSet = new NoteSet(); const resNoteSet = new NoteSet();
for (const note of noteSet.notes) { for (const note of inputNoteSet.notes) {
const value = note[this.propertyName].toLowerCase(); const value = note[this.propertyName].toLowerCase();
if (this.comparator(value)) { if (this.comparator(value)) {

View File

@ -0,0 +1,41 @@
"use strict";
const Expression = require('./expression');
const NoteSet = require('../note_set');
const noteCache = require('../../note_cache/note_cache');
class RelationWhereExp extends Expression {
constructor(relationName, subExpression) {
super();
this.relationName = relationName;
this.subExpression = subExpression;
}
execute(inputNoteSet, searchContext) {
const candidateNoteSet = new NoteSet();
for (const attr of noteCache.findAttributes('relation', this.relationName)) {
const note = attr.note;
if (inputNoteSet.hasNoteId(note.noteId)) {
const subInputNoteSet = new NoteSet([attr.targetNote]);
const subResNoteSet = this.subExpression.execute(subInputNoteSet, searchContext);
if (subResNoteSet.hasNote(attr.targetNote)) {
if (attr.isInheritable) {
candidateNoteSet.addAll(note.subtreeNotesIncludingTemplated);
} else if (note.isTemplate) {
candidateNoteSet.addAll(note.templatedNotes);
} else {
candidateNoteSet.add(note);
}
}
}
}
return candidateNoteSet.intersection(inputNoteSet);
}
}
module.exports = RelationWhereExp;

View File

@ -41,6 +41,18 @@ class NoteSet {
return newNoteSet; return newNoteSet;
} }
intersection(anotherNoteSet) {
const newNoteSet = new NoteSet();
for (const note of this.notes) {
if (anotherNoteSet.hasNote(note)) {
newNoteSet.add(note);
}
}
return newNoteSet;
}
} }
module.exports = NoteSet; module.exports = NoteSet;

View File

@ -5,6 +5,7 @@ const OrExp = require('./expressions/or');
const NotExp = require('./expressions/not'); const NotExp = require('./expressions/not');
const ChildOfExp = require('./expressions/child_of'); const ChildOfExp = require('./expressions/child_of');
const ParentOfExp = require('./expressions/parent_of'); const ParentOfExp = require('./expressions/parent_of');
const RelationWhereExp = require('./expressions/relation_where');
const PropertyComparisonExp = require('./expressions/property_comparison'); const PropertyComparisonExp = require('./expressions/property_comparison');
const AttributeExistsExp = require('./expressions/attribute_exists'); const AttributeExistsExp = require('./expressions/attribute_exists');
const LabelComparisonExp = require('./expressions/label_comparison'); const LabelComparisonExp = require('./expressions/label_comparison');
@ -90,10 +91,9 @@ function getExpression(tokens, parsingContext) {
if (Array.isArray(token)) { if (Array.isArray(token)) {
expressions.push(getExpression(token, parsingContext)); expressions.push(getExpression(token, parsingContext));
} }
else if (token.startsWith('#') || token.startsWith('~')) { else if (token.startsWith('#')) {
const type = token.startsWith('#') ? 'label' : 'relation'; const labelName = token.substr(1);
parsingContext.highlightedTokens.push(labelName);
parsingContext.highlightedTokens.push(token.substr(1));
if (i < tokens.length - 2 && isOperator(tokens[i + 1])) { if (i < tokens.length - 2 && isOperator(tokens[i + 1])) {
let operator = tokens[i + 1]; let operator = tokens[i + 1];
@ -112,12 +112,25 @@ function getExpression(tokens, parsingContext) {
continue; continue;
} }
expressions.push(new LabelComparisonExp(type, token.substr(1), comparator)); expressions.push(new LabelComparisonExp('label', labelName, comparator));
i += 2; i += 2;
} }
else { else {
expressions.push(new AttributeExistsExp(type, token.substr(1), parsingContext.fuzzyAttributeSearch)); expressions.push(new AttributeExistsExp('label', labelName, parsingContext.fuzzyAttributeSearch));
}
}
else if (token.startsWith('~')) {
const relationName = token.substr(1);
parsingContext.highlightedTokens.push(relationName);
if (i < tokens.length - 2 && tokens[i + 1] === '.') {
i += 1;
expressions.push(new RelationWhereExp(relationName, parseNoteProperty()));
}
else {
expressions.push(new AttributeExistsExp('relation', relationName, parsingContext.fuzzyAttributeSearch));
} }
} }
else if (token === 'note') { else if (token === 'note') {