Compare commits

...

25 Commits

Author SHA1 Message Date
azivner
cb69914f09 release 0.13.0-beta 2018-05-22 23:51:43 -04:00
azivner
a372cbb2df fix #105 2018-05-22 23:51:13 -04:00
azivner
0ce5caefe8 refactoring 2018-05-22 22:22:15 -04:00
azivner
94dabb81f6 fix sync of unsyncable options 2018-05-22 19:29:18 -04:00
azivner
cd45bcfd03 converted option operations to repository 2018-05-22 00:22:43 -04:00
azivner
49a53f7a45 added hash columns for faster sync check calculation 2018-05-22 00:15:54 -04:00
azivner
9fa6c0918c add index for note's type + some fixes 2018-05-21 20:12:46 -04:00
azivner
e8d089e37e ckeditor 10.0.0 2018-05-21 19:35:49 -04:00
azivner
a931ce25fa attempt to fix the hoek security warning with package upgrade 2018-05-21 16:08:34 -04:00
azivner
b507abb4f7 electron upgrade to 2.0.0 2018-05-08 16:39:01 -04:00
azivner
66e7c6de62 fix ordering 2018-04-21 12:23:35 -04:00
azivner
4ce5ea9886 autocomplete supports encrypted notes now as well 2018-04-20 00:12:01 -04:00
azivner
8c54b62f07 fix protect branch 2018-04-19 22:18:19 -04:00
azivner
85eb50ed0f autocomplete with prefixes 2018-04-19 20:59:44 -04:00
azivner
5ffd621e9d autocomplete respects hideInAutocomplete label 2018-04-19 00:13:55 -04:00
azivner
df93cb09da fix hide-toggle 2018-04-18 23:13:37 -04:00
azivner
bbf04209f0 autocomplete cache gets updated with note update 2018-04-18 23:11:30 -04:00
azivner
834bfa39c7 limit number of results to 200, other tweaks 2018-04-18 20:56:23 -04:00
azivner
52b445f70b Merge branch 'stable' 2018-04-18 20:22:16 -04:00
azivner
7b9b4fbb0c backend autocomplete WIP 2018-04-18 00:26:42 -04:00
azivner
5af0ba1fcb layout fixes 2018-04-17 20:04:27 -04:00
azivner
85a9748291 fix for clones & optimizations 2018-04-16 23:34:56 -04:00
azivner
b4005a7ffe optimizations to the lazy loading - expanding tree now takes only one request 2018-04-16 23:13:33 -04:00
azivner
82de1c88d4 basic lazy loading of tree now works, still WIP 2018-04-16 20:40:18 -04:00
azivner
1687ed7e0b load only expanded tree with the rest being lazy loaded, WIP 2018-04-16 16:26:47 -04:00
52 changed files with 5802 additions and 3702 deletions

View File

@@ -388,10 +388,14 @@ imageId</ColNames>
<column id="91" parent="13" name="title"> <column id="91" parent="13" name="title">
<Position>2</Position> <Position>2</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;unnamed&quot;</DefaultExpression>
</column> </column>
<column id="92" parent="13" name="content"> <column id="92" parent="13" name="content">
<Position>3</Position> <Position>3</Position>
<DataType>TEXT|0s</DataType> <DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column> </column>
<column id="93" parent="13" name="isProtected"> <column id="93" parent="13" name="isProtected">
<Position>4</Position> <Position>4</Position>

View File

@@ -1,3 +1,4 @@
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('root', 'root', 'none', 0, null, 1, 0, '2018-01-01T00:00:00.000Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z'); INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z'); INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z');
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z'); INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z');

View File

@@ -0,0 +1,5 @@
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, dateModified)
VALUES ('root', 'root', 'none', 0, null, 1, '2018-01-01T00:00:00.000Z');
INSERT INTO sync (entityName, entityId, sourceId, syncDate)
VALUES ('branches' ,'root', 'SYNC_FILL', '2018-01-01T00:00:00.000Z');

View File

@@ -0,0 +1 @@
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);

View File

@@ -0,0 +1,2 @@
-- index confuses planner and is mostly useless anyway since we're mostly used in non-deleted notes (which are presumably majority)
DROP INDEX IDX_notes_isDeleted;

View File

@@ -0,0 +1,2 @@
create index IDX_notes_type
on notes (type);

View File

@@ -0,0 +1,9 @@
ALTER TABLE notes ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE branches ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE note_revisions ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE recent_notes ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE options ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE note_images ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE images ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE labels ADD hash TEXT DEFAULT "" NOT NULL;
ALTER TABLE api_tokens ADD hash TEXT DEFAULT "" NOT NULL;

