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">
<Position>2</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;unnamed&quot;</DefaultExpression>
</column>
<column id="92" parent="13" name="content">
<Position>3</Position>
<DataType>TEXT|0s</DataType>
<NotNull>1</NotNull>
<DefaultExpression>&quot;&quot;</DefaultExpression>
</column>
<column id="93" parent="13" name="isProtected">
<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 ('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');

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

View File

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

View File

@@ -8,6 +8,8 @@ const sql = require('../services/sql');
class Branch extends Entity {
static get tableName() { return "branches"; }
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() {
return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);

View File

@@ -14,6 +14,17 @@ class Entity {
if (!this[this.constructor.primaryKeyName]) {
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() {

View File

@@ -6,6 +6,7 @@ const Branch = require('../entities/branch');
const Label = require('../entities/label');
const RecentNote = require('../entities/recent_note');
const ApiToken = require('../entities/api_token');
const Option = require('../entities/option');
const repository = require('../services/repository');
function createEntityFromRow(row) {
@@ -35,6 +36,9 @@ function createEntityFromRow(row) {
else if (row.noteId) {
entity = new Note(row);
}
else if (row.name) {
entity = new Option(row);
}
else {
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 {
static get tableName() { return "images"; }
static get primaryKeyName() { return "imageId"; }
static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; }
beforeSaving() {
super.beforeSaving();

View File

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

View File

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

View File

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

View File

@@ -1,19 +1,19 @@
"use strict";
const Entity = require('./entity');
const protected_session = require('../services/protected_session');
const utils = require('../services/utils');
const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
class NoteRevision extends Entity {
static get tableName() { return "note_revisions"; }
static get primaryKeyName() { return "noteRevisionId"; }
static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; }
constructor(row) {
super(row);
if (this.isProtected) {
protected_session.decryptNoteRevision(this);
protectedSessionService.decryptNoteRevision(this);
}
}
@@ -25,7 +25,7 @@ class NoteRevision extends Entity {
super.beforeSaving();
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 {
static get tableName() { return "recent_notes"; }
static get primaryKeyName() { return "branchId"; }
static get hashedProperties() { return ["branchId", "notePath", "dateAccessed", "isDeleted"]; }
}
module.exports = RecentNote;

View File

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

View File

@@ -22,7 +22,7 @@ async function showDialog() {
const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
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);
}

View File

@@ -1,7 +1,6 @@
import treeService from '../services/tree.js';
import linkService from '../services/link.js';
import utils from '../services/utils.js';
import autocompleteService from '../services/autocomplete.js';
import server from '../services/server.js';
const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete");
@@ -18,8 +17,12 @@ async function showDialog() {
});
await $autoComplete.autocomplete({
source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems),
minLength: 1
source: async function(request, response) {
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;
}
else {
noteLink = linkService.createNoteLink(change.noteId, change.title);
noteLink = await linkService.createNoteLink(change.noteId, change.title);
}
changesListEl.append($('<li>')

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,45 +2,85 @@ import utils from "./utils.js";
import Branch from "../entities/branch.js";
import NoteShort from "../entities/note_short.js";
import infoService from "./info.js";
import server from "./server.js";
class TreeCache {
load(noteRows, branchRows) {
this.parents = [];
this.children = [];
load(noteRows, branchRows, relations) {
this.parents = {};
this.children = {};
this.childParentToBranch = {};
/** @type {Object.<string, NoteShort>} */
this.notes = {};
/** @type {Object.<string, Branch>} */
this.branches = {};
this.addResp(noteRows, branchRows, relations);
}
addResp(noteRows, branchRows, relations) {
for (const noteRow of noteRows) {
const note = new NoteShort(this, noteRow);
this.notes[note.noteId] = note;
}
/** @type {Object.<string, Branch>} */
this.branches = {};
for (const branchRow of branchRows) {
const branch = new Branch(this, branchRow);
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 */
async getNote(noteId) {
return this.notes[noteId];
return (await this.getNotes([noteId]))[0];
}
addBranch(branch) {
this.branches[branch.branchId] = branch;
this.parents[branch.noteId] = this.parents[branch.noteId] || [];
this.parents[branch.noteId].push(this.notes[branch.parentNoteId]);
this.addBranchRelationship(branch.branchId, branch.noteId, branch.parentNoteId);
}
this.children[branch.parentNoteId] = this.children[branch.parentNoteId] || [];
this.children[branch.parentNoteId].push(this.notes[branch.noteId]);
addBranchRelationship(branchId, childNoteId, parentNoteId) {
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) {
@@ -49,21 +89,46 @@ class TreeCache {
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 */
async getBranch(branchId) {
return this.branches[branchId];
return (await this.getBranches([branchId]))[0];
}
/** @return Branch */
async getBranchByChildParent(childNoteId, parentNoteId) {
const key = (childNoteId + '-' + parentNoteId);
const branch = this.childParentToBranch[key];
const branchId = this.getBranchIdByChildParent(childNoteId, parentNoteId);
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);
}
return branch;
return branchId;
}
/* 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
// remove old associations
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== oldParentNoteId);
treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch.noteId !== childNoteId);
treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p !== oldParentNoteId);
treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch !== childNoteId);
// 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].push(await treeCache.getNote(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);
treeCache.children[newParentNoteId].push(childNoteId);
}
}

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"
"left-pane title"
"left-pane note-detail";
grid-template-columns: 2fr 5fr;
grid-template-columns: 29% 70%;
grid-template-rows: auto
auto
1fr;
@@ -33,7 +33,7 @@
}
#note-detail-component-wrapper {
flex-grow: 1;
flex-grow: 100;
position: relative;
overflow: auto;
flex-basis: content;
@@ -61,8 +61,6 @@
}
ul.fancytree-container {
overflow: auto;
position: relative;
outline: none !important;
}
@@ -161,12 +159,21 @@ div.ui-tooltip {
width: auto;
}
#tree {
overflow: auto;
flex-grow: 100;
flex-shrink: 100;
margin-top: 10px;
}
#parent-list {
display: none;
margin-left: 20px;
border-top: 2px solid #eee;
padding-top: 10px;
grid-area: parent-list;
max-height: 300px;
overflow: auto;
}
#parent-list ul {
@@ -264,7 +271,6 @@ div.ui-tooltip {
}
.CodeMirror {
height: 100%;
font-family: "Liberation Mono", "Lucida Console", monospace;
}
@@ -330,4 +336,15 @@ div.ui-tooltip {
.child-overview a {
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 protectedSessionService = require('../../services/protected_session');
const appInfo = require('../../services/app_info');
const eventService = require('../../services/events');
const cls = require('../../services/cls');
async function loginSync(req) {
const timestampStr = req.body.timestamp;
@@ -53,7 +55,12 @@ async function loginToProtectedSession(req) {
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 {
success: true,

View File

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

View File

@@ -4,58 +4,79 @@ const sql = require('../../services/sql');
const optionService = require('../../services/options');
const protectedSessionService = require('../../services/protected_session');
async function getTree() {
const branches = await sql.getRows(`
SELECT
branchId,
noteId,
parentNoteId,
notePosition,
prefix,
isExpanded
FROM
branches
WHERE
isDeleted = 0
ORDER BY
notePosition`);
async function getNotes(noteIds) {
const questionMarks = noteIds.map(() => "?").join(",");
const notes = [{
noteId: 'root',
title: 'root',
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`));
const notes = await sql.getRows(`
SELECT noteId, title, isProtected, type, mime
FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
protectedSessionService.decryptNotes(notes);
notes.forEach(note => {
note.hideInAutocomplete = !!note.hideInAutocomplete;
note.isProtected = !!note.isProtected;
});
notes.forEach(note => note.isProtected = !!note.isProtected);
return notes;
}
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 {
startNotePath: await optionService.getOption('startNotePath'),
branches: branches,
notes: notes
branches,
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 = {
getTree
getTree,
load
};

View File

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

View File

@@ -3,7 +3,7 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 88;
const APP_DB_VERSION = 93;
module.exports = {
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 sqlInit = require('./sql_init');
const log = require('./log');
const utils = require('./utils');
const messagingService = require('./messaging');
const syncMutexService = require('./sync_mutex');
const cls = require('./cls');
@@ -116,7 +117,7 @@ async function runAllChecks() {
WHERE
notes.isDeleted = 1
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(`
SELECT
@@ -125,12 +126,12 @@ async function runAllChecks() {
branches AS child
WHERE
child.isDeleted = 0
AND child.parentNoteId != 'root'
AND child.parentNoteId != 'none'
AND (SELECT COUNT(*) FROM branches AS parent WHERE parent.noteId = child.parentNoteId
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(`
SELECT
DISTINCT noteId
@@ -150,7 +151,7 @@ async function runAllChecks() {
LEFT JOIN branches AS parent ON parent.noteId = child.parentNoteId
WHERE
parent.noteId IS NULL
AND child.parentNoteId != 'root'`,
AND child.parentNoteId != 'none'`,
"Not existing parent in the following parent > child relations", errorList);
await runCheck(`

View File

@@ -5,117 +5,40 @@ const utils = require('./utils');
const log = require('./log');
const eventLogService = require('./event_log');
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) {
let hash = '';
async function getHash(entityConstructor, whereBranch) {
let contentToHash = await sql.getValue(`SELECT GROUP_CONCAT(hash) FROM ${entityConstructor.tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName}`);
for (const row of rows) {
hash = utils.hash(hash + JSON.stringify(row));
if (!contentToHash) { // might be null in case of no rows
contentToHash = "";
}
return hash;
return utils.hash(contentToHash);
}
async function getHashes() {
const startTime = new Date();
const hashes = {
notes: getHash(await sql.getRows(`
SELECT
noteId,
title,
content,
type,
dateModified,
isProtected,
isDeleted
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`))
notes: await getHash(Note),
branches: await getHash(Branch),
note_revisions: await getHash(NoteRevision),
recent_notes: await getHash(RecentNote),
options: await getHash(Option, "isSynced = 1"),
images: await getHash(Image),
note_images: await getHash(NoteImage),
labels: await getHash(Label),
api_tokens: await getHash(ApiToken)
};
const elapseTimeMs = new Date().getTime() - startTime.getTime();

View File

@@ -31,6 +31,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
}
async function getRootCalendarNote() {
// some caching here could be useful (e.g. in CLS)
let rootNote = await labelService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
if (!rootNote) {
@@ -89,10 +90,8 @@ async function getMonthNote(dateTimeStr, rootNote) {
return monthNote;
}
async function getDateNote(dateTimeStr, rootNote = null) {
if (!rootNote) {
rootNote = await getRootCalendarNote();
}
async function getDateNote(dateTimeStr) {
const rootNote = await getRootCalendarNote();
const dateStr = dateTimeStr.substr(0, 10);
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 dateUtils = require('./date_utils');
const syncTableService = require('./sync_table');
const appInfo = require('./app_info');
const Option = require('../entities/option');
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");
}
return row.value;
return option.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`);
}
if (opt.isSynced) {
await syncTableService.addOptionsSync(name);
}
option.value = value;
await sql.execute("UPDATE options SET value = ?, dateModified = ? WHERE name = ?",
[value, dateUtils.nowDate(), name]);
await option.save();
}
async function createOption(name, value, isSynced) {
await sql.insert("options", {
await new Option({
name: name,
value: value,
isSynced: isSynced,
dateModified: dateUtils.nowDate()
});
if (isSynced) {
await syncTableService.addOptionsSync(name);
}
isSynced: isSynced
}).save();
}
async function initOptions(startNotePath) {

View File

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

View File

@@ -41,6 +41,10 @@ async function getLabel(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) {
if (entity.beforeSaving) {
await entity.beforeSaving();
@@ -55,7 +59,9 @@ async function updateEntity(entity) {
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,
getImage,
getLabel,
getOption,
updateEntity,
setEntityConstructor
};

View File

@@ -1,6 +1,8 @@
const sql = require('./sql');
const ScriptContext = require('./script_context');
const repository = require('./repository');
const cls = require('./cls');
const sourceIdService = require('./source_id');
async function executeNote(note) {
if (!note.isJavaScript()) {
@@ -49,6 +51,9 @@ async function executeScript(script, params, startNoteId, currentNoteId) {
}
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));
}

View File

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

View File

@@ -4,6 +4,7 @@ const dateUtils = require('./date_utils');
const syncSetup = require('./sync_setup');
const log = require('./log');
const cls = require('./cls');
const eventService = require('./events');
async function addNoteSync(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
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) {

View File

@@ -58,7 +58,7 @@ async function start() {
}
// 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 parentNoteId = getRandomParentNoteId();
const prefix = Math.random() > 0.8 ? "prefix" : null;

View File

@@ -68,8 +68,7 @@
</div>
</div>
<div id="tree" class="hide-toggle" style="overflow: auto; flex-grow: 100; flex-shrink: 100; margin-top: 10px;">
</div>
<div id="tree"></div>
<div id="parent-list">
<p><strong>Note locations:</strong></p>
@@ -78,8 +77,8 @@
</div>
</div>
<div class="hide-toggle" style="grid-area: title;">
<div style="display: flex; align-items: center;">
<div style="grid-area: title;">
<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"
class="icon-action"
id="protect-button"
@@ -132,7 +131,7 @@
</div>
</div>
<div id="note-detail-wrapper">
<div id="note-detail-wrapper">
<div id="note-detail-component-wrapper">
<div id="note-detail-text" class="note-detail-component" tabindex="2"></div>
@@ -425,7 +424,7 @@
</div>
<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">
<button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>