diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 048afe109..28639dabd 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -261,5 +261,5 @@ div.ui-tooltip { #attribute-list button { padding: 2px; - margin-right: 10px; + margin-right: 5px; } \ No newline at end of file diff --git a/src/routes/api/notes.js b/src/routes/api/notes.js index 3dc1e26dd..de39cf57c 100644 --- a/src/routes/api/notes.js +++ b/src/routes/api/notes.js @@ -58,15 +58,112 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { })); router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { - const search = '%' + utils.sanitizeSql(req.query.search) + '%'; + let {attrFilters, searchText} = parseFilters(req.query.search); - // searching in protected notes is pointless because of encryption - const noteIds = await sql.getColumn(`SELECT noteId FROM notes - WHERE isDeleted = 0 AND isProtected = 0 AND (title LIKE ? OR content LIKE ?)`, [search, search]); + const {query, params} = getSearchQuery(attrFilters, searchText); + + 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.replace(new RegExp(match[0], 'g'), ''); + + 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 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/views/index.ejs b/src/views/index.ejs index e9f911113..9c81c23ae 100644 --- a/src/views/index.ejs +++ b/src/views/index.ejs @@ -58,12 +58,11 @@ @@ -145,7 +144,7 @@
- +