8503
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{ {
"name": "trilium", "name": "trilium",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.12.0", "version": "0.13.0-beta",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"repository": { "repository": {
@@ -9,11 +9,11 @@
"url": "https://github.com/zadam/trilium.git" "url": "https://github.com/zadam/trilium.git"
}, },
"scripts": { "scripts": {
"start": "node ./bin/www", "start": "node ./src/www",
"test-electron": "xo", "test-electron": "xo",
"rebuild-electron": "electron-rebuild", "rebuild-electron": "electron-rebuild",
"start-electron": "electron . --disable-gpu", "start-electron": "electron . --disable-gpu",
"build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64", "build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64 --app-version=",
"start-forge": "electron-forge start", "start-forge": "electron-forge start",
"package-forge": "electron-forge package", "package-forge": "electron-forge package",
"make-forge": "electron-forge make", "make-forge": "electron-forge make",
@@ -21,22 +21,20 @@
}, },
"dependencies": { "dependencies": {
"async-mutex": "^0.1.3", "async-mutex": "^0.1.3",
"axios": "^0.17.1", "axios": "^0.18",
"body-parser": "~1.18.2", "body-parser": "^1.18.3",
"cls-hooked": "^4.2.2", "cls-hooked": "^4.2.2",
"cookie-parser": "~1.4.3", "cookie-parser": "~1.4.3",
"debug": "~3.1.0", "debug": "~3.1.0",
"devtron": "^1.4.0", "devtron": "^1.4.0",
"ejs": "~2.5.7", "ejs": "~2.6.1",
"electron": "^2.0.0-beta.5",
"electron-debug": "^1.5.0", "electron-debug": "^1.5.0",
"electron-dl": "^1.11.0", "electron-dl": "^1.12.0",
"electron-in-page-search": "^1.2.4", "electron-in-page-search": "^1.3.2",
"electron-rebuild": "^1.7.3",
"express": "~4.16.3", "express": "~4.16.3",
"express-session": "^1.15.6", "express-session": "^1.15.6",
"fs-extra": "^4.0.3", "fs-extra": "^6.0.1",
"helmet": "^3.12.0", "helmet": "^3.12.1",
"html": "^1.0.0", "html": "^1.0.0",
"image-type": "^3.0.0", "image-type": "^3.0.0",
"imagemin": "^5.3.1", "imagemin": "^5.3.1",
@@ -45,30 +43,33 @@
"imagemin-pngquant": "^5.1.0", "imagemin-pngquant": "^5.1.0",
"ini": "^1.3.5", "ini": "^1.3.5",
"jimp": "^0.2.28", "jimp": "^0.2.28",
"moment": "^2.21.0", "moment": "^2.22.1",
"multer": "^1.3.0", "multer": "^1.3.0",
"open": "0.0.5", "open": "0.0.5",
"rand-token": "^0.4.0", "rand-token": "^0.4.0",
"request": "^2.85.0", "rcedit": "^1.1.0",
"request": "^2.87.0",
"request-promise": "^4.2.2", "request-promise": "^4.2.2",
"rimraf": "^2.6.2", "rimraf": "^2.6.2",
"sanitize-filename": "^1.6.1", "sanitize-filename": "^1.6.1",
"scrypt": "^6.0.3", "scrypt": "^6.0.3",
"serve-favicon": "~2.4.5", "serve-favicon": "~2.5.0",
"session-file-store": "^1.2.0", "session-file-store": "^1.2.0",
"simple-node-logger": "^0.93.37", "simple-node-logger": "^0.93.37",
"sqlite": "^2.9.1", "sqlite": "^2.9.2",
"tar-stream": "^1.5.5", "tar-stream": "^1.6.1",
"unescape": "^1.0.1", "unescape": "^1.0.1",
"ws": "^3.3.3" "ws": "^5.2.0"
}, },
"devDependencies": { "devDependencies": {
"electron": "^2.0.1",
"electron-compile": "^6.4.2", "electron-compile": "^6.4.2",
"electron-packager": "^11.1.0", "electron-packager": "^12.1.0",
"electron-prebuilt-compile": "2.0.0-beta.5", "electron-prebuilt-compile": "2.0.0",
"electron-rebuild": "^1.7.3",
"lorem-ipsum": "^1.0.4", "lorem-ipsum": "^1.0.4",
"tape": "^4.9.0", "tape": "^4.9.0",
"xo": "^0.18.0" "xo": "^0.21.1"
}, },
"config": { "config": {
"forge": { "forge": {

View File

@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
class ApiToken extends Entity { class ApiToken extends Entity {
static get tableName() { return "api_tokens"; } static get tableName() { return "api_tokens"; }
static get primaryKeyName() { return "apiTokenId"; } static get primaryKeyName() { return "apiTokenId"; }
static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
beforeSaving() { beforeSaving() {
super.beforeSaving(); super.beforeSaving();

View File

@@ -8,6 +8,8 @@ const sql = require('../services/sql');
class Branch extends Entity { class Branch extends Entity {
static get tableName() { return "branches"; } static get tableName() { return "branches"; }
static get primaryKeyName() { return "branchId"; } static get primaryKeyName() { return "branchId"; }
// notePosition is not part of hash because it would produce a lot of updates in case of reordering
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "dateModified", "isDeleted", "prefix"]; }
async getNote() { async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);

View File

@@ -14,6 +14,17 @@ class Entity {
if (!this[this.constructor.primaryKeyName]) { if (!this[this.constructor.primaryKeyName]) {
this[this.constructor.primaryKeyName] = utils.newEntityId(); this[this.constructor.primaryKeyName] = utils.newEntityId();
} }
let contentToHash = "";
for (const propertyName of this.constructor.hashedProperties) {
contentToHash += "|" + this[propertyName];
}
// this IF is to ease the migration from before hashed options, can be later removed
if (this.constructor.tableName !== 'options' || this.isSynced) {
this["hash"] = utils.hash(contentToHash).substr(0, 10);
}
} }
async save() { async save() {

View File

@@ -6,6 +6,7 @@ const Branch = require('../entities/branch');
const Label = require('../entities/label'); const Label = require('../entities/label');
const RecentNote = require('../entities/recent_note'); const RecentNote = require('../entities/recent_note');
const ApiToken = require('../entities/api_token'); const ApiToken = require('../entities/api_token');
const Option = require('../entities/option');
const repository = require('../services/repository'); const repository = require('../services/repository');
function createEntityFromRow(row) { function createEntityFromRow(row) {
@@ -35,6 +36,9 @@ function createEntityFromRow(row) {
else if (row.noteId) { else if (row.noteId) {
entity = new Note(row); entity = new Note(row);
} }
else if (row.name) {
entity = new Option(row);
}
else { else {
throw new Error('Unknown entity type for row: ' + JSON.stringify(row)); throw new Error('Unknown entity type for row: ' + JSON.stringify(row));
} }

View File

@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
class Image extends Entity { class Image extends Entity {
static get tableName() { return "images"; } static get tableName() { return "images"; }
static get primaryKeyName() { return "imageId"; } static get primaryKeyName() { return "imageId"; }
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; }
beforeSaving() { beforeSaving() {
super.beforeSaving(); super.beforeSaving();

View File

@@ -8,6 +8,7 @@ const sql = require('../services/sql');
class Label extends Entity { class Label extends Entity {
static get tableName() { return "labels"; } static get tableName() { return "labels"; }
static get primaryKeyName() { return "labelId"; } static get primaryKeyName() { return "labelId"; }
static get hashedProperties() { return ["labelId", "noteId", "name", "value", "dateModified", "dateCreated"]; }
async getNote() { async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);

View File

@@ -1,20 +1,21 @@
"use strict"; "use strict";
const Entity = require('./entity'); const Entity = require('./entity');
const protected_session = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository'); const repository = require('../services/repository');
const dateUtils = require('../services/date_utils'); const dateUtils = require('../services/date_utils');
class Note extends Entity { class Note extends Entity {
static get tableName() { return "notes"; } static get tableName() { return "notes"; }
static get primaryKeyName() { return "noteId"; } static get primaryKeyName() { return "noteId"; }
static get hashedProperties() { return ["noteId", "title", "content", "type", "dateModified", "isProtected", "isDeleted"]; }
constructor(row) { constructor(row) {
super(row); super(row);
// check if there's noteId, otherwise this is a new entity which wasn't encrypted yet // check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
if (this.isProtected && this.noteId) { if (this.isProtected && this.noteId) {
protected_session.decryptNote(this); protectedSessionService.decryptNote(this);
} }
this.setContent(this.content); this.setContent(this.content);
@@ -146,7 +147,7 @@ class Note extends Entity {
} }
if (this.isProtected) { if (this.isProtected) {
protected_session.encryptNote(this); protectedSessionService.encryptNote(this);
} }
if (!this.isDeleted) { if (!this.isDeleted) {

View File

@@ -7,6 +7,7 @@ const dateUtils = require('../services/date_utils');
class NoteImage extends Entity { class NoteImage extends Entity {
static get tableName() { return "note_images"; } static get tableName() { return "note_images"; }
static get primaryKeyName() { return "noteImageId"; } static get primaryKeyName() { return "noteImageId"; }
static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateModified", "dateCreated"]; }
async getNote() { async getNote() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]); return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);

View File

@@ -1,19 +1,19 @@
"use strict"; "use strict";
const Entity = require('./entity'); const Entity = require('./entity');
const protected_session = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
const utils = require('../services/utils');
const repository = require('../services/repository'); const repository = require('../services/repository');
class NoteRevision extends Entity { class NoteRevision extends Entity {
static get tableName() { return "note_revisions"; } static get tableName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; } static get primaryKeyName() { return "noteRevisionId"; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; }
constructor(row) { constructor(row) {
super(row); super(row);
if (this.isProtected) { if (this.isProtected) {
protected_session.decryptNoteRevision(this); protectedSessionService.decryptNoteRevision(this);
} }
} }
@@ -25,7 +25,7 @@ class NoteRevision extends Entity {
super.beforeSaving(); super.beforeSaving();
if (this.isProtected) { if (this.isProtected) {
protected_session.encryptNoteRevision(this); protectedSessionService.encryptNoteRevision(this);
} }
} }
} }

18
src/entities/option.js Normal file
View File

@@ -0,0 +1,18 @@
"use strict";
const Entity = require('./entity');
const dateUtils = require('../services/date_utils');
class Option extends Entity {
static get tableName() { return "options"; }
static get primaryKeyName() { return "name"; }
static get hashedProperties() { return ["name", "value"]; }
beforeSaving() {
super.beforeSaving();
this.dateModified = dateUtils.nowDate();
}
}
module.exports = Option;

View File

@@ -5,6 +5,7 @@ const Entity = require('./entity');
class RecentNote extends Entity { class RecentNote extends Entity {
static get tableName() { return "recent_notes"; } static get tableName() { return "recent_notes"; }
static get primaryKeyName() { return "branchId"; } static get primaryKeyName() { return "branchId"; }
static get hashedProperties() { return ["branchId", "notePath", "dateAccessed", "isDeleted"]; }
} }
module.exports = RecentNote; module.exports = RecentNote;

View File

@@ -25,7 +25,7 @@ async function showDialog() {
$treePrefixInput.val(branch.prefix).focus(); $treePrefixInput.val(branch.prefix).focus();
const noteTitle = treeUtils.getNoteTitle(currentNode.data.noteId); const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId);
$noteTitle.html(noteTitle); $noteTitle.html(noteTitle);
} }

View File

@@ -22,7 +22,7 @@ async function showDialog() {
const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded)); const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
if (event.noteId) { if (event.noteId) {
const noteLink = linkService.createNoteLink(event.noteId).prop('outerHTML'); const noteLink = await linkService.createNoteLink(event.noteId).prop('outerHTML');
event.comment = event.comment.replace('<note>', noteLink); event.comment = event.comment.replace('<note>', noteLink);
} }

View File

@@ -1,7 +1,6 @@
import treeService from '../services/tree.js'; import treeService from '../services/tree.js';
import linkService from '../services/link.js'; import linkService from '../services/link.js';
import utils from '../services/utils.js'; import server from '../services/server.js';
import autocompleteService from '../services/autocomplete.js';
const $dialog = $("#jump-to-note-dialog"); const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete"); const $autoComplete = $("#jump-to-note-autocomplete");
@@ -18,8 +17,12 @@ async function showDialog() {
}); });
await $autoComplete.autocomplete({ await $autoComplete.autocomplete({
source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems), source: async function(request, response) {
minLength: 1 const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
response(result);
},
minLength: 2
}); });
} }

View File

@@ -40,7 +40,7 @@ async function showDialog() {
noteLink = change.current_title; noteLink = change.current_title;
} }
else { else {
noteLink = linkService.createNoteLink(change.noteId, change.title); noteLink = await linkService.createNoteLink(change.noteId, change.title);
} }
changesListEl.append($('<li>') changesListEl.append($('<li>')

View File

@@ -14,13 +14,15 @@ class NoteShort {
} }
async getBranches() { async getBranches() {
const branches = []; const branchIds = this.treeCache.parents[this.noteId].map(
parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
for (const parent of this.treeCache.parents[this.noteId]) { return this.treeCache.getBranches(branchIds);
branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId)); }
}
return branches; hasChildren() {
return this.treeCache.children[this.noteId]
&& this.treeCache.children[this.noteId].length > 0;
} }
async getChildBranches() { async getChildBranches() {
@@ -28,23 +30,28 @@ class NoteShort {
return []; return [];
} }
const branches = []; const branchIds = this.treeCache.children[this.noteId].map(
childNoteId => this.treeCache.getBranchIdByChildParent(childNoteId, this.noteId));
for (const child of this.treeCache.children[this.noteId]) { return await this.treeCache.getBranches(branchIds);
branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId));
}
return branches;
} }
async getParentNotes() { getParentNoteIds() {
return this.treeCache.parents[this.noteId] || []; return this.treeCache.parents[this.noteId] || [];
} }
async getChildNotes() { async getParentNotes() {
return await this.treeCache.getNotes(this.getParentNoteIds());
}
getChildNoteIds() {
return this.treeCache.children[this.noteId] || []; return this.treeCache.children[this.noteId] || [];
} }
async getChildNotes() {
return await this.treeCache.getNotes(this.getChildNoteIds());
}
get toString() { get toString() {
return `Note(noteId=${this.noteId}, title=${this.title})`; return `Note(noteId=${this.noteId}, title=${this.title})`;
} }

View File

@@ -23,11 +23,11 @@ function getNodePathFromLabel(label) {
return null; return null;
} }
function createNoteLink(notePath, noteTitle) { async function createNoteLink(notePath, noteTitle) {
if (!noteTitle) { if (!noteTitle) {
const noteId = treeUtils.getNoteIdFromNotePath(notePath); const noteId = treeUtils.getNoteIdFromNotePath(notePath);
noteTitle = treeUtils.getNoteTitle(noteId); noteTitle = await treeUtils.getNoteTitle(noteId);
} }
const noteLink = $("<a>", { const noteLink = $("<a>", {

View File

@@ -203,7 +203,7 @@ async function showParentList(noteId, node) {
item = $("<span/>").attr("title", "Current note").append(title); item = $("<span/>").attr("title", "Current note").append(title);
} }
else { else {
item = linkService.createNoteLink(notePath, title); item = await linkService.createNoteLink(notePath, title);
} }
$parentListList.append($("<li/>").append(item)); $parentListList.append($("<li/>").append(item));
@@ -285,14 +285,14 @@ async function treeInitialized() {
} }
} }
function initFancyTree(branch) { function initFancyTree(tree) {
utils.assertArguments(branch); utils.assertArguments(tree);
$tree.fancytree({ $tree.fancytree({
autoScroll: true, autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"], extensions: ["hotkeys", "filter", "dnd", "clones"],
source: branch, source: tree,
scrollParent: $tree, scrollParent: $tree,
click: (event, data) => { click: (event, data) => {
const targetType = data.targetType; const targetType = data.targetType;
@@ -375,7 +375,7 @@ async function loadTree() {
startNotePath = getNotePathFromAddress(); startNotePath = getNotePathFromAddress();
} }
return await treeBuilder.prepareTree(resp.notes, resp.branches); return await treeBuilder.prepareTree(resp.notes, resp.branches, resp.relations);
} }
function collapseTree(node = null) { function collapseTree(node = null) {

View File

@@ -5,10 +5,10 @@ import server from "./server.js";
import treeCache from "./tree_cache.js"; import treeCache from "./tree_cache.js";
import messagingService from "./messaging.js"; import messagingService from "./messaging.js";
async function prepareTree(noteRows, branchRows) { async function prepareTree(noteRows, branchRows, relations) {
utils.assertArguments(noteRows); utils.assertArguments(noteRows, branchRows, relations);
treeCache.load(noteRows, branchRows); treeCache.load(noteRows, branchRows, relations);
return await prepareRealBranch(await treeCache.getNote('root')); return await prepareRealBranch(await treeCache.getNote('root'));
} }
@@ -49,9 +49,7 @@ async function prepareRealBranch(parentNote) {
expanded: note.type !== 'search' && branch.isExpanded expanded: note.type !== 'search' && branch.isExpanded
}; };
const hasChildren = (await note.getChildNotes()).length > 0; if (note.hasChildren() || note.type === 'search') {
if (hasChildren || note.type === 'search') {
node.folder = true; node.folder = true;
if (node.expanded && note.type !== 'search') { if (node.expanded && note.type !== 'search') {
@@ -96,7 +94,7 @@ async function getExtraClasses(note) {
extraClasses.push("protected"); extraClasses.push("protected");
} }
if ((await note.getParentNotes()).length > 1) { if (note.getParentNoteIds().length > 1) {
extraClasses.push("multiple-parents"); extraClasses.push("multiple-parents");
} }

View File

@@ -2,45 +2,85 @@ import utils from "./utils.js";
import Branch from "../entities/branch.js"; import Branch from "../entities/branch.js";
import NoteShort from "../entities/note_short.js"; import NoteShort from "../entities/note_short.js";
import infoService from "./info.js"; import infoService from "./info.js";
import server from "./server.js";
class TreeCache { class TreeCache {
load(noteRows, branchRows) { load(noteRows, branchRows, relations) {
this.parents = []; this.parents = {};
this.children = []; this.children = {};
this.childParentToBranch = {}; this.childParentToBranch = {};
/** @type {Object.<string, NoteShort>} */ /** @type {Object.<string, NoteShort>} */
this.notes = {}; this.notes = {};
/** @type {Object.<string, Branch>} */
this.branches = {};
this.addResp(noteRows, branchRows, relations);
}
addResp(noteRows, branchRows, relations) {
for (const noteRow of noteRows) { for (const noteRow of noteRows) {
const note = new NoteShort(this, noteRow); const note = new NoteShort(this, noteRow);
this.notes[note.noteId] = note; this.notes[note.noteId] = note;
} }
/** @type {Object.<string, Branch>} */
this.branches = {};
for (const branchRow of branchRows) { for (const branchRow of branchRows) {
const branch = new Branch(this, branchRow); const branch = new Branch(this, branchRow);
this.addBranch(branch); this.addBranch(branch);
} }
for (const relation of relations) {
this.addBranchRelationship(relation.branchId, relation.childNoteId, relation.parentNoteId);
}
}
async getNotes(noteIds) {
const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
if (missingNoteIds.length > 0) {
const resp = await server.post('tree/load', { noteIds: missingNoteIds });
this.addResp(resp.notes, resp.branches, resp.relations);
}
return noteIds.map(noteId => {
if (!this.notes[noteId]) {
throw new Error(`Can't find note ${noteId}`);
}
else {
return this.notes[noteId];
}
});
} }
/** @return NoteShort */ /** @return NoteShort */
async getNote(noteId) { async getNote(noteId) {
return this.notes[noteId]; return (await this.getNotes([noteId]))[0];
} }
addBranch(branch) { addBranch(branch) {
this.branches[branch.branchId] = branch; this.branches[branch.branchId] = branch;
this.parents[branch.noteId] = this.parents[branch.noteId] || []; this.addBranchRelationship(branch.branchId, branch.noteId, branch.parentNoteId);
this.parents[branch.noteId].push(this.notes[branch.parentNoteId]); }
this.children[branch.parentNoteId] = this.children[branch.parentNoteId] || []; addBranchRelationship(branchId, childNoteId, parentNoteId) {
this.children[branch.parentNoteId].push(this.notes[branch.noteId]); this.childParentToBranch[childNoteId + '-' + parentNoteId] = branchId;
this.childParentToBranch[branch.noteId + '-' + branch.parentNoteId] = branch; this.parents[childNoteId] = this.parents[childNoteId] || [];
if (!this.parents[childNoteId].includes(parentNoteId)) {
this.parents[childNoteId].push(parentNoteId);
}
this.children[parentNoteId] = this.children[parentNoteId] || [];
if (!this.children[parentNoteId].includes(childNoteId)) {
this.children[parentNoteId].push(childNoteId);
}
} }
add(note, branch) { add(note, branch) {
@@ -49,21 +89,46 @@ class TreeCache {
this.addBranch(branch); this.addBranch(branch);
} }
async getBranches(branchIds) {
const missingBranchIds = branchIds.filter(branchId => this.branches[branchId] === undefined);
if (missingBranchIds.length > 0) {
const resp = await server.post('tree/load', { branchIds: branchIds });
this.addResp(resp.notes, resp.branches, resp.relations);
}
return branchIds.map(branchId => {
if (!this.branches[branchId]) {
throw new Error(`Can't find branch ${branchId}`);
}
else {
return this.branches[branchId];
}
});
}
/** @return Branch */ /** @return Branch */
async getBranch(branchId) { async getBranch(branchId) {
return this.branches[branchId]; return (await this.getBranches([branchId]))[0];
} }
/** @return Branch */ /** @return Branch */
async getBranchByChildParent(childNoteId, parentNoteId) { async getBranchByChildParent(childNoteId, parentNoteId) {
const key = (childNoteId + '-' + parentNoteId); const branchId = this.getBranchIdByChildParent(childNoteId, parentNoteId);
const branch = this.childParentToBranch[key];
if (!branch) { return await this.getBranch(branchId);
}
getBranchIdByChildParent(childNoteId, parentNoteId) {
const key = childNoteId + '-' + parentNoteId;
const branchId = this.childParentToBranch[key];
if (!branchId) {
infoService.throwError("Cannot find branch for child-parent=" + key); infoService.throwError("Cannot find branch for child-parent=" + key);
} }
return branch; return branchId;
} }
/* Move note from one parent to another. */ /* Move note from one parent to another. */
@@ -78,33 +143,14 @@ class TreeCache {
delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId
// remove old associations // remove old associations
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== oldParentNoteId); treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p !== oldParentNoteId);
treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch.noteId !== childNoteId); treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch !== childNoteId);
// add new associations // add new associations
treeCache.parents[childNoteId].push(await treeCache.getNote(newParentNoteId)); treeCache.parents[childNoteId].push(newParentNoteId);
treeCache.children[newParentNoteId] = treeCache.children[newParentNoteId] || []; // this might be first child treeCache.children[newParentNoteId] = treeCache.children[newParentNoteId] || []; // this might be first child
treeCache.children[newParentNoteId].push(await treeCache.getNote(childNoteId)); treeCache.children[newParentNoteId].push(childNoteId);
}
removeParentChildRelation(parentNoteId, childNoteId) {
utils.assertArguments(parentNoteId, childNoteId);
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== parentNoteId);
treeCache.children[parentNoteId] = treeCache.children[parentNoteId].filter(ch => ch.noteId !== childNoteId);
delete treeCache.childParentToBranch[childNoteId + '-' + parentNoteId];
}
async setParentChildRelation(branchId, parentNoteId, childNoteId) {
treeCache.parents[childNoteId] = treeCache.parents[childNoteId] || [];
treeCache.parents[childNoteId].push(await treeCache.getNote(parentNoteId));
treeCache.children[parentNoteId] = treeCache.children[parentNoteId] || [];
treeCache.children[parentNoteId].push(await treeCache.getNote(childNoteId));
treeCache.childParentToBranch[childNoteId + '-' + parentNoteId] = await treeCache.getBranch(branchId);
} }
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@
grid-template-areas: "header header" grid-template-areas: "header header"
"left-pane title" "left-pane title"
"left-pane note-detail"; "left-pane note-detail";
grid-template-columns: 2fr 5fr; grid-template-columns: 29% 70%;
grid-template-rows: auto grid-template-rows: auto
auto auto
1fr; 1fr;
@@ -33,7 +33,7 @@
} }
#note-detail-component-wrapper { #note-detail-component-wrapper {
flex-grow: 1; flex-grow: 100;
position: relative; position: relative;
overflow: auto; overflow: auto;
flex-basis: content; flex-basis: content;
@@ -61,8 +61,6 @@
} }
ul.fancytree-container { ul.fancytree-container {
overflow: auto;
position: relative;
outline: none !important; outline: none !important;
} }
@@ -161,12 +159,21 @@ div.ui-tooltip {
width: auto; width: auto;
} }
#tree {
overflow: auto;
flex-grow: 100;
flex-shrink: 100;
margin-top: 10px;
}
#parent-list { #parent-list {
display: none; display: none;
margin-left: 20px; margin-left: 20px;
border-top: 2px solid #eee; border-top: 2px solid #eee;
padding-top: 10px; padding-top: 10px;
grid-area: parent-list; grid-area: parent-list;
max-height: 300px;
overflow: auto;
} }
#parent-list ul { #parent-list ul {
@@ -264,7 +271,6 @@ div.ui-tooltip {
} }
.CodeMirror { .CodeMirror {
height: 100%;
font-family: "Liberation Mono", "Lucida Console", monospace; font-family: "Liberation Mono", "Lucida Console", monospace;
} }
@@ -330,4 +336,15 @@ div.ui-tooltip {
.child-overview a { .child-overview a {
color: #444; color: #444;
}
#sql-console-query {
height: 150px;
width: 100%;
border: 1px solid #ccc;
margin-bottom: 10px;
}
#sql-console-query .CodeMirror {
height: 150px;
} }

View File

@@ -0,0 +1,20 @@
"use strict";
const autocompleteService = require('../../services/autocomplete');
async function getAutocomplete(req) {
const query = req.query.query;
const results = autocompleteService.getResults(query);
return results.map(res => {
return {
value: res.title + ' (' + res.path + ')',
title: res.title
}
});
}
module.exports = {
getAutocomplete
};

View File

@@ -7,6 +7,8 @@ const sourceIdService = require('../../services/source_id');
const passwordEncryptionService = require('../../services/password_encryption'); const passwordEncryptionService = require('../../services/password_encryption');
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info'); const appInfo = require('../../services/app_info');
const eventService = require('../../services/events');
const cls = require('../../services/cls');
async function loginSync(req) { async function loginSync(req) {
const timestampStr = req.body.timestamp; const timestampStr = req.body.timestamp;
@@ -53,7 +55,12 @@ async function loginToProtectedSession(req) {
const decryptedDataKey = await passwordEncryptionService.getDataKey(password); const decryptedDataKey = await passwordEncryptionService.getDataKey(password);
const protectedSessionId = protectedSessionService.setDataKey(req, decryptedDataKey); const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey);
// this is set here so that event handlers have access to the protected session
cls.namespace.set('protectedSessionId', protectedSessionId);
eventService.emit(eventService.ENTER_PROTECTED_SESSION);
return { return {
success: true, success: true,

View File

@@ -47,7 +47,7 @@ async function sortNotes(req) {
async function protectBranch(req) { async function protectBranch(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const note = repository.getNote(noteId); const note = await repository.getNote(noteId);
const protect = !!parseInt(req.params.isProtected); const protect = !!parseInt(req.params.isProtected);
await noteService.protectNoteRecursively(note, protect); await noteService.protectNoteRecursively(note, protect);

View File

@@ -4,58 +4,79 @@ const sql = require('../../services/sql');
const optionService = require('../../services/options'); const optionService = require('../../services/options');
const protectedSessionService = require('../../services/protected_session'); const protectedSessionService = require('../../services/protected_session');
async function getTree() { async function getNotes(noteIds) {
const branches = await sql.getRows(` const questionMarks = noteIds.map(() => "?").join(",");
SELECT
branchId,
noteId,
parentNoteId,
notePosition,
prefix,
isExpanded
FROM
branches
WHERE
isDeleted = 0
ORDER BY
notePosition`);
const notes = [{ const notes = await sql.getRows(`
noteId: 'root', SELECT noteId, title, isProtected, type, mime
title: 'root', FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
isProtected: false,
type: 'none',
mime: 'none'
}].concat(await sql.getRows(`
SELECT
notes.noteId,
notes.title,
notes.isProtected,
notes.type,
notes.mime,
hideInAutocomplete.labelId AS 'hideInAutocomplete'
FROM
notes
LEFT JOIN labels AS hideInAutocomplete ON hideInAutocomplete.noteId = notes.noteId
AND hideInAutocomplete.name = 'hideInAutocomplete'
AND hideInAutocomplete.isDeleted = 0
WHERE
notes.isDeleted = 0`));
protectedSessionService.decryptNotes(notes); protectedSessionService.decryptNotes(notes);
notes.forEach(note => { notes.forEach(note => note.isProtected = !!note.isProtected);
note.hideInAutocomplete = !!note.hideInAutocomplete; return notes;
note.isProtected = !!note.isProtected; }
});
async function getRelations(noteIds) {
const questionMarks = noteIds.map(() => "?").join(",");
const doubledNoteIds = noteIds.concat(noteIds);
return await sql.getRows(`SELECT branchId, noteId AS 'childNoteId', parentNoteId FROM branches WHERE isDeleted = 0
AND (parentNoteId IN (${questionMarks}) OR noteId IN (${questionMarks}))`, doubledNoteIds);
}
async function getTree() {
// we fetch all branches of notes, even if that particular branch isn't visible
// this allows us to e.g. detect and properly display clones
const branches = await sql.getRows(`
WITH RECURSIVE
tree(branchId, noteId, isExpanded) AS (
SELECT branchId, noteId, isExpanded FROM branches WHERE branchId = 'root'
UNION ALL
SELECT branches.branchId, branches.noteId, branches.isExpanded FROM branches
JOIN tree ON branches.parentNoteId = tree.noteId
WHERE tree.isExpanded = 1 AND branches.isDeleted = 0
)
SELECT branches.* FROM tree JOIN branches USING(noteId) ORDER BY branches.notePosition`);
const noteIds = branches.map(b => b.noteId);
const notes = await getNotes(noteIds);
const relations = await getRelations(noteIds);
return { return {
startNotePath: await optionService.getOption('startNotePath'), startNotePath: await optionService.getOption('startNotePath'),
branches: branches, branches,
notes: notes notes,
relations
};
}
async function load(req) {
let noteIds = req.body.noteIds;
const branchIds = req.body.branchIds;
if (branchIds && branchIds.length > 0) {
noteIds = await sql.getColumn(`SELECT noteId FROM branches WHERE isDeleted = 0 AND branchId IN(${branchIds.map(() => "?").join(",")})`, branchIds);
}
const questionMarks = noteIds.map(() => "?").join(",");
const branches = await sql.getRows(`SELECT * FROM branches WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
const notes = await getNotes(noteIds);
const relations = await getRelations(noteIds);
return {
branches,
notes,
relations
}; };
} }
module.exports = { module.exports = {
getTree getTree,
load
}; };

View File

@@ -8,6 +8,7 @@ const multer = require('multer')();
const treeApiRoute = require('./api/tree'); const treeApiRoute = require('./api/tree');
const notesApiRoute = require('./api/notes'); const notesApiRoute = require('./api/notes');
const branchesApiRoute = require('./api/branches'); const branchesApiRoute = require('./api/branches');
const autocompleteApiRoute = require('./api/autocomplete');
const cloningApiRoute = require('./api/cloning'); const cloningApiRoute = require('./api/cloning');
const noteRevisionsApiRoute = require('./api/note_revisions'); const noteRevisionsApiRoute = require('./api/note_revisions');
const recentChangesApiRoute = require('./api/recent_changes'); const recentChangesApiRoute = require('./api/recent_changes');
@@ -99,6 +100,7 @@ function register(app) {
route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage); route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
apiRoute(GET, '/api/tree', treeApiRoute.getTree); apiRoute(GET, '/api/tree', treeApiRoute.getTree);
apiRoute(POST, '/api/tree/load', treeApiRoute.load);
apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix); apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix);
apiRoute(PUT, '/api/branches/:branchId/move-to/:parentNoteId', branchesApiRoute.moveBranchToParent); apiRoute(PUT, '/api/branches/:branchId/move-to/:parentNoteId', branchesApiRoute.moveBranchToParent);
@@ -107,6 +109,8 @@ function register(app) {
apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded); apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded);
apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch); apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch);
apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete);
apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote); apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote);
apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote); apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote);
apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);

View File

@@ -3,7 +3,7 @@
const build = require('./build'); const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const APP_DB_VERSION = 88; const APP_DB_VERSION = 93;
module.exports = { module.exports = {
appVersion: packageJson.version, appVersion: packageJson.version,

View File

@@ -0,0 +1,248 @@
const sql = require('./sql');
const sqlInit = require('./sql_init');
const eventService = require('./events');
const repository = require('./repository');
const protectedSessionService = require('./protected_session');
let noteTitles;
let protectedNoteTitles;
let noteIds;
const childToParent = {};
const hideInAutocomplete = {};
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
let prefixes = {};
async function load() {
noteTitles = await sql.getMap(`SELECT noteId, LOWER(title) FROM notes WHERE isDeleted = 0 AND isProtected = 0`);
noteIds = Object.keys(noteTitles);
prefixes = await sql.getMap(`SELECT noteId || '-' || parentNoteId, LOWER(prefix) FROM branches WHERE prefix IS NOT NULL AND prefix != ''`);
const relations = await sql.getRows(`SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0`);
for (const rel of relations) {
childToParent[rel.noteId] = childToParent[rel.noteId] || [];
childToParent[rel.noteId].push(rel.parentNoteId);
}
const hiddenLabels = await sql.getColumn(`SELECT noteId FROM labels WHERE isDeleted = 0 AND name = 'hideInAutocomplete'`);
for (const noteId of hiddenLabels) {
hideInAutocomplete[noteId] = true;
}
}
function getResults(query) {
if (!noteTitles || query.length <= 2) {
return [];
}
const tokens = query.toLowerCase().split(" ");
const results = [];
let noteIds = Object.keys(noteTitles);
if (protectedSessionService.isProtectedSessionAvailable()) {
noteIds = noteIds.concat(Object.keys(protectedNoteTitles));
}
for (const noteId of noteIds) {
if (hideInAutocomplete[noteId]) {
continue;
}
const parents = childToParent[noteId];
if (!parents) {
continue;
}
for (const parentNoteId of parents) {
const title = getNoteTitle(noteId, parentNoteId);
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));
search(parentNoteId, remainingTokens, [noteId], results);
}
}
}
results.sort((a, b) => a.title < b.title ? -1 : 1);
return results;
}
function search(noteId, tokens, path, results) {
if (tokens.length === 0) {
const retPath = getSomePath(noteId, path);
if (retPath) {
const noteTitle = getNoteTitleForPath(retPath);
results.push({
title: noteTitle,
path: retPath.join('/')
});
}
return;
}
const parents = childToParent[noteId];
if (!parents) {
return;
}
for (const parentNoteId of parents) {
if (results.length >= 200) {
return;
}
if (parentNoteId === 'root' || hideInAutocomplete[parentNoteId]) {
continue;
}
const title = getNoteTitle(noteId, parentNoteId);
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));
search(parentNoteId, remainingTokens, path.concat([noteId]), results);
}
else {
search(parentNoteId, tokens, path.concat([noteId]), results);
}
}
}
function getNoteTitle(noteId, parentNoteId) {
const prefix = prefixes[noteId + '-' + parentNoteId];
let title = noteTitles[noteId];
if (!title) {
if (protectedSessionService.isProtectedSessionAvailable()) {
title = protectedNoteTitles[noteId];
}
else {
title = '[protected]';
}
}
return (prefix ? (prefix + ' - ') : '') + title;
}
function getNoteTitleForPath(path) {
const titles = [];
let parentNoteId = 'root';
for (const noteId of path) {
const title = getNoteTitle(noteId, parentNoteId);
titles.push(title);
parentNoteId = noteId;
}
return titles.join(' / ');
}
function getSomePath(noteId, path) {
if (noteId === 'root') {
path.reverse();
return path;
}
const parents = childToParent[noteId];
if (!parents || parents.length === 0) {
return false;
}
for (const parentNoteId of parents) {
const retPath = getSomePath(parentNoteId, path.concat([noteId]));
if (retPath) {
return retPath;
}
}
return false;
}
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => {
if (entityName === 'notes') {
const note = await repository.getNote(entityId);
if (note.isDeleted) {
delete noteTitles[note.noteId];
delete childToParent[note.noteId];
}
else {
noteTitles[note.noteId] = note.title;
}
}
else if (entityName === 'branches') {
const branch = await repository.getBranch(entityId);
if (childToParent[branch.noteId]) {
childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.parentNoteId)
}
if (branch.isDeleted) {
delete prefixes[branch.noteId + '-' + branch.parentNoteId];
}
else {
if (branch.prefix) {
prefixes[branch.noteId + '-' + branch.parentNoteId] = branch.prefix;
}
childToParent[branch.noteId] = childToParent[branch.noteId] || [];
childToParent[branch.noteId].push(branch.parentNoteId);
}
}
else if (entityName === 'labels') {
const label = await repository.getLabel(entityId);
if (label.name === 'hideInAutocomplete') {
// we're not using label object directly, since there might be other non-deleted hideInAutocomplete label
const hideLabel = await repository.getEntity(`SELECT * FROM labels WHERE isDeleted = 0
AND name = 'hideInAutocomplete' AND noteId = ?`, [label.noteId]);
if (hideLabel) {
hideInAutocomplete[label.noteId] = true;
}
else {
delete hideInAutocomplete[label.noteId];
}
}
}
});
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
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]);
}
});
sqlInit.dbReady.then(load);
module.exports = {
getResults
};

