mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	added querying by relation's properties
This commit is contained in:
		| @@ -191,6 +191,31 @@ describe("Search", () => { | ||||
|         expect(searchResults.length).toEqual(1); | ||||
|         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} */ | ||||
| @@ -218,13 +243,13 @@ class NoteBuilder { | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
|     relation(name, note) { | ||||
|     relation(name, targetNote) { | ||||
|         new Attribute(noteCache, { | ||||
|             attributeId: id(), | ||||
|             noteId: this.note.noteId, | ||||
|             type: 'relation', | ||||
|             name, | ||||
|             value: note.noteId | ||||
|             value: targetNote.noteId | ||||
|         }); | ||||
|  | ||||
|         return this; | ||||
|   | ||||
| @@ -13,7 +13,7 @@ class Attribute { | ||||
|         /** @param {string} */ | ||||
|         this.name = row.name.toLowerCase(); | ||||
|         /** @param {string} */ | ||||
|         this.value = row.value.toLowerCase(); | ||||
|         this.value = row.type === 'label'? row.value.toLowerCase() : row.value; | ||||
|         /** @param {boolean} */ | ||||
|         this.isInheritable = !!row.isInheritable; | ||||
|  | ||||
|   | ||||
| @@ -18,12 +18,12 @@ class AndExp extends Expression { | ||||
|         this.subExpressions = subExpressions; | ||||
|     } | ||||
|  | ||||
|     execute(noteSet, searchContext) { | ||||
|     execute(inputNoteSet, searchContext) { | ||||
|         for (const subExpression of this.subExpressions) { | ||||
|             noteSet = subExpression.execute(noteSet, searchContext); | ||||
|             inputNoteSet = subExpression.execute(inputNoteSet, searchContext); | ||||
|         } | ||||
|  | ||||
|         return noteSet; | ||||
|         return inputNoteSet; | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -13,7 +13,7 @@ class AttributeExistsExp extends Expression { | ||||
|         this.prefixMatch = prefixMatch; | ||||
|     } | ||||
|  | ||||
|     execute(noteSet) { | ||||
|     execute(inputNoteSet) { | ||||
|         const attrs = this.prefixMatch | ||||
|             ? noteCache.findAttributesWithPrefix(this.attributeType, this.attributeName) | ||||
|             : noteCache.findAttributes(this.attributeType, this.attributeName); | ||||
| @@ -23,7 +23,7 @@ class AttributeExistsExp extends Expression { | ||||
|         for (const attr of attrs) { | ||||
|             const note = attr.note; | ||||
|  | ||||
|             if (noteSet.hasNoteId(note.noteId)) { | ||||
|             if (inputNoteSet.hasNoteId(note.noteId)) { | ||||
|                 if (attr.isInheritable) { | ||||
|                     resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); | ||||
|                 } | ||||
|   | ||||
| @@ -2,11 +2,11 @@ | ||||
|  | ||||
| class Expression { | ||||
|     /** | ||||
|      * @param {NoteSet} noteSet | ||||
|      * @param {NoteSet} inputNoteSet | ||||
|      * @param {object} searchContext | ||||
|      * @return {NoteSet} | ||||
|      */ | ||||
|     execute(noteSet, searchContext) {} | ||||
|     execute(inputNoteSet, searchContext) {} | ||||
| } | ||||
|  | ||||
| module.exports = Expression; | ||||
|   | ||||
| @@ -13,14 +13,14 @@ class LabelComparisonExp extends Expression { | ||||
|         this.comparator = comparator; | ||||
|     } | ||||
|  | ||||
|     execute(noteSet) { | ||||
|     execute(inputNoteSet) { | ||||
|         const attrs = noteCache.findAttributes(this.attributeType, this.attributeName); | ||||
|         const resultNoteSet = new NoteSet(); | ||||
|  | ||||
|         for (const attr of attrs) { | ||||
|             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) { | ||||
|                     resultNoteSet.addAll(note.subtreeNotesIncludingTemplated); | ||||
|                 } | ||||
|   | ||||
| @@ -9,10 +9,10 @@ class NotExp extends Expression { | ||||
|         this.subExpression = subExpression; | ||||
|     } | ||||
|  | ||||
|     execute(noteSet, searchContext) { | ||||
|         const subNoteSet = this.subExpression.execute(noteSet, searchContext); | ||||
|     execute(inputNoteSet, searchContext) { | ||||
|         const subNoteSet = this.subExpression.execute(inputNoteSet, searchContext); | ||||
|  | ||||
|         return noteSet.minus(subNoteSet); | ||||
|         return inputNoteSet.minus(subNoteSet); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class NoteCacheFulltextExp extends Expression { | ||||
|         this.tokens = tokens; | ||||
|     } | ||||
|  | ||||
|     execute(noteSet, searchContext) { | ||||
|     execute(inputNoteSet, searchContext) { | ||||
|         // has deps on SQL which breaks unit test so needs to be dynamically required | ||||
|         const noteCacheService = require('../../note_cache/note_cache_service'); | ||||
|         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) { | ||||
|             // autocomplete should be able to find notes by their noteIds as well (only leafs) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class NoteContentFulltextExp extends Expression { | ||||
|         this.tokens = tokens; | ||||
|     } | ||||
|  | ||||
|     async execute(noteSet) { | ||||
|     async execute(inputNoteSet) { | ||||
|         const resultNoteSet = new NoteSet(); | ||||
|         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 ')}`); | ||||
|  | ||||
|         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]); | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -21,11 +21,11 @@ class OrExp extends Expression { | ||||
|         this.subExpressions = subExpressions; | ||||
|     } | ||||
|  | ||||
|     execute(noteSet, searchContext) { | ||||
|     execute(inputNoteSet, searchContext) { | ||||
|         const resultNoteSet = new NoteSet(); | ||||
|  | ||||
|         for (const subExpression of this.subExpressions) { | ||||
|             resultNoteSet.mergeIn(subExpression.execute(noteSet, searchContext)); | ||||
|             resultNoteSet.mergeIn(subExpression.execute(inputNoteSet, searchContext)); | ||||
|         } | ||||
|  | ||||
|         return resultNoteSet; | ||||
|   | ||||
| @@ -11,10 +11,10 @@ class PropertyComparisonExp extends Expression { | ||||
|         this.comparator = comparator; | ||||
|     } | ||||
|  | ||||
|     execute(noteSet, searchContext) { | ||||
|     execute(inputNoteSet, searchContext) { | ||||
|         const resNoteSet = new NoteSet(); | ||||
|  | ||||
|         for (const note of noteSet.notes) { | ||||
|         for (const note of inputNoteSet.notes) { | ||||
|             const value = note[this.propertyName].toLowerCase(); | ||||
|  | ||||
|             if (this.comparator(value)) { | ||||
|   | ||||
							
								
								
									
										41
									
								
								src/services/search/expressions/relation_where.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/services/search/expressions/relation_where.js
									
									
									
									
									
										Normal 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; | ||||
| @@ -41,6 +41,18 @@ class NoteSet { | ||||
|  | ||||
|         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; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ const OrExp = require('./expressions/or'); | ||||
| const NotExp = require('./expressions/not'); | ||||
| const ChildOfExp = require('./expressions/child_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'); | ||||
| @@ -90,10 +91,9 @@ function getExpression(tokens, parsingContext) { | ||||
|         if (Array.isArray(token)) { | ||||
|             expressions.push(getExpression(token, parsingContext)); | ||||
|         } | ||||
|         else if (token.startsWith('#') || token.startsWith('~')) { | ||||
|             const type = token.startsWith('#') ? 'label' : 'relation'; | ||||
|  | ||||
|             parsingContext.highlightedTokens.push(token.substr(1)); | ||||
|         else if (token.startsWith('#')) { | ||||
|             const labelName = token.substr(1); | ||||
|             parsingContext.highlightedTokens.push(labelName); | ||||
|  | ||||
|             if (i < tokens.length - 2 && isOperator(tokens[i + 1])) { | ||||
|                 let operator = tokens[i + 1]; | ||||
| @@ -112,12 +112,25 @@ function getExpression(tokens, parsingContext) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 expressions.push(new LabelComparisonExp(type, token.substr(1), comparator)); | ||||
|                 expressions.push(new LabelComparisonExp('label', labelName, comparator)); | ||||
|  | ||||
|                 i += 2; | ||||
|             } | ||||
|             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') { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user