2018-03-23 23:08:29 -04:00
|
|
|
"use strict";
|
|
|
|
|
2019-03-20 22:28:54 +01:00
|
|
|
const repository = require('../../services/repository');
|
2020-09-08 21:47:37 +02:00
|
|
|
const SearchContext = require('../../services/search/search_context.js');
|
2019-03-20 22:28:54 +01:00
|
|
|
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-02-14 21:35:13 +01:00
|
|
|
const noteRevisionService = require("../../services/note_revisions.js");
|
2018-03-23 23:08:29 -04:00
|
|
|
|
2021-01-20 20:31:24 +01:00
|
|
|
async function search(note) {
|
2020-08-20 15:23:24 +02:00
|
|
|
let searchResultNoteIds;
|
2019-03-20 22:28:54 +01:00
|
|
|
|
2021-01-26 22:22:17 +01:00
|
|
|
const searchScript = note.getRelationValue('searchScript');
|
|
|
|
const searchString = note.getLabelValue('searchString');
|
2019-03-20 22:28:54 +01:00
|
|
|
|
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'),
|
2021-02-13 23:52:52 +01:00
|
|
|
limit: note.getLabelValue('limit'),
|
2021-02-18 22:10:49 +01:00
|
|
|
debug: note.hasLabel('debug'),
|
2021-01-26 22:22:17 +01:00
|
|
|
fuzzyAttributeSearch: false
|
|
|
|
});
|
2020-09-14 20:00:36 +02:00
|
|
|
|
2021-01-26 22:22:17 +01:00
|
|
|
searchResultNoteIds = searchService.findNotesWithQuery(searchString, searchContext)
|
|
|
|
.map(sr => sr.noteId);
|
2019-03-20 22:28:54 +01:00
|
|
|
}
|
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));
|
2021-01-20 20:31:24 +01:00
|
|
|
}
|
2019-03-20 22:28:54 +01:00
|
|
|
|
2021-01-20 20:31:24 +01:00
|
|
|
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 a search note.`]
|
|
|
|
}
|
|
|
|
|
2021-01-26 22:22:17 +01:00
|
|
|
return await search(note);
|
2019-03-20 22:28:54 +01:00
|
|
|
}
|
|
|
|
|
2021-01-20 20:31:24 +01:00
|
|
|
const ACTION_HANDLERS = {
|
|
|
|
deleteNote: (action, note) => {
|
2021-02-14 21:35:13 +01:00
|
|
|
note.isDeleted = true;
|
2021-01-20 20:31:24 +01:00
|
|
|
note.save();
|
|
|
|
},
|
2021-02-14 21:35:13 +01:00
|
|
|
deleteNoteRevisions: (action, note) => {
|
|
|
|
noteRevisionService.eraseNoteRevisions(note.getRevisions().map(rev => rev.noteRevisionId));
|
|
|
|
},
|
2021-01-20 21:45:30 +01:00
|
|
|
deleteLabel: (action, note) => {
|
|
|
|
for (const label of note.getOwnedLabels(action.labelName)) {
|
|
|
|
label.isDeleted = true;
|
|
|
|
label.save();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
deleteRelation: (action, note) => {
|
|
|
|
for (const relation of note.getOwnedRelations(action.relationName)) {
|
|
|
|
relation.isDeleted = true;
|
|
|
|
relation.save();
|
|
|
|
}
|
|
|
|
},
|
2021-01-20 20:31:24 +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();
|
|
|
|
}
|
|
|
|
},
|
2021-01-20 20:31:24 +01:00
|
|
|
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();
|
2021-01-20 20:31:24 +01:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
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) {
|
|
|
|
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 a search note.`]
|
|
|
|
}
|
|
|
|
|
|
|
|
const searchResultNoteIds = await search(note);
|
|
|
|
|
|
|
|
const actions = getActions(note);
|
|
|
|
|
|
|
|
for (const resultNoteId of searchResultNoteIds) {
|
|
|
|
const resultNote = repository.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 20:31:24 +01:00
|
|
|
|
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}`);
|
|
|
|
}
|
2021-01-20 20:31:24 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-20 15:23:24 +02:00
|
|
|
async function searchFromRelation(note, relationName) {
|
2020-06-20 12:31:38 +02:00
|
|
|
const scriptNote = note.getRelationTarget(relationName);
|
2019-03-20 22:28:54 +01:00
|
|
|
|
|
|
|
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 [];
|
|
|
|
}
|
|
|
|
|
2020-08-20 15:23:24 +02:00
|
|
|
const result = await scriptService.executeNote(scriptNote, { originEntity: note });
|
2019-03-20 22:28:54 +01:00
|
|
|
|
|
|
|
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
|
|
|
|
});
|
|
|
|
|
|
|
|
return searchService.findNotesWithQuery(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 = {
|
2021-01-18 22:52:07 +01:00
|
|
|
fastSearch: true,
|
|
|
|
includeArchivedNotes: false,
|
2020-09-13 22:23:03 +02:00
|
|
|
fuzzyAttributeSearch: false
|
|
|
|
};
|
|
|
|
|
|
|
|
const matchingNameAndValue = searchService.findNotesWithQuery(formatAttrForSearch(attr, true), new SearchContext(searchSettings));
|
|
|
|
const matchingName = searchService.findNotesWithQuery(formatAttrForSearch(attr, false), new SearchContext(searchSettings));
|
2020-06-25 23:56:06 +02:00
|
|
|
|
|
|
|
const results = [];
|
|
|
|
|
2021-02-16 23:07:40 +01:00
|
|
|
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 {
|
2021-02-16 23:07:40 +01:00
|
|
|
count: allResults.length,
|
2020-06-25 23:56:06 +02:00
|
|
|
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) {
|
2021-02-16 23:07:40 +01:00
|
|
|
if (attr.type === 'relation') {
|
|
|
|
searchStr += ".noteId";
|
|
|
|
}
|
|
|
|
|
2020-06-25 23:56:06 +02:00
|
|
|
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, '\\"') + '"';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-30 17:29:13 -04:00
|
|
|
module.exports = {
|
2020-06-25 23:56:06 +02:00
|
|
|
searchFromNote,
|
2021-01-20 20:31:24 +01:00
|
|
|
searchAndExecute,
|
2021-01-31 22:45:45 +01:00
|
|
|
getRelatedNotes,
|
|
|
|
quickSearch
|
2020-05-16 23:12:29 +02:00
|
|
|
};
|