trilium/src/routes/api/search.js

205 lines
5.4 KiB
JavaScript

"use strict";
const repository = require('../../services/repository');
const SearchContext = require('../../services/search/search_context.js');
const log = require('../../services/log');
const scriptService = require('../../services/script');
const searchService = require('../../services/search/services/search');
function searchNotes(req) {
const searchContext = new SearchContext({
includeNoteContent: req.query.includeNoteContent === 'true',
excludeArchived: req.query.excludeArchived === 'true',
fuzzyAttributeSearch: req.query.fuzzyAttributeSearch === 'true'
});
const {count, results} = searchService.searchTrimmedNotes(req.params.searchString, searchContext);
try {
return {
success: !searchContext.hasError(),
message: searchContext.getError(),
count,
results
}
}
catch {
return {
success: false
}
}
}
async function searchFromNote(req) {
const note = repository.getNote(req.params.noteId);
if (!note) {
return [404, `Note ${req.params.noteId} has not been found.`];
}
if (note.isDeleted) {
// this can be triggered from recent changes and it's harmless to return empty list rather than fail
return [];
}
if (note.type !== 'search') {
return [400, `Note ${req.params.noteId} is not search note.`]
}
const json = note.getJsonContent();
if (!json || !json.searchString) {
return [];
}
let searchResultNoteIds;
try {
if (json.searchString.startsWith('=')) {
const relationName = json.searchString.substr(1).trim();
searchResultNoteIds = await searchFromRelation(note, relationName);
} else {
const searchContext = new SearchContext({
includeNoteContent: true,
excludeArchived: true,
fuzzyAttributeSearch: false
});
searchResultNoteIds = searchService.findNotesWithQuery(json.searchString, searchContext)
.map(sr => sr.noteId);
}
}
catch (e) {
log.error(`Search failed for note ${note.noteId}: ` + e.message + ": " + e.stack);
throw new Error("Search failed, see logs for details.");
}
// we won't return search note's own noteId
// also don't allow root since that would force infinite cycle
searchResultNoteIds = searchResultNoteIds.filter(resultNoteId => !['root', note.noteId].includes(resultNoteId));
if (searchResultNoteIds.length > 200) {
searchResultNoteIds = searchResultNoteIds.slice(0, 200);
}
return searchResultNoteIds;
}
async function searchFromRelation(note, relationName) {
const scriptNote = note.getRelationTarget(relationName);
if (!scriptNote) {
log.info(`Search note's relation ${relationName} has not been found.`);
return [];
}
if (!scriptNote.isJavaScript() || scriptNote.getScriptEnv() !== 'backend') {
log.info(`Note ${scriptNote.noteId} is not executable.`);
return [];
}
if (!note.isContentAvailable) {
log.info(`Note ${scriptNote.noteId} is not available outside of protected session.`);
return [];
}
const result = await scriptService.executeNote(scriptNote, { originEntity: note });
if (!Array.isArray(result)) {
log.info(`Result from ${scriptNote.noteId} is not an array.`);
return [];
}
if (result.length === 0) {
return [];
}
// we expect either array of noteIds (strings) or notes, in that case we extract noteIds ourselves
return typeof result[0] === 'string' ? result : result.map(item => item.noteId);
}
function getRelatedNotes(req) {
const attr = req.body;
const searchSettings = {
includeNoteContent: false,
excludeArchived: true,
fuzzyAttributeSearch: false
};
const matchingNameAndValue = searchService.findNotesWithQuery(formatAttrForSearch(attr, true), new SearchContext(searchSettings));
const matchingName = searchService.findNotesWithQuery(formatAttrForSearch(attr, false), new SearchContext(searchSettings));
const results = [];
for (const record of matchingNameAndValue.concat(matchingName)) {
if (results.length >= 20) {
break;
}
if (results.find(res => res.noteId === record.noteId)) {
continue;
}
results.push(record);
}
return {
count: matchingName.length,
results
};
}
function formatAttrForSearch(attr, searchWithValue) {
let searchStr = '';
if (attr.type === 'label') {
searchStr += '#';
}
else if (attr.type === 'relation') {
searchStr += '~';
}
else {
throw new Error(`Unrecognized attribute type ${JSON.stringify(attr)}`);
}
searchStr += attr.name;
if (searchWithValue && attr.value) {
searchStr += '=';
searchStr += formatValue(attr.value);
}
return searchStr;
}
function formatValue(val) {
if (!/[^\w_-]/.test(val)) {
return val;
}
else if (!val.includes('"')) {
return '"' + val + '"';
}
else if (!val.includes("'")) {
return "'" + val + "'";
}
else if (!val.includes("`")) {
return "`" + val + "`";
}
else {
return '"' + val.replace(/"/g, '\\"') + '"';
}
}
module.exports = {
searchNotes,
searchFromNote,
getRelatedNotes
};