View File

@@ -1 +1 @@
module.exports = { buildDate:"2018-04-14T08:28:50-04:00", buildRevision: "d57057ba28d2d93ffaeed15900116836fc791968" }; module.exports = { buildDate:"2018-05-22T23:51:43-04:00", buildRevision: "a372cbb2dfa918084e2d447a01fca6f076ddf486" };

View File

@@ -3,6 +3,7 @@
const sql = require('./sql'); const sql = require('./sql');
const sqlInit = require('./sql_init'); const sqlInit = require('./sql_init');
const log = require('./log'); const log = require('./log');
const utils = require('./utils');
const messagingService = require('./messaging'); const messagingService = require('./messaging');
const syncMutexService = require('./sync_mutex'); const syncMutexService = require('./sync_mutex');
const cls = require('./cls'); const cls = require('./cls');
@@ -116,7 +117,7 @@ async function runAllChecks() {
WHERE WHERE
notes.isDeleted = 1 notes.isDeleted = 1
AND branches.isDeleted = 0`, AND branches.isDeleted = 0`,
"Note tree is not deleted even though main note is deleted for following branch IDs", errorList); "Branch is not deleted even though main note is deleted for following branch IDs", errorList);
await runCheck(` await runCheck(`
SELECT SELECT
@@ -125,12 +126,12 @@ async function runAllChecks() {
branches AS child branches AS child
WHERE WHERE
child.isDeleted = 0 child.isDeleted = 0
AND child.parentNoteId != 'root' AND child.parentNoteId != 'none'
AND (SELECT COUNT(*) FROM branches AS parent WHERE parent.noteId = child.parentNoteId AND (SELECT COUNT(*) FROM branches AS parent WHERE parent.noteId = child.parentNoteId
AND parent.isDeleted = 0) = 0`, AND parent.isDeleted = 0) = 0`,
"All parent branches are deleted but child note tree is not for these child note tree IDs", errorList); "All parent branches are deleted but child branch is not for these child branch IDs", errorList);
// we do extra JOIN to eliminate orphan notes without note tree (which are reported separately) // we do extra JOIN to eliminate orphan notes without branches (which are reported separately)
await runCheck(` await runCheck(`
SELECT SELECT
DISTINCT noteId DISTINCT noteId
@@ -150,7 +151,7 @@ async function runAllChecks() {
LEFT JOIN branches AS parent ON parent.noteId = child.parentNoteId LEFT JOIN branches AS parent ON parent.noteId = child.parentNoteId
WHERE WHERE
parent.noteId IS NULL parent.noteId IS NULL
AND child.parentNoteId != 'root'`, AND child.parentNoteId != 'none'`,
"Not existing parent in the following parent > child relations", errorList); "Not existing parent in the following parent > child relations", errorList);
await runCheck(` await runCheck(`

View File

@@ -5,117 +5,40 @@ const utils = require('./utils');
const log = require('./log'); const log = require('./log');
const eventLogService = require('./event_log'); const eventLogService = require('./event_log');
const messagingService = require('./messaging'); const messagingService = require('./messaging');
const ApiToken = require('../entities/api_token');
const Branch = require('../entities/branch');
const Image = require('../entities/image');
const Note = require('../entities/note');
const NoteImage = require('../entities/note_image');
const Label = require('../entities/label');
const NoteRevision = require('../entities/note_revision');
const RecentNote = require('../entities/recent_note');
const Option = require('../entities/option');
function getHash(rows) { async function getHash(entityConstructor, whereBranch) {
let hash = ''; let contentToHash = await sql.getValue(`SELECT GROUP_CONCAT(hash) FROM ${entityConstructor.tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName}`);
for (const row of rows) { if (!contentToHash) { // might be null in case of no rows
hash = utils.hash(hash + JSON.stringify(row)); contentToHash = "";
} }
return hash; return utils.hash(contentToHash);
} }
async function getHashes() { async function getHashes() {
const startTime = new Date(); const startTime = new Date();
const hashes = { const hashes = {
notes: getHash(await sql.getRows(` notes: await getHash(Note),
SELECT branches: await getHash(Branch),
noteId, note_revisions: await getHash(NoteRevision),
title, recent_notes: await getHash(RecentNote),
content, options: await getHash(Option, "isSynced = 1"),
type, images: await getHash(Image),
dateModified, note_images: await getHash(NoteImage),
isProtected, labels: await getHash(Label),
isDeleted api_tokens: await getHash(ApiToken)
FROM notes
ORDER BY noteId`)),
branches: getHash(await sql.getRows(`
SELECT
branchId,
noteId,
parentNoteId,
notePosition,
dateModified,
isDeleted,
prefix
FROM branches
ORDER BY branchId`)),
note_revisions: getHash(await sql.getRows(`
SELECT
noteRevisionId,
noteId,
title,
content,
dateModifiedFrom,
dateModifiedTo
FROM note_revisions
ORDER BY noteRevisionId`)),
recent_notes: getHash(await sql.getRows(`
SELECT
branchId,
notePath,
dateAccessed,
isDeleted
FROM recent_notes
ORDER BY notePath`)),
options: getHash(await sql.getRows(`
SELECT
name,
value
FROM options
WHERE isSynced = 1
ORDER BY name`)),
// we don't include image data on purpose because they are quite large, checksum is good enough
// to represent the data anyway
images: getHash(await sql.getRows(`
SELECT
imageId,
format,
checksum,
name,
isDeleted,
dateModified,
dateCreated
FROM images
ORDER BY imageId`)),
note_images: getHash(await sql.getRows(`
SELECT
noteImageId,
noteId,
imageId,
isDeleted,
dateModified,
dateCreated
FROM note_images
ORDER BY noteImageId`)),
labels: getHash(await sql.getRows(`
SELECT
labelId,
noteId,
name,
value,
dateModified,
dateCreated
FROM labels
ORDER BY labelId`)),
api_tokens: getHash(await sql.getRows(`
SELECT
apiTokenId,
token,
dateCreated,
isDeleted
FROM api_tokens
ORDER BY apiTokenId`))
}; };
const elapseTimeMs = new Date().getTime() - startTime.getTime(); const elapseTimeMs = new Date().getTime() - startTime.getTime();

View File

@@ -31,6 +31,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
} }
async function getRootCalendarNote() { async function getRootCalendarNote() {
// some caching here could be useful (e.g. in CLS)
let rootNote = await labelService.getNoteWithLabel(CALENDAR_ROOT_LABEL); let rootNote = await labelService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
if (!rootNote) { if (!rootNote) {
@@ -89,10 +90,8 @@ async function getMonthNote(dateTimeStr, rootNote) {
return monthNote; return monthNote;
} }
async function getDateNote(dateTimeStr, rootNote = null) { async function getDateNote(dateTimeStr) {
if (!rootNote) { const rootNote = await getRootCalendarNote();
rootNote = await getRootCalendarNote();
}
const dateStr = dateTimeStr.substr(0, 10); const dateStr = dateTimeStr.substr(0, 10);
const dayNumber = dateTimeStr.substr(8, 2); const dayNumber = dateTimeStr.substr(8, 2);

28
src/services/events.js Normal file
View File

@@ -0,0 +1,28 @@
const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
const ENTITY_CHANGED = "ENTITY_CHANGED";
const eventListeners = {};
function subscribe(eventType, listener) {
eventListeners[eventType] = eventListeners[eventType] || [];
eventListeners[eventType].push(listener);
}
function emit(eventType, data) {
const listeners = eventListeners[eventType];
if (listeners) {
for (const listener of listeners) {
// not awaiting for async processing
listener(data);
}
}
}
module.exports = {
subscribe,
emit,
// event types:
ENTER_PROTECTED_SESSION,
ENTITY_CHANGED
};

View File

@@ -1,45 +1,37 @@
const sql = require('./sql'); const repository = require('./repository');
const utils = require('./utils'); const utils = require('./utils');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const syncTableService = require('./sync_table');
const appInfo = require('./app_info'); const appInfo = require('./app_info');
const Option = require('../entities/option');
async function getOption(name) { async function getOption(name) {
const row = await await sql.getRowOrNull("SELECT value FROM options WHERE name = ?", [name]); const option = await repository.getOption(name);
if (!row) { if (!option) {
throw new Error("Option " + name + " doesn't exist"); throw new Error("Option " + name + " doesn't exist");
} }
return row.value; return option.value;
} }
async function setOption(name, value) { async function setOption(name, value) {
const opt = await sql.getRow("SELECT * FROM options WHERE name = ?", [name]); const option = await repository.getOption(name);
if (!opt) { if (!option) {
throw new Error(`Option ${name} doesn't exist`); throw new Error(`Option ${name} doesn't exist`);
} }
if (opt.isSynced) { option.value = value;
await syncTableService.addOptionsSync(name);
}
await sql.execute("UPDATE options SET value = ?, dateModified = ? WHERE name = ?", await option.save();
[value, dateUtils.nowDate(), name]);
} }
async function createOption(name, value, isSynced) { async function createOption(name, value, isSynced) {
await sql.insert("options", { await new Option({
name: name, name: name,
value: value, value: value,
isSynced: isSynced, isSynced: isSynced
dateModified: dateUtils.nowDate() }).save();
});
if (isSynced) {
await syncTableService.addOptionsSync(name);
}
} }
async function initOptions(startNotePath) { async function initOptions(startNotePath) {

View File

@@ -6,7 +6,7 @@ const cls = require('./cls');
const dataKeyMap = {}; const dataKeyMap = {};
function setDataKey(req, decryptedDataKey) { function setDataKey(decryptedDataKey) {
const protectedSessionId = utils.randomSecureToken(32); const protectedSessionId = utils.randomSecureToken(32);
dataKeyMap[protectedSessionId] = Array.from(decryptedDataKey); // can't store buffer in session dataKeyMap[protectedSessionId] = Array.from(decryptedDataKey); // can't store buffer in session
@@ -28,12 +28,20 @@ function getDataKey() {
return dataKeyMap[protectedSessionId]; return dataKeyMap[protectedSessionId];
} }
function isProtectedSessionAvailable(req) { function isProtectedSessionAvailable() {
const protectedSessionId = getProtectedSessionId(req); const protectedSessionId = getProtectedSessionId();
return !!dataKeyMap[protectedSessionId]; return !!dataKeyMap[protectedSessionId];
} }
function decryptNoteTitle(noteId, encryptedTitle) {
const dataKey = getDataKey();
const iv = dataEncryptionService.noteTitleIv(noteId);
return dataEncryptionService.decryptString(dataKey, iv, encryptedTitle);
}
function decryptNote(note) { function decryptNote(note) {
const dataKey = getDataKey(); const dataKey = getDataKey();
@@ -99,6 +107,7 @@ module.exports = {
setDataKey, setDataKey,
getDataKey, getDataKey,
isProtectedSessionAvailable, isProtectedSessionAvailable,
decryptNoteTitle,
decryptNote, decryptNote,
decryptNotes, decryptNotes,
decryptNoteRevision, decryptNoteRevision,

View File

@@ -41,6 +41,10 @@ async function getLabel(labelId) {
return await getEntity("SELECT * FROM labels WHERE labelId = ?", [labelId]); return await getEntity("SELECT * FROM labels WHERE labelId = ?", [labelId]);
} }
async function getOption(name) {
return await getEntity("SELECT * FROM options WHERE name = ?", [name]);
}
async function updateEntity(entity) { async function updateEntity(entity) {
if (entity.beforeSaving) { if (entity.beforeSaving) {
await entity.beforeSaving(); await entity.beforeSaving();
@@ -55,7 +59,9 @@ async function updateEntity(entity) {
const primaryKey = entity[entity.constructor.primaryKeyName]; const primaryKey = entity[entity.constructor.primaryKeyName];
await syncTableService.addEntitySync(entity.constructor.tableName, primaryKey); if (entity.constructor.tableName !== 'options' || entity.isSynced) {
await syncTableService.addEntitySync(entity.constructor.tableName, primaryKey);
}
}); });
} }
@@ -66,6 +72,7 @@ module.exports = {
getBranch, getBranch,
getImage, getImage,
getLabel, getLabel,
getOption,
updateEntity, updateEntity,
setEntityConstructor setEntityConstructor
}; };

View File

@@ -1,6 +1,8 @@
const sql = require('./sql'); const sql = require('./sql');
const ScriptContext = require('./script_context'); const ScriptContext = require('./script_context');
const repository = require('./repository'); const repository = require('./repository');
const cls = require('./cls');
const sourceIdService = require('./source_id');
async function executeNote(note) { async function executeNote(note) {
if (!note.isJavaScript()) { if (!note.isJavaScript()) {
@@ -49,6 +51,9 @@ async function executeScript(script, params, startNoteId, currentNoteId) {
} }
async function execute(ctx, script, paramsStr) { async function execute(ctx, script, paramsStr) {
// scripts run as "server" sourceId so clients recognize the changes as "foreign" and update themselves
cls.namespace.set('sourceId', sourceIdService.getCurrentSourceId());
return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx)); return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx));
} }

View File

@@ -212,7 +212,8 @@ const primaryKeys = {
"images": "imageId", "images": "imageId",
"note_images": "noteImageId", "note_images": "noteImageId",
"labels": "labelId", "labels": "labelId",
"api_tokens": "apiTokenId" "api_tokens": "apiTokenId",
"options": "name"
}; };
async function getEntityRow(entityName, entityId) { async function getEntityRow(entityName, entityId) {

View File

@@ -4,6 +4,7 @@ const dateUtils = require('./date_utils');
const syncSetup = require('./sync_setup'); const syncSetup = require('./sync_setup');
const log = require('./log'); const log = require('./log');
const cls = require('./cls'); const cls = require('./cls');
const eventService = require('./events');
async function addNoteSync(noteId, sourceId) { async function addNoteSync(noteId, sourceId) {
await addEntitySync("notes", noteId, sourceId) await addEntitySync("notes", noteId, sourceId)
@@ -58,6 +59,11 @@ async function addEntitySync(entityName, entityId, sourceId) {
// useful when you fork the DB for new "client" instance, it won't try to sync the whole DB // useful when you fork the DB for new "client" instance, it won't try to sync the whole DB
await sql.execute("UPDATE options SET value = (SELECT MAX(id) FROM sync) WHERE name IN('lastSyncedPush', 'lastSyncedPull')"); await sql.execute("UPDATE options SET value = (SELECT MAX(id) FROM sync) WHERE name IN('lastSyncedPush', 'lastSyncedPull')");
} }
eventService.emit(eventService.ENTITY_CHANGED, {
entityName,
entityId
});
} }
async function cleanupSyncRowsForMissingEntities(entityName, entityKey) { async function cleanupSyncRowsForMissingEntities(entityName, entityKey) {

View File

@@ -58,7 +58,7 @@ async function start() {
} }
// we'll create clones for 20% of notes // we'll create clones for 20% of notes
for (let i = 0; i < (noteCount / 5); i++) { for (let i = 0; i < (noteCount / 50); i++) {
const noteIdToClone = getRandomParentNoteId(); const noteIdToClone = getRandomParentNoteId();
const parentNoteId = getRandomParentNoteId(); const parentNoteId = getRandomParentNoteId();
const prefix = Math.random() > 0.8 ? "prefix" : null; const prefix = Math.random() > 0.8 ? "prefix" : null;

View File

@@ -68,8 +68,7 @@
</div> </div>
</div> </div>
<div id="tree" class="hide-toggle" style="overflow: auto; flex-grow: 100; flex-shrink: 100; margin-top: 10px;"> <div id="tree"></div>
</div>
<div id="parent-list"> <div id="parent-list">
<p><strong>Note locations:</strong></p> <p><strong>Note locations:</strong></p>
@@ -78,8 +77,8 @@
</div> </div>
</div> </div>
<div class="hide-toggle" style="grid-area: title;"> <div style="grid-area: title;">
<div style="display: flex; align-items: center;"> <div class="hide-toggle" style="display: flex; align-items: center;">
<a title="Protect the note so that password will be required to view the note" <a title="Protect the note so that password will be required to view the note"
class="icon-action" class="icon-action"
id="protect-button" id="protect-button"
@@ -132,7 +131,7 @@
</div> </div>
</div> </div>
<div id="note-detail-wrapper"> <div id="note-detail-wrapper">
<div id="note-detail-component-wrapper"> <div id="note-detail-component-wrapper">
<div id="note-detail-text" class="note-detail-component" tabindex="2"></div> <div id="note-detail-text" class="note-detail-component" tabindex="2"></div>
@@ -425,7 +424,7 @@
</div> </div>
<div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;"> <div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
<div style="height: 150px; width: 100%; border: 1px solid #ccc; margin-bottom: 10px;" id="sql-console-query"></div> <div id="sql-console-query"></div>
<div style="text-align: center"> <div style="text-align: center">
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button> <button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>