From f8649feea46e4a16b31582cbad2cfa2bb0932d53 Mon Sep 17 00:00:00 2001 From: azivner Date: Fri, 23 Mar 2018 23:08:29 -0400 Subject: [PATCH] saved search can now be created from the search dialog --- src/public/javascripts/note_tree.js | 2 +- src/public/javascripts/search_tree.js | 10 ++- src/routes/api/notes.js | 109 -------------------------- src/routes/api/search.js | 35 +++++++++ src/routes/routes.js | 2 + src/services/build_search_query.js | 67 ++++++++++++++++ src/services/notes.js | 2 +- src/services/parse_filters.js | 28 +++++++ 8 files changed, 142 insertions(+), 113 deletions(-) create mode 100644 src/routes/api/search.js create mode 100644 src/services/build_search_query.js create mode 100644 src/services/parse_filters.js diff --git a/src/public/javascripts/note_tree.js b/src/public/javascripts/note_tree.js index f61b0189b..71358be55 100644 --- a/src/public/javascripts/note_tree.js +++ b/src/public/javascripts/note_tree.js @@ -659,7 +659,7 @@ const noteTree = (function() { const json = JSON.parse(note.detail.content); - const noteIds = await server.get('notes?search=' + encodeURIComponent(json.searchString)); + const noteIds = await server.get('search/' + encodeURIComponent(json.searchString)); for (const noteId of noteIds) { const noteTreeId = "virt" + randomString(10); diff --git a/src/public/javascripts/search_tree.js b/src/public/javascripts/search_tree.js index fd73ee093..fcd578898 100644 --- a/src/public/javascripts/search_tree.js +++ b/src/public/javascripts/search_tree.js @@ -35,7 +35,7 @@ const searchTree = (function() { async function doSearch() { const searchText = $searchInput.val(); - const noteIds = await server.get('notes?search=' + encodeURIComponent(searchText)); + const noteIds = await server.get('search/' + encodeURIComponent(searchText)); for (const noteId of noteIds) { await noteTree.expandToNote(noteId, {noAnimation: true, noEvents: true}); @@ -60,7 +60,13 @@ const searchTree = (function() { $doSearchButton.click(doSearch); - $saveSearchButton.click(() => alert("Save search")); + $saveSearchButton.click(async () => { + const {noteId} = await server.post('search/' + encodeURIComponent($searchInput.val())); + + await noteTree.reload(); + + await noteTree.activateNode(noteId); + }); $(document).bind('keydown', 'ctrl+s', e => { toggleSearch(); diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index 6f058ee59..187843b4c 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -69,115 +69,6 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { res.send({}); })); -router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { - let {attrFilters, searchText} = parseFilters(req.query.search); - - const {query, params} = getSearchQuery(attrFilters, searchText); - - console.log(query, params); - - const noteIds = await sql.getColumn(query, params); - - res.send(noteIds); -})); - -function parseFilters(searchText) { - const attrFilters = []; - - const attrRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i; - - let match = attrRegex.exec(searchText); - - function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; } - - while (match != null) { - const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and'; - const operator = match[3] === '!' ? 'not-exists' : 'exists'; - - attrFilters.push({ - relation: relation, - name: trimQuotes(match[4]), - operator: match[6] !== undefined ? match[6] : operator, - value: match[7] !== undefined ? trimQuotes(match[7]) : null - }); - - // remove attributes from further fulltext search - searchText = searchText.split(match[0]).join(''); - - match = attrRegex.exec(searchText); - } - - return {attrFilters, searchText}; -} - -function getSearchQuery(attrFilters, searchText) { - const joins = []; - const joinParams = []; - let where = '1'; - const whereParams = []; - - let i = 1; - - for (const filter of attrFilters) { - joins.push(`LEFT JOIN attributes AS attr${i} ON attr${i}.noteId = notes.noteId AND attr${i}.name = ?`); - joinParams.push(filter.name); - - where += " " + filter.relation + " "; - - if (filter.operator === 'exists') { - where += `attr${i}.attributeId IS NOT NULL`; - } - else if (filter.operator === 'not-exists') { - where += `attr${i}.attributeId IS NULL`; - } - else if (filter.operator === '=' || filter.operator === '!=') { - where += `attr${i}.value ${filter.operator} ?`; - whereParams.push(filter.value); - } - else if ([">", ">=", "<", "<="].includes(filter.operator)) { - const floatParam = parseFloat(filter.value); - - if (isNaN(floatParam)) { - where += `attr${i}.value ${filter.operator} ?`; - whereParams.push(filter.value); - } - else { - where += `CAST(attr${i}.value AS DECIMAL) ${filter.operator} ?`; - whereParams.push(floatParam); - } - } - else { - throw new Error("Unknown operator " + filter.operator); - } - - i++; - } - - let searchCondition = ''; - const searchParams = []; - - if (searchText.trim() !== '') { - // searching in protected notes is pointless because of encryption - searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))'; - - searchText = '%' + searchText.trim() + '%'; - - searchParams.push(searchText); - searchParams.push(searchText); // two occurences in searchCondition - } - - const query = `SELECT DISTINCT notes.noteId FROM notes - ${joins.join('\r\n')} - WHERE - notes.isDeleted = 0 - AND (${where}) - ${searchCondition}`; - - const params = joinParams.concat(whereParams).concat(searchParams); - - return { query, params }; -} - router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => { const noteId = req.params.noteId; const sourceId = req.headers.source_id; diff --git a/src/routes/api/search.js b/src/routes/api/search.js new file mode 100644 index 000000000..61ce68d02 --- /dev/null +++ b/src/routes/api/search.js @@ -0,0 +1,35 @@ +"use strict"; + +const express = require('express'); +const router = express.Router(); +const auth = require('../../services/auth'); +const sql = require('../../services/sql'); +const notes = require('../../services/notes'); +const wrap = require('express-promise-wrap').wrap; +const parseFilters = require('../../services/parse_filters'); +const buildSearchQuery = require('../../services/build_search_query'); + +router.get('/:searchString', auth.checkApiAuth, wrap(async (req, res, next) => { + const {attrFilters, searchText} = parseFilters(req.params.searchString); + + const {query, params} = buildSearchQuery(attrFilters, searchText); + + const noteIds = await sql.getColumn(query, params); + + res.send(noteIds); +})); + +router.post('/:searchString', auth.checkApiAuth, wrap(async (req, res, next) => { + const noteContent = { + searchString: req.params.searchString + }; + + const noteId = await notes.createNote('root', 'Search note', noteContent, { + json: true, + type: 'search' + }); + + res.send({ noteId }); +})); + +module.exports = router; \ No newline at end of file diff --git a/src/routes/routes.js b/src/routes/routes.js index 0d3704b5d..8d7055cdd 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -30,6 +30,7 @@ const attributesRoute = require('./api/attributes'); const scriptRoute = require('./api/script'); const senderRoute = require('./api/sender'); const attachmentsRoute = require('./api/attachments'); +const searchRoute = require('./api/search'); function register(app) { app.use('/', indexRoute); @@ -63,6 +64,7 @@ function register(app) { app.use('/api/script', scriptRoute); app.use('/api/sender', senderRoute); app.use('/api/attachments', attachmentsRoute); + app.use('/api/search', searchRoute); } module.exports = { diff --git a/src/services/build_search_query.js b/src/services/build_search_query.js new file mode 100644 index 000000000..e877cd184 --- /dev/null +++ b/src/services/build_search_query.js @@ -0,0 +1,67 @@ +module.exports = function(attrFilters, searchText) { + const joins = []; + const joinParams = []; + let where = '1'; + const whereParams = []; + + let i = 1; + + for (const filter of attrFilters) { + joins.push(`LEFT JOIN attributes AS attr${i} ON attr${i}.noteId = notes.noteId AND attr${i}.name = ?`); + joinParams.push(filter.name); + + where += " " + filter.relation + " "; + + if (filter.operator === 'exists') { + where += `attr${i}.attributeId IS NOT NULL`; + } + else if (filter.operator === 'not-exists') { + where += `attr${i}.attributeId IS NULL`; + } + else if (filter.operator === '=' || filter.operator === '!=') { + where += `attr${i}.value ${filter.operator} ?`; + whereParams.push(filter.value); + } + else if ([">", ">=", "<", "<="].includes(filter.operator)) { + const floatParam = parseFloat(filter.value); + + if (isNaN(floatParam)) { + where += `attr${i}.value ${filter.operator} ?`; + whereParams.push(filter.value); + } + else { + where += `CAST(attr${i}.value AS DECIMAL) ${filter.operator} ?`; + whereParams.push(floatParam); + } + } + else { + throw new Error("Unknown operator " + filter.operator); + } + + i++; + } + + let searchCondition = ''; + const searchParams = []; + + if (searchText.trim() !== '') { + // searching in protected notes is pointless because of encryption + searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))'; + + searchText = '%' + searchText.trim() + '%'; + + searchParams.push(searchText); + searchParams.push(searchText); // two occurences in searchCondition + } + + const query = `SELECT DISTINCT notes.noteId FROM notes + ${joins.join('\r\n')} + WHERE + notes.isDeleted = 0 + AND (${where}) + ${searchCondition}`; + + const params = joinParams.concat(whereParams).concat(searchParams); + + return { query, params }; +}; \ No newline at end of file diff --git a/src/services/notes.js b/src/services/notes.js index 3f38f1039..a7c2967d4 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -96,7 +96,7 @@ async function createNote(parentNoteId, title, content = "", extraOptions = {}) mime: extraOptions.mime }; - if (extraOptions.json) { + if (extraOptions.json && !note.type) { note.type = "code"; note.mime = "application/json"; } diff --git a/src/services/parse_filters.js b/src/services/parse_filters.js new file mode 100644 index 000000000..2235d6e7d --- /dev/null +++ b/src/services/parse_filters.js @@ -0,0 +1,28 @@ +module.exports = function(searchText) { + const attrFilters = []; + + const attrRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i; + + let match = attrRegex.exec(searchText); + + function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; } + + while (match != null) { + const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and'; + const operator = match[3] === '!' ? 'not-exists' : 'exists'; + + attrFilters.push({ + relation: relation, + name: trimQuotes(match[4]), + operator: match[6] !== undefined ? match[6] : operator, + value: match[7] !== undefined ? trimQuotes(match[7]) : null + }); + + // remove attributes from further fulltext search + searchText = searchText.split(match[0]).join(''); + + match = attrRegex.exec(searchText); + } + + return {attrFilters, searchText}; +}; \ No newline at end of file