From a1423e485129aa3c302c8feae111337441d6761b Mon Sep 17 00:00:00 2001
From: Naveen M V <30305957+naviji@users.noreply.github.com>
Date: Thu, 11 Mar 2021 20:12:39 +0530
Subject: [PATCH] All: Filter "notebook" can now be negated (#4651)
---
README.md | 2 +-
packages/app-cli/tests/filterParser.js | 2 -
.../app-cli/tests/services_SearchFilter.js | 48 +++++++++++++++++++
.../lib/services/searchengine/filterParser.ts | 2 +-
.../lib/services/searchengine/queryBuilder.ts | 30 ++++++++----
5 files changed, 72 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index e4cdbabe6b..2d4f3cc949 100644
--- a/README.md
+++ b/README.md
@@ -386,7 +386,7 @@ You can also use search filters to further restrict the search.
|**any:**|Return notes that satisfy any/all of the required conditions. `any:0` is the default, which means all conditions must be satisfied.|`any:1 cat dog` will return notes that have the word `cat` or `dog`.
`any:0 cat dog` will return notes with both the words `cat` and `dog`. |
| **title:**
**body:**|Restrict your search to just the title or the body field.|`title:"hello world"` searches for notes whose title contains `hello` and `world`.
`title:hello -body:world` searches for notes whose title contains `hello` and body does not contain `world`.
| **tag:** |Restrict the search to the notes with the specified tags.|`tag:office` searches for all notes having tag office.
`tag:office tag:important` searches for all notes having both office and important tags.
`tag:office -tag:spam` searches for notes having tag `office` which do not have tag `spam`.
`any:1 tag:office tag:spam` searches for notes having tag `office` or tag `spam`.
`tag:be*ful` does a search with wildcards.
`tag:*` returns all notes with tags.
`-tag:*` returns all notes without tags.|
-| **notebook:** | Restrict the search to the specified notebook(s). It cannot be negated. |`notebook:books` limits the search scope within `books` and all its subnotebooks.
`notebook:wheel*time` does a wildcard search.|
+| **notebook:** | Restrict the search to the specified notebook(s). |`notebook:books` limits the search scope within `books` and all its subnotebooks.
`notebook:wheel*time` does a wildcard search.|
| **created:**
**updated:** | Searches for notes created/updated on dates specified using YYYYMMDD format. You can also search relative to the current day, week, month, or year. | `created:20201218` will return notes created on or after December 18, 2020.
`-updated:20201218` will return notes updated before December 18, 2020.
`created:20200118 -created:20201215` will return notes created between January 18, 2020, and before December 15, 2020.
`created:202001 -created:202003` will return notes created on or after January and before March 2020.
`updated:1997 -updated:2020` will return all notes updated between the years 1997 and 2019.
`created:day-2` searches for all notes created in the past two days.
`updated:year-0` searches all notes updated in the current year.
| **type:** |Restrict the search to either notes or todos. | `type:note` to return all notes
`type:todo` to return all todos |
| **iscompleted:** | Restrict the search to either completed or uncompleted todos. | `iscompleted:1` to return all completed todos
`iscompleted:0` to return all uncompleted todos|
diff --git a/packages/app-cli/tests/filterParser.js b/packages/app-cli/tests/filterParser.js
index 71bcb56a40..3566291c4f 100644
--- a/packages/app-cli/tests/filterParser.js
+++ b/packages/app-cli/tests/filterParser.js
@@ -152,8 +152,6 @@ describe('filterParser should be correct filter for keyword', () => {
searchString = 'iscompleted:blah';
expect(() => filterParser(searchString)).toThrow(new Error('The value of filter "iscompleted" must be "1" or "0"'));
- searchString = '-notebook:n1';
- expect(() => filterParser(searchString)).toThrow(new Error('notebook can\'t be negated'));
searchString = '-iscompleted:1';
expect(() => filterParser(searchString)).toThrow(new Error('iscompleted can\'t be negated'));
diff --git a/packages/app-cli/tests/services_SearchFilter.js b/packages/app-cli/tests/services_SearchFilter.js
index 36c29d1213..93a0e6cfe6 100644
--- a/packages/app-cli/tests/services_SearchFilter.js
+++ b/packages/app-cli/tests/services_SearchFilter.js
@@ -742,4 +742,52 @@ describe('services_SearchFilter', function() {
}));
+ it('should support negating notebooks', (async () => {
+
+ const folder1 = await Folder.save({ title: 'folder1' });
+ let n1 = await Note.save({ title: 'task1', body: 'foo', parent_id: folder1.id });
+ let n2 = await Note.save({ title: 'task2', body: 'bar', parent_id: folder1.id });
+
+
+ const folder2 = await Folder.save({ title: 'folder2' });
+ let n3 = await Note.save({ title: 'task3', body: 'baz', parent_id: folder2.id });
+ let n4 = await Note.save({ title: 'task4', body: 'blah', parent_id: folder2.id });
+
+
+ await engine.syncTables();
+
+ let rows = await engine.search('-notebook:folder1');
+ expect(rows.length).toBe(2);
+ expect(ids(rows)).toContain(n3.id);
+ expect(ids(rows)).toContain(n4.id);
+
+
+ rows = await engine.search('-notebook:folder2');
+ expect(rows.length).toBe(2);
+ expect(ids(rows)).toContain(n1.id);
+ expect(ids(rows)).toContain(n2.id);
+
+ }));
+
+ it('should support both inclusion and exclusion of notebooks together', (async () => {
+
+ const parentFolder = await Folder.save({ title: 'parent' });
+ let n1 = await Note.save({ title: 'task1', body: 'foo', parent_id: parentFolder.id });
+ let n2 = await Note.save({ title: 'task2', body: 'bar', parent_id: parentFolder.id });
+
+
+ const subFolder = await Folder.save({ title: 'child', parent_id: parentFolder.id });
+ let n3 = await Note.save({ title: 'task3', body: 'baz', parent_id: subFolder.id });
+ let n4 = await Note.save({ title: 'task4', body: 'blah', parent_id: subFolder.id });
+
+
+ await engine.syncTables();
+
+ let rows = await engine.search('notebook:parent -notebook:child');
+ expect(rows.length).toBe(2);
+ expect(ids(rows)).toContain(n1.id);
+ expect(ids(rows)).toContain(n2.id);
+
+ }));
+
});
diff --git a/packages/lib/services/searchengine/filterParser.ts b/packages/lib/services/searchengine/filterParser.ts
index eb1b7d77d7..4c978ed0bc 100644
--- a/packages/lib/services/searchengine/filterParser.ts
+++ b/packages/lib/services/searchengine/filterParser.ts
@@ -118,7 +118,7 @@ const parseQuery = (query: string): Term[] => {
}
// validation
- let incorrect = result.filter(term => term.name === 'type' || term.name === 'iscompleted' || term.name === 'notebook')
+ let incorrect = result.filter(term => term.name === 'type' || term.name === 'iscompleted')
.find(x => x.negated);
if (incorrect) throw new Error(`${incorrect.name} can't be negated`);
diff --git a/packages/lib/services/searchengine/queryBuilder.ts b/packages/lib/services/searchengine/queryBuilder.ts
index 99dac2edaf..38ccef9a71 100644
--- a/packages/lib/services/searchengine/queryBuilder.ts
+++ b/packages/lib/services/searchengine/queryBuilder.ts
@@ -21,18 +21,19 @@ enum Requirement {
INCLUSION = 'INCLUSION',
}
-const notebookFilter = (terms: Term[], conditions: string[], params: string[], withs: string[]) => {
- const notebooks = terms.filter(x => x.name === 'notebook' && !x.negated).map(x => x.value);
+const _notebookFilter = (notebooks: string[], requirement: Requirement, conditions: string[], params: string[], withs: string[]) => {
if (notebooks.length === 0) return;
const likes = [];
for (let i = 0; i < notebooks.length; i++) {
likes.push('folders.title LIKE ?');
}
+
const relevantFolders = likes.join(' OR ');
+ const viewName = requirement === Requirement.EXCLUSION ? 'notebooks_not_in_scope' : 'notebooks_in_scope';
const withInNotebook = `
- notebooks_in_scope(id)
+ ${viewName}(id)
AS (
SELECT folders.id
FROM folders
@@ -45,22 +46,35 @@ const notebookFilter = (terms: Term[], conditions: string[], params: string[], w
UNION ALL
SELECT folders.id
FROM folders
- JOIN notebooks_in_scope
- ON folders.parent_id=notebooks_in_scope.id
+ JOIN ${viewName}
+ ON folders.parent_id=${viewName}.id
)`;
+
const where = `
- AND ROWID IN (
+ AND ROWID ${requirement === Requirement.EXCLUSION ? 'NOT' : ''} IN (
SELECT notes_normalized.ROWID
- FROM notebooks_in_scope
+ FROM ${viewName}
JOIN notes_normalized
- ON notebooks_in_scope.id=notes_normalized.parent_id
+ ON ${viewName}.id=notes_normalized.parent_id
)`;
+
withs.push(withInNotebook);
params.push(...notebooks);
conditions.push(where);
+
};
+const notebookFilter = (terms: Term[], conditions: string[], params: string[], withs: string[]) => {
+ const notebooksToInclude = terms.filter(x => x.name === 'notebook' && !x.negated).map(x => x.value);
+ _notebookFilter(notebooksToInclude, Requirement.INCLUSION, conditions, params, withs);
+
+ const notebooksToExclude = terms.filter(x => x.name === 'notebook' && x.negated).map(x => x.value);
+ _notebookFilter(notebooksToExclude, Requirement.EXCLUSION, conditions, params, withs);
+};
+
+
+
const getOperator = (requirement: Requirement, relation: Relation): Operation => {
if (relation === 'AND' && requirement === 'INCLUSION') { return Operation.INTERSECT; } else { return Operation.UNION; }
};