diff --git a/spec/search/search.spec.js b/spec/search/search.spec.js index 95544fb2a..c628ac2a0 100644 --- a/spec/search/search.spec.js +++ b/spec/search/search.spec.js @@ -1,4 +1,4 @@ -const searchService = require('../../src/services/search/services/search.js'); +const searchService = require('../../src/services/search/services/search'); const BNote = require('../../src/becca/entities/bnote.js'); const BBranch = require('../../src/becca/entities/bbranch.js'); const SearchContext = require('../../src/services/search/search_context'); diff --git a/src/becca/entities/bnote.ts b/src/becca/entities/bnote.ts index 9886e7ae6..bda904f5b 100644 --- a/src/becca/entities/bnote.ts +++ b/src/becca/entities/bnote.ts @@ -78,13 +78,13 @@ class BNote extends AbstractBeccaEntity { // following attributes are filled during searching in the database /** size of the content in bytes */ - private contentSize!: number | null; + contentSize!: number | null; /** size of the note content, attachment contents in bytes */ - private contentAndAttachmentsSize!: number | null; + contentAndAttachmentsSize!: number | null; /** size of the note content, attachment contents and revision contents in bytes */ - private contentAndAttachmentsAndRevisionsSize!: number | null; + contentAndAttachmentsAndRevisionsSize!: number | null; /** number of note revisions for this note */ - private revisionCount!: number | null; + revisionCount!: number | null; constructor(row?: Partial) { super(); diff --git a/src/etapi/notes.js b/src/etapi/notes.js index 1ab0df248..ebb12910b 100644 --- a/src/etapi/notes.js +++ b/src/etapi/notes.js @@ -5,7 +5,7 @@ const mappers = require('./mappers.js'); const noteService = require('../services/notes'); const TaskContext = require('../services/task_context'); const v = require('./validators.js'); -const searchService = require('../services/search/services/search.js'); +const searchService = require('../services/search/services/search'); const SearchContext = require('../services/search/search_context'); const zipExportService = require('../services/export/zip.js'); const zipImportService = require('../services/import/zip.js'); diff --git a/src/routes/api/autocomplete.js b/src/routes/api/autocomplete.js index 4d48709a0..9ed36f89e 100644 --- a/src/routes/api/autocomplete.js +++ b/src/routes/api/autocomplete.js @@ -1,7 +1,7 @@ "use strict"; const beccaService = require('../../becca/becca_service'); -const searchService = require('../../services/search/services/search.js'); +const searchService = require('../../services/search/services/search'); const log = require('../../services/log'); const utils = require('../../services/utils'); const cls = require('../../services/cls'); diff --git a/src/routes/api/options.js b/src/routes/api/options.js index fd24422dc..88f72ae71 100644 --- a/src/routes/api/options.js +++ b/src/routes/api/options.js @@ -2,7 +2,7 @@ const optionService = require('../../services/options'); const log = require('../../services/log'); -const searchService = require('../../services/search/services/search.js'); +const searchService = require('../../services/search/services/search'); const ValidationError = require('../../errors/validation_error'); // options allowed to be updated directly in the Options dialog diff --git a/src/routes/api/search.js b/src/routes/api/search.js index f01093726..e4b9dec8f 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.js @@ -2,7 +2,7 @@ const becca = require('../../becca/becca'); const SearchContext = require('../../services/search/search_context'); -const searchService = require('../../services/search/services/search.js'); +const searchService = require('../../services/search/services/search'); const bulkActionService = require('../../services/bulk_actions.js'); const cls = require('../../services/cls'); const {formatAttrForSearch} = require('../../services/attribute_formatter'); diff --git a/src/routes/routes.js b/src/routes/routes.js index 2f2bdcb7d..3291fe7e5 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -46,7 +46,7 @@ const attributesRoute = require('./api/attributes.js'); const scriptRoute = require('./api/script.js'); const senderRoute = require('./api/sender.js'); const filesRoute = require('./api/files.js'); -const searchRoute = require('./api/search.js'); +const searchRoute = require('./api/search'); const bulkActionRoute = require('./api/bulk_action.js'); const specialNotesRoute = require('./api/special_notes.js'); const noteMapRoute = require('./api/note_map.js'); diff --git a/src/services/attributes.js b/src/services/attributes.js index fc527429c..a670ec731 100644 --- a/src/services/attributes.js +++ b/src/services/attributes.js @@ -1,6 +1,6 @@ "use strict"; -const searchService = require('./search/services/search.js'); +const searchService = require('./search/services/search'); const sql = require('./sql'); const becca = require('../becca/becca'); const BAttribute = require('../becca/entities/battribute'); diff --git a/src/services/backend_script_api.js b/src/services/backend_script_api.js index a09283478..592946ee1 100644 --- a/src/services/backend_script_api.js +++ b/src/services/backend_script_api.js @@ -11,7 +11,7 @@ const dayjs = require('dayjs'); const xml2js = require('xml2js'); const cloningService = require('./cloning.js'); const appInfo = require('./app_info'); -const searchService = require('./search/services/search.js'); +const searchService = require('./search/services/search'); const SearchContext = require('./search/search_context'); const becca = require('../becca/becca'); const ws = require('./ws'); diff --git a/src/services/date_notes.js b/src/services/date_notes.js index d5e5b38b6..afced2392 100644 --- a/src/services/date_notes.js +++ b/src/services/date_notes.js @@ -5,7 +5,7 @@ const attributeService = require('./attributes.js'); const dateUtils = require('./date_utils'); const sql = require('./sql'); const protectedSessionService = require('./protected_session'); -const searchService = require('../services/search/services/search.js'); +const searchService = require('../services/search/services/search'); const SearchContext = require('../services/search/search_context'); const hoistedNoteService = require('./hoisted_note'); diff --git a/src/services/search/search_context.ts b/src/services/search/search_context.ts index d9eb5945e..f7c1e2198 100644 --- a/src/services/search/search_context.ts +++ b/src/services/search/search_context.ts @@ -1,20 +1,7 @@ "use strict"; import hoistedNoteService = require('../hoisted_note'); - -interface SearchParams { - fastSearch?: boolean; - includeArchivedNotes?: boolean; - includeHiddenNotes?: boolean; - ignoreHoistedNote?: boolean; - ancestorNoteId?: string; - ancestorDepth?: string; - orderBy?: string; - orderDirection?: string; - limit?: number; - debug?: boolean; - fuzzyAttributeSearch?: boolean; -} +import { SearchParams } from './services/types'; class SearchContext { @@ -26,9 +13,9 @@ class SearchContext { ancestorDepth?: string; orderBy?: string; orderDirection?: string; - limit?: number; + limit?: number | null; debug?: boolean; - debugInfo: string | null; + debugInfo: {} | null; fuzzyAttributeSearch: boolean; highlightedTokens: string[]; originalQuery: string; diff --git a/src/services/search/search_result.ts b/src/services/search/search_result.ts index 61075910f..cd4e31e7a 100644 --- a/src/services/search/search_result.ts +++ b/src/services/search/search_result.ts @@ -4,13 +4,15 @@ import beccaService = require('../../becca/becca_service'); import becca = require('../../becca/becca'); class SearchResult { - private notePathArray: string[]; - private notePathTitle: string; - private score?: number; + notePathArray: string[]; + score: number; + notePathTitle: string; + highlightedNotePathTitle?: string; constructor(notePathArray: string[]) { this.notePathArray = notePathArray; this.notePathTitle = beccaService.getNoteTitleForPath(notePathArray); + this.score = 0; } get notePath() { diff --git a/src/services/search/services/parse.ts b/src/services/search/services/parse.ts index 2e24ed8ac..de943fdd2 100644 --- a/src/services/search/services/parse.ts +++ b/src/services/search/services/parse.ts @@ -448,13 +448,14 @@ function getExpression(tokens: TokenData[], searchContext: SearchContext, level function parse({fulltextTokens, expressionTokens, searchContext}: { fulltextTokens: TokenData[], - expressionTokens: TokenData[], - searchContext: SearchContext + expressionTokens: (TokenData | TokenData[])[], + searchContext: SearchContext, + originalQuery: string }) { let expression: Expression | undefined | null; try { - expression = getExpression(expressionTokens, searchContext); + expression = getExpression(expressionTokens as TokenData[], searchContext); } catch (e: any) { searchContext.addError(e.message); @@ -475,7 +476,7 @@ function parse({fulltextTokens, expressionTokens, searchContext}: { exp = new OrderByAndLimitExp([{ valueExtractor: new ValueExtractor(searchContext, ['note', searchContext.orderBy]), direction: searchContext.orderDirection - }], searchContext.limit); + }], searchContext.limit || undefined); (exp as any).subExpression = filterExp; } diff --git a/src/services/search/services/search.js b/src/services/search/services/search.ts similarity index 78% rename from src/services/search/services/search.js rename to src/services/search/services/search.ts index 67b7d4f1f..c986b80bb 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.ts @@ -1,22 +1,28 @@ "use strict"; -const normalizeString = require("normalize-strings"); -const lex = require('./lex'); -const handleParens = require('./handle_parens'); -const parse = require('./parse'); -const SearchResult = require('../search_result'); -const SearchContext = require('../search_context'); -const becca = require('../../../becca/becca'); -const beccaService = require('../../../becca/becca_service'); -const utils = require('../../utils'); -const log = require('../../log'); -const hoistedNoteService = require('../../hoisted_note'); +import normalizeString = require("normalize-strings"); +import lex = require('./lex'); +import handleParens = require('./handle_parens'); +import parse = require('./parse'); +import SearchResult = require('../search_result'); +import SearchContext = require('../search_context'); +import becca = require('../../../becca/becca'); +import beccaService = require('../../../becca/becca_service'); +import utils = require('../../utils'); +import log = require('../../log'); +import hoistedNoteService = require('../../hoisted_note'); +import BNote = require("../../../becca/entities/bnote"); +import BAttribute = require("../../../becca/entities/battribute"); +import { SearchParams, TokenData } from "./types"; +import Expression = require("../expressions/expression"); +import sql = require("../../sql"); -function searchFromNote(note) { - let searchResultNoteIds, highlightedTokens; +function searchFromNote(note: BNote) { + let searchResultNoteIds; + let highlightedTokens: string[]; const searchScript = note.getRelationValue('searchScript'); - const searchString = note.getLabelValue('searchString'); + const searchString = note.getLabelValue('searchString') || ""; let error = null; if (searchScript) { @@ -25,12 +31,12 @@ function searchFromNote(note) { } else { const searchContext = new SearchContext({ fastSearch: note.hasLabel('fastSearch'), - ancestorNoteId: note.getRelationValue('ancestor'), - ancestorDepth: note.getLabelValue('ancestorDepth'), + ancestorNoteId: note.getRelationValue('ancestor') || undefined, + ancestorDepth: note.getLabelValue('ancestorDepth') || undefined, includeArchivedNotes: note.hasLabel('includeArchivedNotes'), - orderBy: note.getLabelValue('orderBy'), - orderDirection: note.getLabelValue('orderDirection'), - limit: note.getLabelValue('limit'), + orderBy: note.getLabelValue('orderBy') || undefined, + orderDirection: note.getLabelValue('orderDirection') || undefined, + limit: parseInt(note.getLabelValue('limit') || "0", 10), debug: note.hasLabel('debug'), fuzzyAttributeSearch: false }); @@ -51,7 +57,7 @@ function searchFromNote(note) { }; } -function searchFromRelation(note, relationName) { +function searchFromRelation(note: BNote, relationName: string) { const scriptNote = note.getRelationTarget(relationName); if (!scriptNote) { @@ -90,18 +96,21 @@ function searchFromRelation(note, relationName) { } function loadNeededInfoFromDatabase() { - const sql = require('../../sql'); - /** * This complex structure is needed to calculate total occupied space by a note. Several object instances * (note, revisions, attachments) can point to a single blobId, and thus the blob size should count towards the total * only once. * - * @var {Object.>} - noteId => { blobId => blobSize } + * noteId => { blobId => blobSize } */ - const noteBlobs = {}; + const noteBlobs: Record> = {}; - const noteContentLengths = sql.getRows(` + type NoteContentLengthsRow = { + noteId: string; + blobId: string; + length: number; + }; + const noteContentLengths = sql.getRows(` SELECT noteId, blobId, @@ -122,7 +131,12 @@ function loadNeededInfoFromDatabase() { noteBlobs[noteId] = { [blobId]: length }; } - const attachmentContentLengths = sql.getRows(` + type AttachmentContentLengthsRow = { + noteId: string; + blobId: string; + length: number; + }; + const attachmentContentLengths = sql.getRows(` SELECT ownerId AS noteId, attachments.blobId, @@ -151,7 +165,13 @@ function loadNeededInfoFromDatabase() { becca.notes[noteId].contentAndAttachmentsSize = Object.values(noteBlobs[noteId]).reduce((acc, size) => acc + size, 0); } - const revisionContentLengths = sql.getRows(` + type RevisionRow = { + noteId: string; + blobId: string; + length: number; + isNoteRevision: true; + }; + const revisionContentLengths = sql.getRows(` SELECT noteId, revisions.blobId, @@ -186,8 +206,11 @@ function loadNeededInfoFromDatabase() { noteBlobs[noteId][blobId] = length; - if (isNoteRevision) { - becca.notes[noteId].revisionCount++; + if (isNoteRevision) { + const noteRevision = becca.notes[noteId]; + if (noteRevision && noteRevision.revisionCount) { + noteRevision.revisionCount++; + } } } @@ -196,20 +219,16 @@ function loadNeededInfoFromDatabase() { } } -/** - * @param {Expression} expression - * @param {SearchContext} searchContext - * @returns {SearchResult[]} - */ -function findResultsWithExpression(expression, searchContext) { +function findResultsWithExpression(expression: Expression, searchContext: SearchContext): SearchResult[] { if (searchContext.dbLoadNeeded) { loadNeededInfoFromDatabase(); } const allNoteSet = becca.getAllNoteSet(); + const noteIdToNotePath: Record = {}; const executionContext = { - noteIdToNotePath: {} + noteIdToNotePath }; const noteSet = expression.execute(allNoteSet, executionContext, searchContext); @@ -250,16 +269,16 @@ function findResultsWithExpression(expression, searchContext) { return searchResults; } -function parseQueryToExpression(query, searchContext) { +function parseQueryToExpression(query: string, searchContext: SearchContext) { const {fulltextQuery, fulltextTokens, expressionTokens} = lex(query); searchContext.fulltextQuery = fulltextQuery; - let structuredExpressionTokens; + let structuredExpressionTokens: (TokenData | TokenData[])[]; try { structuredExpressionTokens = handleParens(expressionTokens); } - catch (e) { + catch (e: any) { structuredExpressionTokens = []; searchContext.addError(e.message); } @@ -284,23 +303,13 @@ function parseQueryToExpression(query, searchContext) { return expression; } -/** - * @param {string} query - * @param {object} params - see SearchContext - * @returns {BNote[]} - */ -function searchNotes(query, params = {}) { +function searchNotes(query: string, params: SearchParams = {}): BNote[] { const searchResults = findResultsWithQuery(query, new SearchContext(params)); return searchResults.map(sr => becca.notes[sr.noteId]); } -/** - * @param {string} query - * @param {SearchContext} searchContext - * @returns {SearchResult[]} - */ -function findResultsWithQuery(query, searchContext) { +function findResultsWithQuery(query: string, searchContext: SearchContext): SearchResult[] { query = query || ""; searchContext.originalQuery = query; @@ -313,18 +322,13 @@ function findResultsWithQuery(query, searchContext) { return findResultsWithExpression(expression, searchContext); } -/** - * @param {string} query - * @param {SearchContext} searchContext - * @returns {BNote|null} - */ -function findFirstNoteWithQuery(query, searchContext) { +function findFirstNoteWithQuery(query: string, searchContext: SearchContext): BNote | null { const searchResults = findResultsWithQuery(query, searchContext); return searchResults.length > 0 ? becca.notes[searchResults[0].noteId] : null; } -function searchNotesForAutocomplete(query) { +function searchNotesForAutocomplete(query: string) { const searchContext = new SearchContext({ fastSearch: true, includeArchivedNotes: false, @@ -351,7 +355,7 @@ function searchNotesForAutocomplete(query) { }); } -function highlightSearchResults(searchResults, highlightedTokens) { +function highlightSearchResults(searchResults: SearchResult[], highlightedTokens: string[]) { highlightedTokens = Array.from(new Set(highlightedTokens)); // we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks @@ -387,7 +391,7 @@ function highlightSearchResults(searchResults, highlightedTokens) { } } - function wrapText(text, start, length, prefix, suffix) { + function wrapText(text: string, start: number, length: number, prefix: string, suffix: string) { return text.substring(0, start) + prefix + text.substr(start, length) + suffix + text.substring(start + length); } @@ -403,6 +407,7 @@ function highlightSearchResults(searchResults, highlightedTokens) { let match; // Find all matches + if (!result.highlightedNotePathTitle) { continue; } while ((match = tokenRegex.exec(normalizeString(result.highlightedNotePathTitle))) !== null) { result.highlightedNotePathTitle = wrapText(result.highlightedNotePathTitle, match.index, token.length, "{", "}"); @@ -413,6 +418,7 @@ function highlightSearchResults(searchResults, highlightedTokens) { } for (const result of searchResults) { + if (!result.highlightedNotePathTitle) { continue; } result.highlightedNotePathTitle = result.highlightedNotePathTitle .replace(/"/g, "") .replace(/'/g, "") @@ -421,7 +427,7 @@ function highlightSearchResults(searchResults, highlightedTokens) { } } -function formatAttribute(attr) { +function formatAttribute(attr: BAttribute) { if (attr.type === 'relation') { return `~${utils.escapeHtml(attr.name)}=…`; } @@ -438,7 +444,7 @@ function formatAttribute(attr) { } } -module.exports = { +export = { searchFromNote, searchNotesForAutocomplete, findResultsWithQuery, diff --git a/src/services/search/services/types.ts b/src/services/search/services/types.ts index c383bdc2d..09450f760 100644 --- a/src/services/search/services/types.ts +++ b/src/services/search/services/types.ts @@ -3,4 +3,18 @@ export interface TokenData { inQuotes?: boolean; startIndex?: number; endIndex?: number; +} + +export interface SearchParams { + fastSearch?: boolean; + includeArchivedNotes?: boolean; + includeHiddenNotes?: boolean; + ignoreHoistedNote?: boolean; + ancestorNoteId?: string; + ancestorDepth?: string; + orderBy?: string; + orderDirection?: string; + limit?: number | null; + debug?: boolean; + fuzzyAttributeSearch?: boolean; } \ No newline at end of file diff --git a/src/services/special_notes.js b/src/services/special_notes.js index f6e2af9ae..a600261c2 100644 --- a/src/services/special_notes.js +++ b/src/services/special_notes.js @@ -5,7 +5,7 @@ const noteService = require('./notes'); const dateUtils = require('./date_utils'); const log = require('./log'); const hoistedNoteService = require('./hoisted_note'); -const searchService = require('./search/services/search.js'); +const searchService = require('./search/services/search'); const SearchContext = require('./search/search_context'); const {LBTPL_NOTE_LAUNCHER, LBTPL_CUSTOM_WIDGET, LBTPL_SPACER, LBTPL_SCRIPT} = require('./hidden_subtree'); diff --git a/src/share/routes.js b/src/share/routes.js index 0e14fd6ca..4ad5a15c6 100644 --- a/src/share/routes.js +++ b/src/share/routes.js @@ -9,7 +9,7 @@ const shareRoot = require('./share_root.js'); const contentRenderer = require('./content_renderer.js'); const assetPath = require('../services/asset_path'); const appPath = require('../services/app_path'); -const searchService = require('../services/search/services/search.js'); +const searchService = require('../services/search/services/search'); const SearchContext = require('../services/search/search_context'); const log = require('../services/log'); diff --git a/src/types.d.ts b/src/types.d.ts index c4ccea844..602d291c4 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -11,4 +11,9 @@ declare module 'unescape' { declare module 'html2plaintext' { function html2plaintext(htmlText: string): string; export = html2plaintext; +} + +declare module 'normalize-strings' { + function normalizeString(string: string): string; + export = normalizeString; } \ No newline at end of file