diff --git a/package-lock.json b/package-lock.json index ed2f68159..06d8e634f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2061,6 +2061,11 @@ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz", "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=" }, + "dayjs": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.6.tgz", + "integrity": "sha512-NLhaSS1/wWLRFy0Kn/VmsAExqll2zxRUPmPbqJoeMKQrFxG+RT94VMSE+cVljB6A76/zZkR0Xub4ihTHQ5HgGg==" + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", diff --git a/package.json b/package.json index 47feec441..332b4d3f9 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "cls-hooked": "4.2.2", "commonmark": "0.28.1", "cookie-parser": "1.4.4", + "dayjs": "^1.8.5", "debug": "4.1.1", "ejs": "2.6.1", "electron-debug": "2.1.0", diff --git a/src/services/build_search_query.js b/src/services/build_search_query.js index a7c3d3a43..8cdb5748c 100644 --- a/src/services/build_search_query.js +++ b/src/services/build_search_query.js @@ -1,3 +1,18 @@ +function isVirtualAttribute(filter) { + return ( + filter.name == "dateModified" + || filter.name == "dateCreated" + || filter.name == "isProtected" + ); +} + +function getValueForFilter(filter, i) { + return (isVirtualAttribute(filter) + ? `substr(notes.${filter.name}, 0, ${filter.value.length + 1})` + :`attribute${i}.value` + ); +} + module.exports = function(attributeFilters) { const joins = []; const joinParams = []; @@ -7,23 +22,34 @@ module.exports = function(attributeFilters) { let i = 1; for (const filter of attributeFilters) { - joins.push(`LEFT JOIN attributes AS attribute${i} ON attribute${i}.noteId = notes.noteId AND attribute${i}.name = ? AND attribute${i}.isDeleted = 0`); - joinParams.push(filter.name); + const virtual = isVirtualAttribute(filter); + + if (!virtual) { + joins.push(`LEFT JOIN attributes AS attribute${i} ` + + `ON attribute${i}.noteId = notes.noteId ` + + `AND attribute${i}.name = ? AND attribute${i}.isDeleted = 0` + ); + joinParams.push(filter.name); + } where += " " + filter.relation + " "; + // the value we need to test + const test = virtual ? filter.name : `attribute${i}.attributeId`; + if (filter.operator === 'exists') { - where += `attribute${i}.attributeId IS NOT NULL`; + where += `${test} IS NOT NULL`; } else if (filter.operator === 'not-exists') { - where += `attribute${i}.attributeId IS NULL`; + where += `${test} IS NULL`; } else if (filter.operator === '=' || filter.operator === '!=') { - where += `attribute${i}.value ${filter.operator} ?`; + where += `${getValueForFilter(filter, i)} ${filter.operator} ?`; whereParams.push(filter.value); } else if ([">", ">=", "<", "<="].includes(filter.operator)) { let floatParam; + const value = getValueForFilter(filter, i); // from https://stackoverflow.com/questions/12643009/regular-expression-for-floating-point-numbers if (/^[+-]?([0-9]*[.])?[0-9]+$/.test(filter.value)) { @@ -32,11 +58,11 @@ module.exports = function(attributeFilters) { if (floatParam === undefined || isNaN(floatParam)) { // if the value can't be parsed as float then we assume that string comparison should be used instead of numeric - where += `attribute${i}.value ${filter.operator} ?`; + where += `${value} ${filter.operator} ?`; whereParams.push(filter.value); } else { - where += `CAST(attribute${i}.value AS DECIMAL) ${filter.operator} ?`; + where += `CAST(${value} AS DECIMAL) ${filter.operator} ?`; whereParams.push(floatParam); } } @@ -52,12 +78,12 @@ module.exports = function(attributeFilters) { const query = `SELECT DISTINCT notes.noteId FROM notes ${joins.join('\r\n')} - WHERE + WHERE notes.isDeleted = 0 - AND (${where}) + AND (${where}) ${searchCondition}`; const params = joinParams.concat(whereParams).concat(searchParams); return { query, params }; -}; \ No newline at end of file +}; diff --git a/src/services/parse_filters.js b/src/services/parse_filters.js index b17ff5ae6..31285e521 100644 --- a/src/services/parse_filters.js +++ b/src/services/parse_filters.js @@ -1,8 +1,30 @@ +const dayjs = require("dayjs"); + +const labelRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i; +const smartValueRegex = /^(TODAY|NOW)((\+|\-)(\d+)(H|D|M|Y)){0,1}$/i; + +function calculateSmartValue(v) { + const normalizedV = v.toUpperCase() + "+0D"; // defaults of sorts + const [ , keyword, sign, snum, unit] = /(TODAY|NOW)(\+|\-)(\d+)(H|D|M|Y)/.exec(normalizedV); + const num = parseInt(snum); + + if (keyword != "TODAY" && keyword != "NOW") { + return v; + } + + const fullUnit = { + TODAY: { D: "days", M: "months", Y: "years" }, + NOW: { D: "days", M: "minutes", H: "hours" } + }[keyword][unit]; + + const format = keyword == "TODAY" ? "YYYY-MM-DD" : "YYYY-MM-DDTHH:mm"; + const date = (sign == "+" ? dayjs().add(num, fullUnit) : dayjs().subtract(num, fullUnit)); + + return date.format(format); +} + module.exports = function(searchText) { const labelFilters = []; - - const labelRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i; - let match = labelRegex.exec(searchText); function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; } @@ -11,11 +33,17 @@ module.exports = function(searchText) { const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and'; const operator = match[3] === '!' ? 'not-exists' : 'exists'; + const value = match[7] !== undefined ? trimQuotes(match[7]) : null + labelFilters.push({ relation: relation, name: trimQuotes(match[4]), operator: match[6] !== undefined ? match[6] : operator, - value: match[7] !== undefined ? trimQuotes(match[7]) : null + value: ( + value && value.match(smartValueRegex) + ? calculateSmartValue(value) + : value + ) }); // remove labels from further fulltext search @@ -24,5 +52,5 @@ module.exports = function(searchText) { match = labelRegex.exec(searchText); } - return {labelFilters: labelFilters, searchText}; -}; \ No newline at end of file + return { labelFilters: labelFilters, searchText }; +};