mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	implemented query language for attributes, closes #26
This commit is contained in:
		| @@ -261,5 +261,5 @@ div.ui-tooltip { | |||||||
|  |  | ||||||
| #attribute-list button { | #attribute-list button { | ||||||
|     padding: 2px; |     padding: 2px; | ||||||
|     margin-right: 10px; |     margin-right: 5px; | ||||||
| } | } | ||||||
| @@ -58,15 +58,112 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
| })); | })); | ||||||
|  |  | ||||||
| router.get('/', 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 {query, params} = getSearchQuery(attrFilters, searchText); | ||||||
|     const noteIds = await sql.getColumn(`SELECT noteId FROM notes  |  | ||||||
|               WHERE isDeleted = 0 AND isProtected = 0 AND (title LIKE ? OR content LIKE ?)`, [search, search]); |     const noteIds = await sql.getColumn(query, params); | ||||||
|  |  | ||||||
|     res.send(noteIds); |     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) => { | router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|     const sourceId = req.headers.source_id; |     const sourceId = req.headers.source_id; | ||||||
|   | |||||||
| @@ -58,12 +58,11 @@ | |||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div id="search-box" style="display: none; padding: 10px; margin-top: 10px;"> |         <div id="search-box" style="display: none; padding: 10px; margin-top: 10px;"> | ||||||
|           <p> |           <div style="display: flex; align-items: center;"> | ||||||
|             <label>Search:</label> |             <label>Search:</label> | ||||||
|             <input name="search-text" autocomplete="off"> |             <input name="search-text" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off"> | ||||||
|             <button id="reset-search-button">×</button> |             <button id="reset-search-button" class="btn btn-sm" title="Reset search">×</button> | ||||||
|             <span id="matches"></span> |           </div> | ||||||
|           </p> |  | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
| @@ -145,7 +144,7 @@ | |||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div id="attribute-list"> |       <div id="attribute-list"> | ||||||
|         <button class="btn-default btn-sm" onclick="attributesDialog.showDialog();">Attributes:</button> |         <button class="btn btn-sm" onclick="attributesDialog.showDialog();">Attributes:</button> | ||||||
|  |  | ||||||
|         <span id="attribute-list-inner"></span> |         <span id="attribute-list-inner"></span> | ||||||
|       </div> |       </div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user