Notes/src/routes/api/search.js

275 lines
8.0 KiB
JavaScript
Raw Normal View History

"use strict";
2021-05-17 22:09:49 +02:00
const becca = require('../../becca/becca.js');
2021-05-11 22:00:16 +02:00
const SearchContext = require('../../services/search/search_context');
const log = require('../../services/log');
const scriptService = require('../../services/script');
2020-08-06 23:55:17 +02:00
const searchService = require('../../services/search/services/search');
2021-05-11 22:00:16 +02:00
const noteRevisionService = require("../../services/note_revisions");
const {formatAttrForSearch} = require("../../services/attribute_formatter");
2021-04-06 20:18:34 +02:00
async function searchFromNoteInt(note) {
2020-08-20 15:23:24 +02:00
let searchResultNoteIds;
2021-01-26 22:22:17 +01:00
const searchScript = note.getRelationValue('searchScript');
const searchString = note.getLabelValue('searchString');
2021-01-26 22:22:17 +01:00
if (searchScript) {
searchResultNoteIds = await searchFromRelation(note, 'searchScript');
} else {
const searchContext = new SearchContext({
fastSearch: note.hasLabel('fastSearch'),
ancestorNoteId: note.getRelationValue('ancestor'),
2021-01-26 23:25:18 +01:00
ancestorDepth: note.getLabelValue('ancestorDepth'),
2021-01-26 22:22:17 +01:00
includeArchivedNotes: note.hasLabel('includeArchivedNotes'),
orderBy: note.getLabelValue('orderBy'),
orderDirection: note.getLabelValue('orderDirection'),
limit: note.getLabelValue('limit'),
debug: note.hasLabel('debug'),
2021-01-26 22:22:17 +01:00
fuzzyAttributeSearch: false
});
2020-09-14 20:00:36 +02:00
2021-04-17 20:52:46 +02:00
searchResultNoteIds = searchService.findResultsWithQuery(searchString, searchContext)
2021-01-26 22:22:17 +01:00
.map(sr => sr.noteId);
}
2021-01-26 22:22:17 +01:00
// we won't return search note's own noteId
// also don't allow root since that would force infinite cycle
return searchResultNoteIds.filter(resultNoteId => !['root', note.noteId].includes(resultNoteId));
}
async function searchFromNote(req) {
2021-05-02 11:23:58 +02:00
const note = becca.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 a search note.`]
}
2021-04-06 20:18:34 +02:00
return await searchFromNoteInt(note);
}
const ACTION_HANDLERS = {
deleteNote: (action, note) => {
note.markAsDeleted();
},
deleteNoteRevisions: (action, note) => {
noteRevisionService.eraseNoteRevisions(note.getNoteRevisions().map(rev => rev.noteRevisionId));
},
2021-01-20 21:45:30 +01:00
deleteLabel: (action, note) => {
for (const label of note.getOwnedLabels(action.labelName)) {
label.markAsDeleted();
2021-01-20 21:45:30 +01:00
}
},
deleteRelation: (action, note) => {
for (const relation of note.getOwnedRelations(action.relationName)) {
relation.markAsDeleted();
2021-01-20 21:45:30 +01:00
}
},
renameLabel: (action, note) => {
for (const label of note.getOwnedLabels(action.oldLabelName)) {
label.name = action.newLabelName;
label.save();
}
},
2021-01-20 21:45:30 +01:00
renameRelation: (action, note) => {
for (const relation of note.getOwnedRelations(action.oldRelationName)) {
relation.name = action.newRelationName;
relation.save();
}
},
setLabelValue: (action, note) => {
note.setLabel(action.labelName, action.labelValue);
2021-01-20 21:45:30 +01:00
},
setRelationTarget: (action, note) => {
note.setRelation(action.relationName, action.targetNoteId);
},
executeScript: (action, note) => {
if (!action.script || !action.script.trim()) {
log.info("Ignoring executeScript since the script is empty.")
return;
}
const scriptFunc = new Function("note", action.script);
scriptFunc(note);
note.save();
}
};
function getActions(note) {
return note.getLabels('action')
.map(actionLabel => {
let action;
try {
action = JSON.parse(actionLabel.value);
} catch (e) {
log.error(`Cannot parse '${actionLabel.value}' into search action, skipping.`);
return null;
}
if (!(action.name in ACTION_HANDLERS)) {
log.error(`Cannot find '${action.name}' search action handler, skipping.`);
return null;
}
return action;
})
.filter(a => !!a);
}
async function searchAndExecute(req) {
2021-05-02 11:23:58 +02:00
const note = becca.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 a search note.`]
}
2021-04-06 20:18:34 +02:00
const searchResultNoteIds = await searchFromNoteInt(note);
const actions = getActions(note);
for (const resultNoteId of searchResultNoteIds) {
2021-05-02 11:23:58 +02:00
const resultNote = becca.getNote(resultNoteId);
if (!resultNote || resultNote.isDeleted) {
continue;
}
for (const action of actions) {
2021-01-20 21:45:30 +01:00
try {
log.info(`Applying action handler to note ${resultNote.noteId}: ${JSON.stringify(action)}`);
2021-01-20 21:45:30 +01:00
ACTION_HANDLERS[action.name](action, resultNote);
}
catch (e) {
log.error(`ExecuteScript search action failed with ${e.message}`);
}
}
}
}
function searchFromRelation(note, relationName) {
2020-06-20 12:31:38 +02:00
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 [];
}
2021-05-17 22:35:36 +02:00
if (!note.isContentAvailable()) {
log.info(`Note ${scriptNote.noteId} is not available outside of protected session.`);
return [];
}
const result = 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);
}
2021-01-31 22:45:45 +01:00
function quickSearch(req) {
const {searchString} = req.params;
const searchContext = new SearchContext({
fastSearch: false,
includeArchivedNotes: false,
fuzzyAttributeSearch: false
});
2021-04-17 20:52:46 +02:00
return searchService.findResultsWithQuery(searchString, searchContext)
2021-01-31 22:45:45 +01:00
.map(sr => sr.noteId);
}
function search(req) {
const {searchString} = req.params;
const searchContext = new SearchContext({
fastSearch: false,
includeArchivedNotes: true,
fuzzyAttributeSearch: false,
ignoreHoistedNote: true
});
2021-04-17 20:52:46 +02:00
return searchService.findResultsWithQuery(searchString, searchContext)
.map(sr => sr.noteId);
}
2020-06-25 23:56:06 +02:00
function getRelatedNotes(req) {
const attr = req.body;
2020-09-13 22:23:03 +02:00
const searchSettings = {
fastSearch: true,
includeArchivedNotes: false,
2020-09-13 22:23:03 +02:00
fuzzyAttributeSearch: false
};
2021-04-17 20:52:46 +02:00
const matchingNameAndValue = searchService.findResultsWithQuery(formatAttrForSearch(attr, true), new SearchContext(searchSettings));
const matchingName = searchService.findResultsWithQuery(formatAttrForSearch(attr, false), new SearchContext(searchSettings));
2020-06-25 23:56:06 +02:00
const results = [];
const allResults = matchingNameAndValue.concat(matchingName);
for (const record of allResults) {
2020-06-25 23:56:06 +02:00
if (results.length >= 20) {
break;
}
if (results.find(res => res.noteId === record.noteId)) {
continue;
}
results.push(record);
}
return {
count: allResults.length,
2020-06-25 23:56:06 +02:00
results
};
}
module.exports = {
2020-06-25 23:56:06 +02:00
searchFromNote,
searchAndExecute,
2021-01-31 22:45:45 +01:00
getRelatedNotes,
quickSearch,
search
2020-05-16 23:12:29 +02:00
};