From 62650a4545299f546f7b5f3bffb671103e2071b7 Mon Sep 17 00:00:00 2001 From: zadam Date: Sat, 16 Mar 2019 22:19:01 +0100 Subject: [PATCH] refactoring of router search code into service --- src/routes/api/search.js | 13 +++---------- src/services/app_icon.js | 2 +- src/services/build_search_query.js | 15 ++++++++------- src/services/parse_filters.js | 20 +++++++++++++++----- src/services/search.js | 25 +++++++++++++++++++++++++ src/services/utils.js | 13 ++++++++++++- 6 files changed, 64 insertions(+), 24 deletions(-) create mode 100644 src/services/search.js diff --git a/src/routes/api/search.js b/src/routes/api/search.js index e86eb1505..4b605e416 100644 --- a/src/routes/api/search.js +++ b/src/routes/api/search.js @@ -1,20 +1,13 @@ "use strict"; -const sql = require('../../services/sql'); -const utils = require('../../services/utils'); const noteService = require('../../services/notes'); const noteCacheService = require('../../services/note_cache'); -const parseFilters = require('../../services/parse_filters'); -const buildSearchQuery = require('../../services/build_search_query'); +const searchService = require('../../services/search'); async function searchNotes(req) { - const filters = parseFilters(req.params.searchString); + const noteIds = await searchService.searchForNoteIds(req.params.searchString); - const {query, params} = buildSearchQuery(filters); - - const labelFiltersNoteIds = await sql.getColumn(query, params); - - return labelFiltersNoteIds.map(noteCacheService.getNotePath).filter(res => !!res); + return noteIds.map(noteCacheService.getNotePath).filter(res => !!res); } async function saveSearchToNote(req) { diff --git a/src/services/app_icon.js b/src/services/app_icon.js index 9c6c0f3be..07d031418 100644 --- a/src/services/app_icon.js +++ b/src/services/app_icon.js @@ -55,7 +55,7 @@ function getDesktopFileContent() { } function escapePath(path) { - return path.replace(" ", "\\ "); + return path.replace(/ /g, "\\ "); } function getExePath() { diff --git a/src/services/build_search_query.js b/src/services/build_search_query.js index 69c299368..efd678bc9 100644 --- a/src/services/build_search_query.js +++ b/src/services/build_search_query.js @@ -2,7 +2,7 @@ const utils = require('./utils'); const VIRTUAL_ATTRIBUTES = ["dateCreated", "dateCreated", "dateModified", "utcDateCreated", "utcDateModified", "isProtected", "title", "content", "type", "mime", "text"]; -module.exports = function(filters) { +module.exports = function(filters, selectedColumns = 'notes.*') { // alias => join const joins = { "notes": null @@ -54,7 +54,7 @@ module.exports = function(filters) { if (orderByFilter) { orderBy = orderByFilter.value.split(",").map(prop => { const direction = prop.includes("-") ? "DESC" : "ASC"; - const cleanedProp = prop.trim().replace("-", ""); + const cleanedProp = prop.trim().replace(/-/g, ""); return getAccessor(cleanedProp) + " " + direction; }); @@ -101,17 +101,17 @@ module.exports = function(filters) { else if (filter.operator === '*=' || filter.operator === '!*=') { where += `${accessor}` + (filter.operator.includes('!') ? ' NOT' : '') - + ` LIKE '%` + filter.value + "'"; // FIXME: escaping + + ` LIKE ` + utils.prepareSqlForLike('%', filter.value, ''); } else if (filter.operator === '=*' || filter.operator === '!=*') { where += `${accessor}` + (filter.operator.includes('!') ? ' NOT' : '') - + ` LIKE '` + filter.value + "%'"; // FIXME: escaping + + ` LIKE '` + utils.prepareSqlForLike('', filter.value, '%'); } else if (filter.operator === '*=*' || filter.operator === '!*=*') { where += `${accessor}` + (filter.operator.includes('!') ? ' NOT' : '') - + ` LIKE '%` + filter.value + "%'"; // FIXME: escaping + + ` LIKE ` + utils.prepareSqlForLike('%', filter.value, '%'); } else if ([">", ">=", "<", "<="].includes(filter.operator)) { let floatParam; @@ -141,12 +141,13 @@ module.exports = function(filters) { orderBy.push("notes.title"); } - const query = `SELECT DISTINCT notes.noteId FROM notes + const query = `SELECT ${selectedColumns} FROM notes ${Object.values(joins).join('\r\n')} WHERE notes.isDeleted = 0 AND (${where}) - ORDER BY ` + orderBy.join(", "); + GROUP BY notes.noteId + ORDER BY ${orderBy.join(", ")}`; console.log(query); console.log(params); diff --git a/src/services/parse_filters.js b/src/services/parse_filters.js index 9c8fe85c5..c1d94679a 100644 --- a/src/services/parse_filters.js +++ b/src/services/parse_filters.js @@ -10,7 +10,7 @@ function calculateSmartValue(v) { } const keyword = match[1].toUpperCase(); - const num = match[2] ? parseInt(match[2].replace(" ", "")) : 0; // can contain spaces between sign and digits + const num = match[2] ? parseInt(match[2].replace(/ /g, "")) : 0; // can contain spaces between sign and digits let format, date; @@ -23,8 +23,8 @@ function calculateSmartValue(v) { format = "YYYY-MM-DD"; } else if (keyword === 'WEEK') { - // FIXME - //date = dayjs().add(num, 'day'); + // FIXME: this will always use sunday as start of the week + date = dayjs().startOf('week').add(7 * num, 'day'); format = "YYYY-MM-DD"; } else if (keyword === 'MONTH') { @@ -43,6 +43,18 @@ function calculateSmartValue(v) { } module.exports = function (searchText) { + // if the string doesn't start with attribute then we consider it as just standard full text search + if (!searchText.trim().startsWith("@")) { + return [ + { + relation: 'and', + name: 'text', + operator: '=', + value: searchText + } + ] + } + const filters = []; function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; } @@ -67,7 +79,5 @@ module.exports = function (searchText) { }); } - console.log(filters); - return filters; }; diff --git a/src/services/search.js b/src/services/search.js new file mode 100644 index 000000000..057c357c5 --- /dev/null +++ b/src/services/search.js @@ -0,0 +1,25 @@ +const repository = require('./repository'); +const sql = require('./sql'); +const parseFilters = require('./parse_filters'); +const buildSearchQuery = require('./build_search_query'); + +async function searchForNotes(searchString) { + const filters = parseFilters(searchString); + + const {query, params} = buildSearchQuery(filters); + + return await repository.getEntities(query, params); +} + +async function searchForNoteIds(searchString) { + const filters = parseFilters(searchString); + + const {query, params} = buildSearchQuery(filters, 'notes.noteId'); + + return await sql.getColumn(query, params); +} + +module.exports = { + searchForNotes, + searchForNoteIds +}; \ No newline at end of file diff --git a/src/services/utils.js b/src/services/utils.js index a0c9c7fa3..b5b9e3684 100644 --- a/src/services/utils.js +++ b/src/services/utils.js @@ -50,7 +50,17 @@ function isEmptyOrWhitespace(str) { function sanitizeSql(str) { // should be improved or usage eliminated - return str.replace(/'/g, "\\'"); + return str.replace(/'/g, "''"); +} + +function prepareSqlForLike(prefix, str, suffix) { + const value = str + .replace(/\\/g, "\\\\") + .replace(/'/g, "''") + .replace(/_/g, "\\_") + .replace(/%/g, "\\%"); + + return `'${prefix}${value}${suffix}' ESCAPE '\\'`; } async function stopWatch(what, func) { @@ -156,6 +166,7 @@ module.exports = { hash, isEmptyOrWhitespace, sanitizeSql, + prepareSqlForLike, stopWatch, escapeHtml, unescapeHtml,