mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +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(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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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;
 | 
					        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;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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') {
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user