2018-04-18 12:26:42 +08:00
|
|
|
const sql = require('./sql');
|
|
|
|
const sqlInit = require('./sql_init');
|
2018-04-20 12:12:01 +08:00
|
|
|
const eventService = require('./events');
|
2018-04-19 11:11:30 +08:00
|
|
|
const repository = require('./repository');
|
2018-04-20 12:12:01 +08:00
|
|
|
const protectedSessionService = require('./protected_session');
|
2018-05-26 22:24:33 +08:00
|
|
|
const utils = require('./utils');
|
2018-04-18 12:26:42 +08:00
|
|
|
|
2018-07-24 03:15:32 +08:00
|
|
|
let loaded = false;
|
2018-08-29 01:22:46 +08:00
|
|
|
let noteTitles = {};
|
|
|
|
let protectedNoteTitles = {};
|
2018-04-18 12:26:42 +08:00
|
|
|
let noteIds;
|
2018-06-05 11:21:45 +08:00
|
|
|
let childParentToBranchId = {};
|
2018-04-18 12:26:42 +08:00
|
|
|
const childToParent = {};
|
2018-08-08 22:14:35 +08:00
|
|
|
let archived = {};
|
2018-04-18 12:26:42 +08:00
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
|
|
|
|
let prefixes = {};
|
|
|
|
|
2018-04-18 12:26:42 +08:00
|
|
|
async function load() {
|
2018-05-26 22:24:33 +08:00
|
|
|
noteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 0`);
|
2018-04-18 12:26:42 +08:00
|
|
|
noteIds = Object.keys(noteTitles);
|
|
|
|
|
2018-05-26 22:24:33 +08:00
|
|
|
prefixes = await sql.getMap(`SELECT noteId || '-' || parentNoteId, prefix FROM branches WHERE prefix IS NOT NULL AND prefix != ''`);
|
2018-04-20 08:59:44 +08:00
|
|
|
|
2018-06-05 11:21:45 +08:00
|
|
|
const relations = await sql.getRows(`SELECT branchId, noteId, parentNoteId FROM branches WHERE isDeleted = 0`);
|
2018-04-18 12:26:42 +08:00
|
|
|
|
|
|
|
for (const rel of relations) {
|
|
|
|
childToParent[rel.noteId] = childToParent[rel.noteId] || [];
|
|
|
|
childToParent[rel.noteId].push(rel.parentNoteId);
|
2018-06-05 11:21:45 +08:00
|
|
|
childParentToBranchId[`${rel.noteId}-${rel.parentNoteId}`] = rel.branchId;
|
2018-04-18 12:26:42 +08:00
|
|
|
}
|
2018-04-19 12:13:55 +08:00
|
|
|
|
2018-08-08 22:14:35 +08:00
|
|
|
archived = await sql.getMap(`SELECT noteId, isInheritable FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name = 'archived'`);
|
2018-07-24 03:15:32 +08:00
|
|
|
|
|
|
|
loaded = true;
|
2018-04-18 12:26:42 +08:00
|
|
|
}
|
|
|
|
|
2018-11-07 16:35:29 +08:00
|
|
|
function highlightResults(results, allTokens) {
|
|
|
|
// we remove < signs because they can cause trouble in matching and overwriting existing highlighted chunks
|
|
|
|
// which would make the resulting HTML string invalid.
|
|
|
|
allTokens = allTokens.map(token => token.replace('/</g', ''));
|
|
|
|
|
|
|
|
// sort by the longest so we first highlight longest matches
|
|
|
|
allTokens.sort((a, b) => a.length > b.length ? -1 : 1);
|
|
|
|
|
2018-11-08 00:16:33 +08:00
|
|
|
for (const result of results) {
|
|
|
|
result.highlighted = result.title;
|
|
|
|
}
|
|
|
|
|
2018-11-07 16:35:29 +08:00
|
|
|
for (const token of allTokens) {
|
|
|
|
const tokenRegex = new RegExp("(" + utils.escapeRegExp(token) + ")", "gi");
|
|
|
|
|
|
|
|
for (const result of results) {
|
2018-11-08 00:16:33 +08:00
|
|
|
result.highlighted = result.highlighted.replace(tokenRegex, "<b>$1</b>");
|
2018-11-07 16:35:29 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-05 07:48:02 +08:00
|
|
|
function findNotes(query) {
|
2018-08-03 19:06:56 +08:00
|
|
|
if (!noteTitles || !query.length) {
|
2018-04-18 12:26:42 +08:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
2018-11-07 16:35:29 +08:00
|
|
|
// trim is necessary because even with .split() trailing spaces are tokens which causes havoc
|
2018-11-08 00:16:33 +08:00
|
|
|
// filtering '/' because it's used as separator
|
|
|
|
const allTokens = query.trim().toLowerCase().split(" ").filter(token => token !== '/');
|
2018-11-07 16:35:29 +08:00
|
|
|
const tokens = allTokens.slice();
|
2018-04-18 12:26:42 +08:00
|
|
|
const results = [];
|
|
|
|
|
2018-04-20 12:12:01 +08:00
|
|
|
let noteIds = Object.keys(noteTitles);
|
|
|
|
|
|
|
|
if (protectedSessionService.isProtectedSessionAvailable()) {
|
|
|
|
noteIds = noteIds.concat(Object.keys(protectedNoteTitles));
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const noteId of noteIds) {
|
2018-08-17 16:06:52 +08:00
|
|
|
// autocomplete should be able to find notes by their noteIds as well (only leafs)
|
|
|
|
if (noteId === query) {
|
|
|
|
search(noteId, [], [], results);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-11-21 04:22:20 +08:00
|
|
|
// for leaf note it doesn't matter if "archived" label is inheritable or not
|
2018-08-08 22:14:35 +08:00
|
|
|
if (noteId in archived) {
|
2018-04-19 12:13:55 +08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
const parents = childToParent[noteId];
|
|
|
|
if (!parents) {
|
|
|
|
continue;
|
|
|
|
}
|
2018-04-18 12:26:42 +08:00
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
for (const parentNoteId of parents) {
|
2018-08-08 22:14:35 +08:00
|
|
|
// for parent note archived needs to be inheritable
|
|
|
|
if (archived[parentNoteId] === 1) {
|
2018-05-26 22:50:13 +08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-06-05 08:22:41 +08:00
|
|
|
const title = getNoteTitle(noteId, parentNoteId).toLowerCase();
|
2018-04-20 08:59:44 +08:00
|
|
|
const foundTokens = [];
|
|
|
|
|
|
|
|
for (const token of tokens) {
|
|
|
|
if (title.includes(token)) {
|
|
|
|
foundTokens.push(token);
|
|
|
|
}
|
2018-04-18 12:26:42 +08:00
|
|
|
}
|
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
if (foundTokens.length > 0) {
|
|
|
|
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
|
2018-04-18 12:26:42 +08:00
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
search(parentNoteId, remainingTokens, [noteId], results);
|
|
|
|
}
|
2018-04-18 12:26:42 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-11-21 05:22:26 +08:00
|
|
|
// sort results by depth of the note. This is based on the assumption that more important results
|
|
|
|
// are closer to the note root.
|
2018-11-21 04:22:20 +08:00
|
|
|
results.sort((a, b) => {
|
2018-11-21 05:22:26 +08:00
|
|
|
if (a.pathArray.length === b.pathArray.length) {
|
2018-11-21 04:22:20 +08:00
|
|
|
return a.title < b.title ? -1 : 1;
|
|
|
|
}
|
|
|
|
|
2018-11-21 05:22:26 +08:00
|
|
|
return a.pathArray.length < b.pathArray.length ? -1 : 1;
|
2018-11-21 04:22:20 +08:00
|
|
|
});
|
2018-04-19 08:56:23 +08:00
|
|
|
|
2018-11-21 04:22:20 +08:00
|
|
|
const apiResults = results.slice(0, 200).map(res => {
|
|
|
|
return {
|
|
|
|
noteId: res.noteId,
|
|
|
|
branchId: res.branchId,
|
|
|
|
path: res.pathArray.join('/'),
|
|
|
|
title: res.titleArray.join(' / ')
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
highlightResults(apiResults, allTokens);
|
2018-11-07 16:35:29 +08:00
|
|
|
|
2018-11-21 04:22:20 +08:00
|
|
|
return apiResults;
|
2018-04-18 12:26:42 +08:00
|
|
|
}
|
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
function search(noteId, tokens, path, results) {
|
2018-04-19 08:56:23 +08:00
|
|
|
if (tokens.length === 0) {
|
2018-04-20 08:59:44 +08:00
|
|
|
const retPath = getSomePath(noteId, path);
|
2018-04-18 12:26:42 +08:00
|
|
|
|
2018-04-19 12:13:55 +08:00
|
|
|
if (retPath) {
|
2018-06-05 11:21:45 +08:00
|
|
|
const thisNoteId = retPath[retPath.length - 1];
|
|
|
|
const thisParentNoteId = retPath[retPath.length - 2];
|
2018-04-18 12:26:42 +08:00
|
|
|
|
2018-04-19 12:13:55 +08:00
|
|
|
results.push({
|
2018-06-05 11:21:45 +08:00
|
|
|
noteId: thisNoteId,
|
|
|
|
branchId: childParentToBranchId[`${thisNoteId}-${thisParentNoteId}`],
|
2018-11-21 04:22:20 +08:00
|
|
|
pathArray: retPath,
|
|
|
|
titleArray: getNoteTitleArrayForPath(retPath)
|
2018-04-19 12:13:55 +08:00
|
|
|
});
|
2018-04-19 08:56:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
const parents = childToParent[noteId];
|
2018-08-14 03:01:14 +08:00
|
|
|
if (!parents || noteId === 'root') {
|
2018-04-20 08:59:44 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const parentNoteId of parents) {
|
2018-08-08 22:14:35 +08:00
|
|
|
// archived must be inheritable
|
2018-08-14 03:01:14 +08:00
|
|
|
if (archived[parentNoteId] === 1) {
|
2018-04-18 12:26:42 +08:00
|
|
|
continue;
|
|
|
|
}
|
2018-05-26 22:50:13 +08:00
|
|
|
|
2018-08-14 03:01:14 +08:00
|
|
|
const title = getNoteTitle(noteId, parentNoteId).toLowerCase();
|
2018-04-18 12:26:42 +08:00
|
|
|
const foundTokens = [];
|
|
|
|
|
|
|
|
for (const token of tokens) {
|
|
|
|
if (title.includes(token)) {
|
|
|
|
foundTokens.push(token);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (foundTokens.length > 0) {
|
|
|
|
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
|
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
search(parentNoteId, remainingTokens, path.concat([noteId]), results);
|
2018-04-18 12:26:42 +08:00
|
|
|
}
|
|
|
|
else {
|
2018-04-20 08:59:44 +08:00
|
|
|
search(parentNoteId, tokens, path.concat([noteId]), results);
|
2018-04-18 12:26:42 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-05 08:22:41 +08:00
|
|
|
function getNoteTitle(noteId, parentNoteId) {
|
2018-04-20 12:12:01 +08:00
|
|
|
const prefix = prefixes[noteId + '-' + parentNoteId];
|
|
|
|
|
|
|
|
let title = noteTitles[noteId];
|
|
|
|
|
|
|
|
if (!title) {
|
|
|
|
if (protectedSessionService.isProtectedSessionAvailable()) {
|
|
|
|
title = protectedNoteTitles[noteId];
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
title = '[protected]';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return (prefix ? (prefix + ' - ') : '') + title;
|
|
|
|
}
|
|
|
|
|
2018-11-21 04:22:20 +08:00
|
|
|
function getNoteTitleArrayForPath(path) {
|
2018-04-20 08:59:44 +08:00
|
|
|
const titles = [];
|
2018-04-18 12:26:42 +08:00
|
|
|
|
2018-06-07 10:38:36 +08:00
|
|
|
if (path[0] === 'root') {
|
|
|
|
if (path.length === 1) {
|
2018-11-21 04:22:20 +08:00
|
|
|
return [ getNoteTitle('root') ];
|
2018-06-07 10:38:36 +08:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
path = path.slice(1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
let parentNoteId = 'root';
|
2018-04-18 12:26:42 +08:00
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
for (const noteId of path) {
|
2018-06-05 08:22:41 +08:00
|
|
|
const title = getNoteTitle(noteId, parentNoteId);
|
2018-04-19 12:13:55 +08:00
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
titles.push(title);
|
|
|
|
parentNoteId = noteId;
|
|
|
|
}
|
2018-04-19 12:13:55 +08:00
|
|
|
|
2018-11-21 04:22:20 +08:00
|
|
|
return titles;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getNoteTitleForPath(path) {
|
|
|
|
const titles = getNoteTitleArrayForPath(path);
|
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
return titles.join(' / ');
|
|
|
|
}
|
2018-04-19 12:13:55 +08:00
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
function getSomePath(noteId, path) {
|
|
|
|
if (noteId === 'root') {
|
2018-10-07 03:32:07 +08:00
|
|
|
path.push(noteId);
|
2018-04-20 08:59:44 +08:00
|
|
|
path.reverse();
|
2018-04-19 12:13:55 +08:00
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
return path;
|
|
|
|
}
|
2018-04-19 12:13:55 +08:00
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
const parents = childToParent[noteId];
|
|
|
|
if (!parents || parents.length === 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const parentNoteId of parents) {
|
2018-08-08 22:14:35 +08:00
|
|
|
// archived applies here only if inheritable
|
|
|
|
if (archived[parentNoteId] === 1) {
|
2018-06-04 08:42:25 +08:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-04-20 08:59:44 +08:00
|
|
|
const retPath = getSomePath(parentNoteId, path.concat([noteId]));
|
2018-04-19 12:13:55 +08:00
|
|
|
|
|
|
|
if (retPath) {
|
|
|
|
return retPath;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-06-05 07:48:02 +08:00
|
|
|
function getNotePath(noteId) {
|
2018-06-04 08:42:25 +08:00
|
|
|
const retPath = getSomePath(noteId, []);
|
|
|
|
|
|
|
|
if (retPath) {
|
|
|
|
const noteTitle = getNoteTitleForPath(retPath);
|
2018-06-06 10:47:47 +08:00
|
|
|
const parentNoteId = childToParent[noteId][0];
|
2018-06-04 08:42:25 +08:00
|
|
|
|
|
|
|
return {
|
|
|
|
noteId: noteId,
|
2018-06-06 10:47:47 +08:00
|
|
|
branchId: childParentToBranchId[`${noteId}-${parentNoteId}`],
|
2018-06-04 08:42:25 +08:00
|
|
|
title: noteTitle,
|
|
|
|
path: retPath.join('/')
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-16 04:06:49 +08:00
|
|
|
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entity}) => {
|
2018-07-24 03:15:32 +08:00
|
|
|
if (!loaded) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-19 11:11:30 +08:00
|
|
|
if (entityName === 'notes') {
|
2018-08-16 04:06:49 +08:00
|
|
|
const note = entity;
|
2018-04-19 11:11:30 +08:00
|
|
|
|
|
|
|
if (note.isDeleted) {
|
|
|
|
delete noteTitles[note.noteId];
|
|
|
|
delete childToParent[note.noteId];
|
|
|
|
}
|
|
|
|
else {
|
2018-08-29 01:22:46 +08:00
|
|
|
if (note.isProtected) {
|
|
|
|
if (protectedSessionService.isProtectedSessionAvailable()) {
|
|
|
|
protectedNoteTitles[note.noteId] = protectedSessionService.decryptNoteTitle(note.noteId, note.title);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
noteTitles[note.noteId] = note.title;
|
|
|
|
}
|
2018-04-19 11:11:30 +08:00
|
|
|
}
|
|
|
|
}
|
2018-04-20 08:59:44 +08:00
|
|
|
else if (entityName === 'branches') {
|
2018-08-16 04:06:49 +08:00
|
|
|
const branch = entity;
|
2018-04-20 08:59:44 +08:00
|
|
|
|
2018-09-01 01:55:56 +08:00
|
|
|
// first we remove records for original placement (if they exist)
|
|
|
|
childToParent[branch.noteId] = childToParent[branch.noteId] || [];
|
|
|
|
childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.origParentNoteId);
|
2018-04-20 08:59:44 +08:00
|
|
|
|
2018-09-01 01:55:56 +08:00
|
|
|
delete prefixes[branch.noteId + '-' + branch.origParentNoteId];
|
|
|
|
delete childParentToBranchId[branch.noteId + '-' + branch.origParentNoteId];
|
|
|
|
|
|
|
|
if (!branch.isDeleted) {
|
|
|
|
// ... and then we create new records
|
2018-04-20 08:59:44 +08:00
|
|
|
if (branch.prefix) {
|
|
|
|
prefixes[branch.noteId + '-' + branch.parentNoteId] = branch.prefix;
|
|
|
|
}
|
|
|
|
|
|
|
|
childToParent[branch.noteId].push(branch.parentNoteId);
|
2018-06-05 11:21:45 +08:00
|
|
|
childParentToBranchId[branch.noteId + '-' + branch.parentNoteId] = branch.branchId;
|
2018-04-20 08:59:44 +08:00
|
|
|
}
|
|
|
|
}
|
2018-08-07 19:44:51 +08:00
|
|
|
else if (entityName === 'attributes') {
|
2018-08-16 04:06:49 +08:00
|
|
|
const attribute = entity;
|
2018-04-19 12:13:55 +08:00
|
|
|
|
2018-08-07 19:44:51 +08:00
|
|
|
if (attribute.type === 'label' && attribute.name === 'archived') {
|
2018-06-08 07:26:28 +08:00
|
|
|
// we're not using label object directly, since there might be other non-deleted archived label
|
2018-08-07 19:44:51 +08:00
|
|
|
const hideLabel = await repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label'
|
|
|
|
AND name = 'archived' AND noteId = ?`, [attribute.noteId]);
|
2018-04-19 12:13:55 +08:00
|
|
|
|
|
|
|
if (hideLabel) {
|
2018-08-08 22:14:35 +08:00
|
|
|
archived[attribute.noteId] = hideLabel.isInheritable ? 1 : 0;
|
2018-04-19 12:13:55 +08:00
|
|
|
}
|
|
|
|
else {
|
2018-08-07 19:44:51 +08:00
|
|
|
delete archived[attribute.noteId];
|
2018-04-19 12:13:55 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-04-19 11:11:30 +08:00
|
|
|
});
|
|
|
|
|
2018-04-20 12:12:01 +08:00
|
|
|
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
|
2018-07-24 03:15:32 +08:00
|
|
|
if (!loaded) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-20 12:12:01 +08:00
|
|
|
protectedNoteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 1`);
|
|
|
|
|
|
|
|
for (const noteId in protectedNoteTitles) {
|
|
|
|
protectedNoteTitles[noteId] = protectedSessionService.decryptNoteTitle(noteId, protectedNoteTitles[noteId]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2018-05-26 22:24:33 +08:00
|
|
|
sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load));
|
2018-04-18 12:26:42 +08:00
|
|
|
|
|
|
|
module.exports = {
|
2018-06-05 07:48:02 +08:00
|
|
|
findNotes,
|
2018-06-07 10:38:36 +08:00
|
|
|
getNotePath,
|
|
|
|
getNoteTitleForPath
|
2018-04-18 12:26:42 +08:00
|
|
|
};
|