From 3667bf3ed93b02c61ba00993143641f855dcc80c Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Wed, 25 Oct 2023 14:41:05 +0100 Subject: [PATCH] All: Allow searching by note ID or using a callback URL --- .eslintignore | 1 + .gitignore | 1 + packages/lib/models/utils/isItemId.ts | 3 + .../searchengine/SearchEngine.test.js | 18 ++++++ .../lib/services/searchengine/SearchEngine.ts | 55 +++++++++++++++++-- .../gotoAnythingStyleQuery.test.ts | 3 + .../searchengine/gotoAnythingStyleQuery.ts | 5 ++ 7 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 packages/lib/models/utils/isItemId.ts diff --git a/.eslintignore b/.eslintignore index e8e44d2799..70ce88dd40 100644 --- a/.eslintignore +++ b/.eslintignore @@ -660,6 +660,7 @@ packages/lib/models/SmartFilter.js packages/lib/models/Tag.js packages/lib/models/dateTimeFormats.test.js packages/lib/models/settings/FileHandler.js +packages/lib/models/utils/isItemId.js packages/lib/models/utils/itemCanBeEncrypted.js packages/lib/models/utils/paginatedFeed.js packages/lib/models/utils/paginationToSql.js diff --git a/.gitignore b/.gitignore index adce23da0f..ac3ee421df 100644 --- a/.gitignore +++ b/.gitignore @@ -642,6 +642,7 @@ packages/lib/models/SmartFilter.js packages/lib/models/Tag.js packages/lib/models/dateTimeFormats.test.js packages/lib/models/settings/FileHandler.js +packages/lib/models/utils/isItemId.js packages/lib/models/utils/itemCanBeEncrypted.js packages/lib/models/utils/paginatedFeed.js packages/lib/models/utils/paginationToSql.js diff --git a/packages/lib/models/utils/isItemId.ts b/packages/lib/models/utils/isItemId.ts new file mode 100644 index 0000000000..b0ecfbf994 --- /dev/null +++ b/packages/lib/models/utils/isItemId.ts @@ -0,0 +1,3 @@ +export default (id: string) => { + return id.match(/^[0-9a-zA-Z]{32}$/); +}; diff --git a/packages/lib/services/searchengine/SearchEngine.test.js b/packages/lib/services/searchengine/SearchEngine.test.js index 58a2e5c124..02345b3a32 100644 --- a/packages/lib/services/searchengine/SearchEngine.test.js +++ b/packages/lib/services/searchengine/SearchEngine.test.js @@ -3,6 +3,7 @@ const { setupDatabaseAndSynchronizer, db, sleep, switchClient, msleep } = require('../../testing/test-utils.js'); const SearchEngine = require('../../services/searchengine/SearchEngine').default; const Note = require('../../models/Note').default; +const Folder = require('../../models/Folder').default; const ItemChange = require('../../models/ItemChange').default; const Setting = require('../../models/Setting').default; @@ -526,4 +527,21 @@ describe('services_SearchEngine', () => { expect((await engine.search('hello')).length).toBe(0); expect((await engine.search('hello', { appendWildCards: true })).length).toBe(2); })); + + it('should search by item ID if no other result was found', (async () => { + const f1 = await Folder.save({}); + const n1 = await Note.save({ title: 'hello1', parent_id: f1.id }); + const n2 = await Note.save({ title: 'hello2' }); + + await engine.syncTables(); + + const results = await engine.search(n1.id); + expect(results.length).toBe(1); + expect(results[0].id).toBe(n1.id); + expect(results[0].title).toBe(n1.title); + expect(results[0].parent_id).toBe(n1.parent_id); + + expect((await engine.search(n2.id))[0].id).toBe(n2.id); + expect(await engine.search(f1.id)).toEqual([]); + })); }); diff --git a/packages/lib/services/searchengine/SearchEngine.ts b/packages/lib/services/searchengine/SearchEngine.ts index c00a1027a7..805690f223 100644 --- a/packages/lib/services/searchengine/SearchEngine.ts +++ b/packages/lib/services/searchengine/SearchEngine.ts @@ -9,6 +9,9 @@ import filterParser, { Term } from './filterParser'; import queryBuilder from './queryBuilder'; import { ItemChangeEntity, NoteEntity } from '../database/types'; import JoplinDatabase from '../../JoplinDatabase'; +import isItemId from '../../models/utils/isItemId'; +import BaseItem from '../../models/BaseItem'; +import { isCallbackUrl, parseCallbackUrl } from '../../callbackUrlUtils'; const { sprintf } = require('sprintf-js'); const { pregQuote, scriptType, removeDiacritics } = require('../../string-utils.js'); @@ -31,8 +34,11 @@ interface SearchOptions { export interface ProcessResultsRow { id: string; + parent_id: string; + title: string; offsets: string; user_updated_time: number; + user_created_time: number; matchinfo: Buffer; item_type?: ModelType; fields?: string[]; @@ -613,6 +619,39 @@ export default class SearchEngine { return SearchEngine.SEARCH_TYPE_FTS; } + private async searchFromItemIds(searchString: string): Promise { + let itemId = ''; + + if (isCallbackUrl(searchString)) { + const parsed = parseCallbackUrl(searchString); + itemId = parsed.params.id; + } else if (isItemId(searchString)) { + itemId = searchString; + } + + if (itemId) { + const item = await BaseItem.loadItemById(itemId); + + // We only return notes for now because the UI doesn't handle anything else. + if (item && item.type_ === ModelType.Note) { + return [ + { + id: item.id, + parent_id: item.parent_id || '', + matchinfo: Buffer.from(''), + offsets: '', + title: item.title || item.id, + user_updated_time: item.user_updated_time || item.updated_time, + user_created_time: item.user_created_time || item.created_time, + fields: ['id'], + }, + ]; + } + } + + return []; + } + public async search(searchString: string, options: SearchOptions = null): Promise { if (!searchString) return []; @@ -625,11 +664,12 @@ export default class SearchEngine { const searchType = this.determineSearchType_(searchString, options.searchType); const parsedQuery = await this.parseQuery(searchString); + let rows: ProcessResultsRow[] = []; + if (searchType === SearchEngine.SEARCH_TYPE_BASIC) { searchString = this.normalizeText_(searchString); - const rows = await this.basicSearch(searchString); + rows = await this.basicSearch(searchString); this.processResults_(rows, parsedQuery, true); - return rows; } else { // SEARCH_TYPE_FTS // FTS will ignore all special characters, like "-" in the index. So if @@ -654,14 +694,19 @@ export default class SearchEngine { const useFts = searchType === SearchEngine.SEARCH_TYPE_FTS; try { const { query, params } = queryBuilder(parsedQuery.allTerms, useFts); - const rows = (await this.db().selectAll(query, params)) as ProcessResultsRow[]; + rows = (await this.db().selectAll(query, params)) as ProcessResultsRow[]; this.processResults_(rows, parsedQuery, !useFts); - return rows; } catch (error) { this.logger().warn(`Cannot execute MATCH query: ${searchString}: ${error.message}`); - return []; + rows = []; } } + + if (!rows.length) { + rows = await this.searchFromItemIds(searchString); + } + + return rows; } public async destroy() { diff --git a/packages/lib/services/searchengine/gotoAnythingStyleQuery.test.ts b/packages/lib/services/searchengine/gotoAnythingStyleQuery.test.ts index 5f5bb4b87e..3db0b1a409 100644 --- a/packages/lib/services/searchengine/gotoAnythingStyleQuery.test.ts +++ b/packages/lib/services/searchengine/gotoAnythingStyleQuery.test.ts @@ -6,6 +6,9 @@ describe('searchengine/gotoAnythingStyleQuery', () => { const testCases: [string, string][] = [ ['hello', 'hello*'], ['hello welc', 'hello* welc*'], + ['joplin://x-callback-url/openNote?id=3600e074af0e4b06aeb0ae76d3d96af7', 'joplin://x-callback-url/openNote?id=3600e074af0e4b06aeb0ae76d3d96af7'], + ['3600e074af0e4b06aeb0ae76d3d96af7', '3600e074af0e4b06aeb0ae76d3d96af7'], + ['', ''], ]; for (const [input, expected] of testCases) { diff --git a/packages/lib/services/searchengine/gotoAnythingStyleQuery.ts b/packages/lib/services/searchengine/gotoAnythingStyleQuery.ts index 0d09f0ee17..82d73385e0 100644 --- a/packages/lib/services/searchengine/gotoAnythingStyleQuery.ts +++ b/packages/lib/services/searchengine/gotoAnythingStyleQuery.ts @@ -1,6 +1,11 @@ +import { isCallbackUrl } from '../../callbackUrlUtils'; +import isItemId from '../../models/utils/isItemId'; + export default (query: string) => { if (!query) return ''; + if (isItemId(query) || isCallbackUrl(query)) return query; + const output = []; const splitted = query.split(' ');