syncification

This commit is contained in:
zadam 2020-06-20 12:31:38 +02:00
parent 30062d687f
commit 88348c560c
97 changed files with 1673 additions and 1700 deletions

View File

@ -7,11 +7,8 @@ const bodyParser = require('body-parser');
const helmet = require('helmet'); const helmet = require('helmet');
const session = require('express-session'); const session = require('express-session');
const FileStore = require('session-file-store')(session); const FileStore = require('session-file-store')(session);
const os = require('os');
const sessionSecret = require('./services/session_secret'); const sessionSecret = require('./services/session_secret');
const cls = require('./services/cls');
const dataDir = require('./services/data_dir'); const dataDir = require('./services/data_dir');
require('./entities/entity_constructor');
require('./services/handlers'); require('./services/handlers');
require('./services/hoisted_note_loader'); require('./services/hoisted_note_loader');
require('./services/note_cache/note_cache_loader'); require('./services/note_cache/note_cache_loader');

View File

@ -1,7 +1,6 @@
"use strict"; "use strict";
const Entity = require('./entity'); const Entity = require('./entity');
const repository = require('../services/repository');
const dateUtils = require('../services/date_utils'); const dateUtils = require('../services/date_utils');
const sql = require('../services/sql'); const sql = require('../services/sql');
@ -44,14 +43,14 @@ class Attribute extends Entity {
/** /**
* @returns {Promise<Note|null>} * @returns {Promise<Note|null>}
*/ */
async getNote() { getNote() {
return await repository.getNote(this.noteId); return this.repository.getNote(this.noteId);
} }
/** /**
* @returns {Promise<Note|null>} * @returns {Promise<Note|null>}
*/ */
async getTargetNote() { getTargetNote() {
if (this.type !== 'relation') { if (this.type !== 'relation') {
throw new Error(`Attribute ${this.attributeId} is not relation`); throw new Error(`Attribute ${this.attributeId} is not relation`);
} }
@ -60,7 +59,7 @@ class Attribute extends Entity {
return null; return null;
} }
return await repository.getNote(this.value); return this.repository.getNote(this.value);
} }
/** /**
@ -70,7 +69,7 @@ class Attribute extends Entity {
return this.type === 'label-definition' || this.type === 'relation-definition'; return this.type === 'label-definition' || this.type === 'relation-definition';
} }
async beforeSaving() { beforeSaving() {
if (!this.value) { if (!this.value) {
if (this.type === 'relation') { if (this.type === 'relation') {
throw new Error(`Cannot save relation ${this.name} since it does not target any note.`); throw new Error(`Cannot save relation ${this.name} since it does not target any note.`);
@ -81,7 +80,7 @@ class Attribute extends Entity {
} }
if (this.position === undefined) { if (this.position === undefined) {
this.position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]); this.position = 1 + sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [this.noteId]);
} }
if (!this.isInheritable) { if (!this.isInheritable) {

View File

@ -2,7 +2,6 @@
const Entity = require('./entity'); const Entity = require('./entity');
const dateUtils = require('../services/date_utils'); const dateUtils = require('../services/date_utils');
const repository = require('../services/repository');
const sql = require('../services/sql'); const sql = require('../services/sql');
/** /**
@ -29,18 +28,18 @@ class Branch extends Entity {
static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "deleteId", "prefix"]; } static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "isDeleted", "deleteId", "prefix"]; }
/** @returns {Promise<Note|null>} */ /** @returns {Promise<Note|null>} */
async getNote() { getNote() {
return await repository.getNote(this.noteId); return this.repository.getNote(this.noteId);
} }
/** @returns {Promise<Note|null>} */ /** @returns {Promise<Note|null>} */
async getParentNote() { getParentNote() {
return await repository.getNote(this.parentNoteId); return this.repository.getNote(this.parentNoteId);
} }
async beforeSaving() { beforeSaving() {
if (this.notePosition === undefined) { if (this.notePosition === undefined) {
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]); const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]);
this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10; this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10;
} }

View File

@ -1,6 +1,7 @@
"use strict"; "use strict";
const utils = require('../services/utils'); const utils = require('../services/utils');
let repo = null;
class Entity { class Entity {
/** /**
@ -51,8 +52,16 @@ class Entity {
return utils.hash(contentToHash).substr(0, 10); return utils.hash(contentToHash).substr(0, 10);
} }
async save() { get repository() {
await require('../services/repository').updateEntity(this); if (!repo) {
repo = require('../services/repository');
}
return repo;
}
save() {
this.repository.updateEntity(this);
return this; return this;
} }

View File

@ -1,3 +1,4 @@
const repository = require('../services/repository');
const Note = require('../entities/note'); const Note = require('../entities/note');
const NoteRevision = require('../entities/note_revision'); const NoteRevision = require('../entities/note_revision');
const Branch = require('../entities/branch'); const Branch = require('../entities/branch');
@ -5,7 +6,6 @@ const Attribute = require('../entities/attribute');
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 Option = require('../entities/option');
const repository = require('../services/repository');
const cls = require('../services/cls'); const cls = require('../services/cls');
const ENTITY_NAME_TO_ENTITY = { const ENTITY_NAME_TO_ENTITY = {
@ -71,5 +71,3 @@ module.exports = {
createEntityFromRow, createEntityFromRow,
getEntityFromEntityName getEntityFromEntityName
}; };
repository.setEntityConstructor(module.exports);

View File

@ -3,7 +3,6 @@
const Entity = require('./entity'); const Entity = require('./entity');
const Attribute = require('./attribute'); const Attribute = require('./attribute');
const protectedSessionService = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
const sql = require('../services/sql'); const sql = require('../services/sql');
const utils = require('../services/utils'); const utils = require('../services/utils');
const dateUtils = require('../services/date_utils'); const dateUtils = require('../services/date_utils');
@ -72,9 +71,9 @@ class Note extends Entity {
*/ */
/** @returns {Promise<*>} */ /** @returns {Promise<*>} */
async getContent(silentNotFoundError = false) { getContent(silentNotFoundError = false) {
if (this.content === undefined) { if (this.content === undefined) {
const res = await sql.getRow(`SELECT content, hash FROM note_contents WHERE noteId = ?`, [this.noteId]); const res = sql.getRow(`SELECT content, hash FROM note_contents WHERE noteId = ?`, [this.noteId]);
if (!res) { if (!res) {
if (silentNotFoundError) { if (silentNotFoundError) {
@ -108,8 +107,8 @@ class Note extends Entity {
} }
/** @returns {Promise<*>} */ /** @returns {Promise<*>} */
async getJsonContent() { getJsonContent() {
const content = await this.getContent(); const content = this.getContent();
if (!content || !content.trim()) { if (!content || !content.trim()) {
return null; return null;
@ -119,7 +118,7 @@ class Note extends Entity {
} }
/** @returns {Promise} */ /** @returns {Promise} */
async setContent(content) { setContent(content) {
if (content === null || content === undefined) { if (content === null || content === undefined) {
throw new Error(`Cannot set null content to note ${this.noteId}`); throw new Error(`Cannot set null content to note ${this.noteId}`);
} }
@ -129,7 +128,7 @@ class Note extends Entity {
// force updating note itself so that dateModified is represented correctly even for the content // force updating note itself so that dateModified is represented correctly even for the content
this.forcedChange = true; this.forcedChange = true;
this.contentLength = content.byteLength; this.contentLength = content.byteLength;
await this.save(); this.save();
this.content = content; this.content = content;
@ -149,14 +148,14 @@ class Note extends Entity {
} }
} }
await sql.upsert("note_contents", "noteId", pojo); sql.upsert("note_contents", "noteId", pojo);
await syncTableService.addNoteContentSync(this.noteId); syncTableService.addNoteContentSync(this.noteId);
} }
/** @returns {Promise} */ /** @returns {Promise} */
async setJsonContent(content) { setJsonContent(content) {
await this.setContent(JSON.stringify(content, null, '\t')); this.setContent(JSON.stringify(content, null, '\t'));
} }
/** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */ /** @returns {boolean} true if this note is the root of the note tree. Root note has "root" noteId */
@ -204,8 +203,8 @@ class Note extends Entity {
return null; return null;
} }
async loadOwnedAttributesToCache() { loadOwnedAttributesToCache() {
this.__ownedAttributeCache = await repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]); this.__ownedAttributeCache = this.repository.getEntities(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ?`, [this.noteId]);
return this.__ownedAttributeCache; return this.__ownedAttributeCache;
} }
@ -217,9 +216,9 @@ class Note extends Entity {
* @param {string} [name] - (optional) attribute name to filter * @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} note's "owned" attributes - excluding inherited ones * @returns {Promise<Attribute[]>} note's "owned" attributes - excluding inherited ones
*/ */
async getOwnedAttributes(type, name) { getOwnedAttributes(type, name) {
if (!this.__ownedAttributeCache) { if (!this.__ownedAttributeCache) {
await this.loadOwnedAttributesToCache(); this.loadOwnedAttributesToCache();
} }
if (type && name) { if (type && name) {
@ -241,8 +240,8 @@ class Note extends Entity {
* *
* This method can be significantly faster than the getAttribute() * This method can be significantly faster than the getAttribute()
*/ */
async getOwnedAttribute(type, name) { getOwnedAttribute(type, name) {
const attrs = await this.getOwnedAttributes(type, name); const attrs = this.getOwnedAttributes(type, name);
return attrs.length > 0 ? attrs[0] : null; return attrs.length > 0 ? attrs[0] : null;
} }
@ -250,8 +249,8 @@ class Note extends Entity {
/** /**
* @returns {Promise<Attribute[]>} relations targetting this specific note * @returns {Promise<Attribute[]>} relations targetting this specific note
*/ */
async getTargetRelations() { getTargetRelations() {
return await repository.getEntities("SELECT * FROM attributes WHERE type = 'relation' AND isDeleted = 0 AND value = ?", [this.noteId]); return this.repository.getEntities("SELECT * FROM attributes WHERE type = 'relation' AND isDeleted = 0 AND value = ?", [this.noteId]);
} }
/** /**
@ -259,9 +258,9 @@ class Note extends Entity {
* @param {string} [name] - (optional) attribute name to filter * @param {string} [name] - (optional) attribute name to filter
* @returns {Promise<Attribute[]>} all note's attributes, including inherited ones * @returns {Promise<Attribute[]>} all note's attributes, including inherited ones
*/ */
async getAttributes(type, name) { getAttributes(type, name) {
if (!this.__attributeCache) { if (!this.__attributeCache) {
await this.loadAttributesToCache(); this.loadAttributesToCache();
} }
if (type && name) { if (type && name) {
@ -282,52 +281,52 @@ class Note extends Entity {
* @param {string} [name] - label name to filter * @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones * @returns {Promise<Attribute[]>} all note's labels (attributes with type label), including inherited ones
*/ */
async getLabels(name) { getLabels(name) {
return await this.getAttributes(LABEL, name); return this.getAttributes(LABEL, name);
} }
/** /**
* @param {string} [name] - label name to filter * @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's labels (attributes with type label), excluding inherited ones * @returns {Promise<Attribute[]>} all note's labels (attributes with type label), excluding inherited ones
*/ */
async getOwnedLabels(name) { getOwnedLabels(name) {
return await this.getOwnedAttributes(LABEL, name); return this.getOwnedAttributes(LABEL, name);
} }
/** /**
* @param {string} [name] - label name to filter * @param {string} [name] - label name to filter
* @returns {Promise<Attribute[]>} all note's label definitions, including inherited ones * @returns {Promise<Attribute[]>} all note's label definitions, including inherited ones
*/ */
async getLabelDefinitions(name) { getLabelDefinitions(name) {
return await this.getAttributes(LABEL_DEFINITION, name); return this.getAttributes(LABEL_DEFINITION, name);
} }
/** /**
* @param {string} [name] - relation name to filter * @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones * @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), including inherited ones
*/ */
async getRelations(name) { getRelations(name) {
return await this.getAttributes(RELATION, name); return this.getAttributes(RELATION, name);
} }
/** /**
* @param {string} [name] - relation name to filter * @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), excluding inherited ones * @returns {Promise<Attribute[]>} all note's relations (attributes with type relation), excluding inherited ones
*/ */
async getOwnedRelations(name) { getOwnedRelations(name) {
return await this.getOwnedAttributes(RELATION, name); return this.getOwnedAttributes(RELATION, name);
} }
/** /**
* @param {string} [name] - relation name to filter * @param {string} [name] - relation name to filter
* @returns {Promise<Note[]>} * @returns {Promise<Note[]>}
*/ */
async getRelationTargets(name) { getRelationTargets(name) {
const relations = await this.getRelations(name); const relations = this.getRelations(name);
const targets = []; const targets = [];
for (const relation of relations) { for (const relation of relations) {
targets.push(await relation.getTargetNote()); targets.push(relation.getTargetNote());
} }
return targets; return targets;
@ -337,8 +336,8 @@ class Note extends Entity {
* @param {string} [name] - relation name to filter * @param {string} [name] - relation name to filter
* @returns {Promise<Attribute[]>} all note's relation definitions including inherited ones * @returns {Promise<Attribute[]>} all note's relation definitions including inherited ones
*/ */
async getRelationDefinitions(name) { getRelationDefinitions(name) {
return await this.getAttributes(RELATION_DEFINITION, name); return this.getAttributes(RELATION_DEFINITION, name);
} }
/** /**
@ -351,8 +350,8 @@ class Note extends Entity {
} }
/** @returns {Promise<void>} */ /** @returns {Promise<void>} */
async loadAttributesToCache() { loadAttributesToCache() {
const attributes = await repository.getEntities(` const attributes = this.repository.getEntities(`
WITH RECURSIVE WITH RECURSIVE
tree(noteId, level) AS ( tree(noteId, level) AS (
SELECT ?, 0 SELECT ?, 0
@ -419,8 +418,8 @@ class Note extends Entity {
* @param {string} name - attribute name * @param {string} name - attribute name
* @returns {Promise<boolean>} true if note has an attribute with given type and name (including inherited) * @returns {Promise<boolean>} true if note has an attribute with given type and name (including inherited)
*/ */
async hasAttribute(type, name) { hasAttribute(type, name) {
return !!await this.getAttribute(type, name); return !!this.getAttribute(type, name);
} }
/** /**
@ -428,8 +427,8 @@ class Note extends Entity {
* @param {string} name - attribute name * @param {string} name - attribute name
* @returns {Promise<boolean>} true if note has an attribute with given type and name (excluding inherited) * @returns {Promise<boolean>} true if note has an attribute with given type and name (excluding inherited)
*/ */
async hasOwnedAttribute(type, name) { hasOwnedAttribute(type, name) {
return !!await this.getOwnedAttribute(type, name); return !!this.getOwnedAttribute(type, name);
} }
/** /**
@ -437,8 +436,8 @@ class Note extends Entity {
* @param {string} name - attribute name * @param {string} name - attribute name
* @returns {Promise<Attribute>} attribute of given type and name. If there's more such attributes, first is returned. Returns null if there's no such attribute belonging to this note. * @returns {Promise<Attribute>} attribute of given type and name. If there's more such attributes, first is returned. Returns null if there's no such attribute belonging to this note.
*/ */
async getAttribute(type, name) { getAttribute(type, name) {
const attributes = await this.getAttributes(); const attributes = this.getAttributes();
return attributes.find(attr => attr.type === type && attr.name === name); return attributes.find(attr => attr.type === type && attr.name === name);
} }
@ -448,8 +447,8 @@ class Note extends Entity {
* @param {string} name - attribute name * @param {string} name - attribute name
* @returns {Promise<string|null>} attribute value of given type and name or null if no such attribute exists. * @returns {Promise<string|null>} attribute value of given type and name or null if no such attribute exists.
*/ */
async getAttributeValue(type, name) { getAttributeValue(type, name) {
const attr = await this.getAttribute(type, name); const attr = this.getAttribute(type, name);
return attr ? attr.value : null; return attr ? attr.value : null;
} }
@ -459,8 +458,8 @@ class Note extends Entity {
* @param {string} name - attribute name * @param {string} name - attribute name
* @returns {Promise<string|null>} attribute value of given type and name or null if no such attribute exists. * @returns {Promise<string|null>} attribute value of given type and name or null if no such attribute exists.
*/ */
async getOwnedAttributeValue(type, name) { getOwnedAttributeValue(type, name) {
const attr = await this.getOwnedAttribute(type, name); const attr = this.getOwnedAttribute(type, name);
return attr ? attr.value : null; return attr ? attr.value : null;
} }
@ -474,12 +473,12 @@ class Note extends Entity {
* @param {string} [value] - attribute value (optional) * @param {string} [value] - attribute value (optional)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async toggleAttribute(type, enabled, name, value) { toggleAttribute(type, enabled, name, value) {
if (enabled) { if (enabled) {
await this.setAttribute(type, name, value); this.setAttribute(type, name, value);
} }
else { else {
await this.removeAttribute(type, name, value); this.removeAttribute(type, name, value);
} }
} }
@ -491,14 +490,14 @@ class Note extends Entity {
* @param {string} [value] - attribute value (optional) * @param {string} [value] - attribute value (optional)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async setAttribute(type, name, value) { setAttribute(type, name, value) {
const attributes = await this.loadOwnedAttributesToCache(); const attributes = this.loadOwnedAttributesToCache();
let attr = attributes.find(attr => attr.type === type && attr.name === name); let attr = attributes.find(attr => attr.type === type && attr.name === name);
if (attr) { if (attr) {
if (attr.value !== value) { if (attr.value !== value) {
attr.value = value; attr.value = value;
await attr.save(); attr.save();
this.invalidateAttributeCache(); this.invalidateAttributeCache();
} }
@ -511,7 +510,7 @@ class Note extends Entity {
value: value !== undefined ? value : "" value: value !== undefined ? value : ""
}); });
await attr.save(); attr.save();
this.invalidateAttributeCache(); this.invalidateAttributeCache();
} }
@ -525,13 +524,13 @@ class Note extends Entity {
* @param {string} [value] - attribute value (optional) * @param {string} [value] - attribute value (optional)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async removeAttribute(type, name, value) { removeAttribute(type, name, value) {
const attributes = await this.loadOwnedAttributesToCache(); const attributes = this.loadOwnedAttributesToCache();
for (const attribute of attributes) { for (const attribute of attributes) {
if (attribute.type === type && attribute.name === name && (value === undefined || value === attribute.value)) { if (attribute.type === type && attribute.name === name && (value === undefined || value === attribute.value)) {
attribute.isDeleted = true; attribute.isDeleted = true;
await attribute.save(); attribute.save();
this.invalidateAttributeCache(); this.invalidateAttributeCache();
} }
@ -541,7 +540,7 @@ class Note extends Entity {
/** /**
* @return {Promise<Attribute>} * @return {Promise<Attribute>}
*/ */
async addAttribute(type, name, value = "", isInheritable = false, position = 1000) { addAttribute(type, name, value = "", isInheritable = false, position = 1000) {
const attr = new Attribute({ const attr = new Attribute({
noteId: this.noteId, noteId: this.noteId,
type: type, type: type,
@ -551,111 +550,111 @@ class Note extends Entity {
position: position position: position
}); });
await attr.save(); attr.save();
this.invalidateAttributeCache(); this.invalidateAttributeCache();
return attr; return attr;
} }
async addLabel(name, value = "", isInheritable = false) { addLabel(name, value = "", isInheritable = false) {
return await this.addAttribute(LABEL, name, value, isInheritable); return this.addAttribute(LABEL, name, value, isInheritable);
} }
async addRelation(name, targetNoteId, isInheritable = false) { addRelation(name, targetNoteId, isInheritable = false) {
return await this.addAttribute(RELATION, name, targetNoteId, isInheritable); return this.addAttribute(RELATION, name, targetNoteId, isInheritable);
} }
/** /**
* @param {string} name - label name * @param {string} name - label name
* @returns {Promise<boolean>} true if label exists (including inherited) * @returns {Promise<boolean>} true if label exists (including inherited)
*/ */
async hasLabel(name) { return await this.hasAttribute(LABEL, name); } hasLabel(name) { return this.hasAttribute(LABEL, name); }
/** /**
* @param {string} name - label name * @param {string} name - label name
* @returns {Promise<boolean>} true if label exists (excluding inherited) * @returns {Promise<boolean>} true if label exists (excluding inherited)
*/ */
async hasOwnedLabel(name) { return await this.hasOwnedAttribute(LABEL, name); } hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
/** /**
* @param {string} name - relation name * @param {string} name - relation name
* @returns {Promise<boolean>} true if relation exists (including inherited) * @returns {Promise<boolean>} true if relation exists (including inherited)
*/ */
async hasRelation(name) { return await this.hasAttribute(RELATION, name); } hasRelation(name) { return this.hasAttribute(RELATION, name); }
/** /**
* @param {string} name - relation name * @param {string} name - relation name
* @returns {Promise<boolean>} true if relation exists (excluding inherited) * @returns {Promise<boolean>} true if relation exists (excluding inherited)
*/ */
async hasOwnedRelation(name) { return await this.hasOwnedAttribute(RELATION, name); } hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
/** /**
* @param {string} name - label name * @param {string} name - label name
* @returns {Promise<Attribute|null>} label if it exists, null otherwise * @returns {Promise<Attribute|null>} label if it exists, null otherwise
*/ */
async getLabel(name) { return await this.getAttribute(LABEL, name); } getLabel(name) { return this.getAttribute(LABEL, name); }
/** /**
* @param {string} name - label name * @param {string} name - label name
* @returns {Promise<Attribute|null>} label if it exists, null otherwise * @returns {Promise<Attribute|null>} label if it exists, null otherwise
*/ */
async getOwnedLabel(name) { return await this.getOwnedAttribute(LABEL, name); } getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
/** /**
* @param {string} name - relation name * @param {string} name - relation name
* @returns {Promise<Attribute|null>} relation if it exists, null otherwise * @returns {Promise<Attribute|null>} relation if it exists, null otherwise
*/ */
async getRelation(name) { return await this.getAttribute(RELATION, name); } getRelation(name) { return this.getAttribute(RELATION, name); }
/** /**
* @param {string} name - relation name * @param {string} name - relation name
* @returns {Promise<Attribute|null>} relation if it exists, null otherwise * @returns {Promise<Attribute|null>} relation if it exists, null otherwise
*/ */
async getOwnedRelation(name) { return await this.getOwnedAttribute(RELATION, name); } getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
/** /**
* @param {string} name - label name * @param {string} name - label name
* @returns {Promise<string|null>} label value if label exists, null otherwise * @returns {Promise<string|null>} label value if label exists, null otherwise
*/ */
async getLabelValue(name) { return await this.getAttributeValue(LABEL, name); } getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
/** /**
* @param {string} name - label name * @param {string} name - label name
* @returns {Promise<string|null>} label value if label exists, null otherwise * @returns {Promise<string|null>} label value if label exists, null otherwise
*/ */
async getOwnedLabelValue(name) { return await this.getOwnedAttributeValue(LABEL, name); } getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
/** /**
* @param {string} name - relation name * @param {string} name - relation name
* @returns {Promise<string|null>} relation value if relation exists, null otherwise * @returns {Promise<string|null>} relation value if relation exists, null otherwise
*/ */
async getRelationValue(name) { return await this.getAttributeValue(RELATION, name); } getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
/** /**
* @param {string} name - relation name * @param {string} name - relation name
* @returns {Promise<string|null>} relation value if relation exists, null otherwise * @returns {Promise<string|null>} relation value if relation exists, null otherwise
*/ */
async getOwnedRelationValue(name) { return await this.getOwnedAttributeValue(RELATION, name); } getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
/** /**
* @param {string} name * @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found) * @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
*/ */
async getRelationTarget(name) { getRelationTarget(name) {
const relation = await this.getRelation(name); const relation = this.getRelation(name);
return relation ? await repository.getNote(relation.value) : null; return relation ? this.repository.getNote(relation.value) : null;
} }
/** /**
* @param {string} name * @param {string} name
* @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found) * @returns {Promise<Note>|null} target note of the relation or null (if target is empty or note was not found)
*/ */
async getOwnedRelationTarget(name) { getOwnedRelationTarget(name) {
const relation = await this.getOwnedRelation(name); const relation = this.getOwnedRelation(name);
return relation ? await repository.getNote(relation.value) : null; return relation ? this.repository.getNote(relation.value) : null;
} }
/** /**
@ -666,7 +665,7 @@ class Note extends Entity {
* @param {string} [value] - label value (optional) * @param {string} [value] - label value (optional)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async toggleLabel(enabled, name, value) { return await this.toggleAttribute(LABEL, enabled, name, value); } toggleLabel(enabled, name, value) { return this.toggleAttribute(LABEL, enabled, name, value); }
/** /**
* Based on enabled, relation is either set or removed. * Based on enabled, relation is either set or removed.
@ -676,7 +675,7 @@ class Note extends Entity {
* @param {string} [value] - relation value (noteId) * @param {string} [value] - relation value (noteId)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async toggleRelation(enabled, name, value) { return await this.toggleAttribute(RELATION, enabled, name, value); } toggleRelation(enabled, name, value) { return this.toggleAttribute(RELATION, enabled, name, value); }
/** /**
* Update's given label's value or creates it if it doesn't exist * Update's given label's value or creates it if it doesn't exist
@ -685,7 +684,7 @@ class Note extends Entity {
* @param {string} [value] - label value * @param {string} [value] - label value
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async setLabel(name, value) { return await this.setAttribute(LABEL, name, value); } setLabel(name, value) { return this.setAttribute(LABEL, name, value); }
/** /**
* Update's given relation's value or creates it if it doesn't exist * Update's given relation's value or creates it if it doesn't exist
@ -694,7 +693,7 @@ class Note extends Entity {
* @param {string} [value] - relation value (noteId) * @param {string} [value] - relation value (noteId)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async setRelation(name, value) { return await this.setAttribute(RELATION, name, value); } setRelation(name, value) { return this.setAttribute(RELATION, name, value); }
/** /**
* Remove label name-value pair, if it exists. * Remove label name-value pair, if it exists.
@ -703,7 +702,7 @@ class Note extends Entity {
* @param {string} [value] - label value * @param {string} [value] - label value
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async removeLabel(name, value) { return await this.removeAttribute(LABEL, name, value); } removeLabel(name, value) { return this.removeAttribute(LABEL, name, value); }
/** /**
* Remove relation name-value pair, if it exists. * Remove relation name-value pair, if it exists.
@ -712,13 +711,13 @@ class Note extends Entity {
* @param {string} [value] - relation value (noteId) * @param {string} [value] - relation value (noteId)
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async removeRelation(name, value) { return await this.removeAttribute(RELATION, name, value); } removeRelation(name, value) { return this.removeAttribute(RELATION, name, value); }
/** /**
* @return {Promise<string[]>} return list of all descendant noteIds of this note. Returning just noteIds because number of notes can be huge. Includes also this note's noteId * @return {Promise<string[]>} return list of all descendant noteIds of this note. Returning just noteIds because number of notes can be huge. Includes also this note's noteId
*/ */
async getDescendantNoteIds() { getDescendantNoteIds() {
return await sql.getColumn(` return sql.getColumn(`
WITH RECURSIVE WITH RECURSIVE
tree(noteId) AS ( tree(noteId) AS (
SELECT ? SELECT ?
@ -740,7 +739,7 @@ class Note extends Entity {
* @param {string} [value] - attribute value * @param {string} [value] - attribute value
* @returns {Promise<Note[]>} * @returns {Promise<Note[]>}
*/ */
async getDescendantNotesWithAttribute(type, name, value) { getDescendantNotesWithAttribute(type, name, value) {
const params = [this.noteId, name]; const params = [this.noteId, name];
let valueCondition = ""; let valueCondition = "";
@ -749,7 +748,7 @@ class Note extends Entity {
valueCondition = " AND attributes.value = ?"; valueCondition = " AND attributes.value = ?";
} }
const notes = await repository.getEntities(` const notes = this.repository.getEntities(`
WITH RECURSIVE WITH RECURSIVE
tree(noteId) AS ( tree(noteId) AS (
SELECT ? SELECT ?
@ -778,7 +777,7 @@ class Note extends Entity {
* @param {string} [value] - label value * @param {string} [value] - label value
* @returns {Promise<Note[]>} * @returns {Promise<Note[]>}
*/ */
async getDescendantNotesWithLabel(name, value) { return await this.getDescendantNotesWithAttribute(LABEL, name, value); } getDescendantNotesWithLabel(name, value) { return this.getDescendantNotesWithAttribute(LABEL, name, value); }
/** /**
* Finds descendant notes with given relation name and value. Only own relations are considered, not inherited ones * Finds descendant notes with given relation name and value. Only own relations are considered, not inherited ones
@ -787,15 +786,15 @@ class Note extends Entity {
* @param {string} [value] - relation value * @param {string} [value] - relation value
* @returns {Promise<Note[]>} * @returns {Promise<Note[]>}
*/ */
async getDescendantNotesWithRelation(name, value) { return await this.getDescendantNotesWithAttribute(RELATION, name, value); } getDescendantNotesWithRelation(name, value) { return this.getDescendantNotesWithAttribute(RELATION, name, value); }
/** /**
* Returns note revisions of this note. * Returns note revisions of this note.
* *
* @returns {Promise<NoteRevision[]>} * @returns {Promise<NoteRevision[]>}
*/ */
async getRevisions() { getRevisions() {
return await repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]); return this.repository.getEntities("SELECT * FROM note_revisions WHERE noteId = ?", [this.noteId]);
} }
/** /**
@ -804,8 +803,8 @@ class Note extends Entity {
* @deprecated - not intended for general use * @deprecated - not intended for general use
* @returns {Promise<Attribute[]>} * @returns {Promise<Attribute[]>}
*/ */
async getLinks() { getLinks() {
return await repository.getEntities(` return this.repository.getEntities(`
SELECT * SELECT *
FROM attributes FROM attributes
WHERE noteId = ? AND WHERE noteId = ? AND
@ -817,22 +816,22 @@ class Note extends Entity {
/** /**
* @returns {Promise<Branch[]>} * @returns {Promise<Branch[]>}
*/ */
async getBranches() { getBranches() {
return await repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); return this.repository.getEntities("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
} }
/** /**
* @returns {boolean} - true if note has children * @returns {boolean} - true if note has children
*/ */
async hasChildren() { hasChildren() {
return (await this.getChildNotes()).length > 0; return (this.getChildNotes()).length > 0;
} }
/** /**
* @returns {Promise<Note[]>} child notes of this note * @returns {Promise<Note[]>} child notes of this note
*/ */
async getChildNotes() { getChildNotes() {
return await repository.getEntities(` return this.repository.getEntities(`
SELECT notes.* SELECT notes.*
FROM branches FROM branches
JOIN notes USING(noteId) JOIN notes USING(noteId)
@ -845,8 +844,8 @@ class Note extends Entity {
/** /**
* @returns {Promise<Branch[]>} child branches of this note * @returns {Promise<Branch[]>} child branches of this note
*/ */
async getChildBranches() { getChildBranches() {
return await repository.getEntities(` return this.repository.getEntities(`
SELECT branches.* SELECT branches.*
FROM branches FROM branches
WHERE branches.isDeleted = 0 WHERE branches.isDeleted = 0
@ -857,8 +856,8 @@ class Note extends Entity {
/** /**
* @returns {Promise<Note[]>} parent notes of this note (note can have multiple parents because of cloning) * @returns {Promise<Note[]>} parent notes of this note (note can have multiple parents because of cloning)
*/ */
async getParentNotes() { getParentNotes() {
return await repository.getEntities(` return this.repository.getEntities(`
SELECT parent_notes.* SELECT parent_notes.*
FROM FROM
branches AS child_tree branches AS child_tree
@ -871,15 +870,15 @@ class Note extends Entity {
/** /**
* @return {Promise<string[][]>} - array of notePaths (each represented by array of noteIds constituting the particular note path) * @return {Promise<string[][]>} - array of notePaths (each represented by array of noteIds constituting the particular note path)
*/ */
async getAllNotePaths() { getAllNotePaths() {
if (this.noteId === 'root') { if (this.noteId === 'root') {
return [['root']]; return [['root']];
} }
const notePaths = []; const notePaths = [];
for (const parentNote of await this.getParentNotes()) { for (const parentNote of this.getParentNotes()) {
for (const parentPath of await parentNote.getAllNotePaths()) { for (const parentPath of parentNote.getAllNotePaths()) {
parentPath.push(this.noteId); parentPath.push(this.noteId);
notePaths.push(parentPath); notePaths.push(parentPath);
} }
@ -892,8 +891,8 @@ class Note extends Entity {
* @param ancestorNoteId * @param ancestorNoteId
* @return {Promise<boolean>} - true if ancestorNoteId occurs in at least one of the note's paths * @return {Promise<boolean>} - true if ancestorNoteId occurs in at least one of the note's paths
*/ */
async isDescendantOfNote(ancestorNoteId) { isDescendantOfNote(ancestorNoteId) {
const notePaths = await this.getAllNotePaths(); const notePaths = this.getAllNotePaths();
return notePaths.some(path => path.includes(ancestorNoteId)); return notePaths.some(path => path.includes(ancestorNoteId));
} }

View File

@ -2,7 +2,6 @@
const Entity = require('./entity'); const Entity = require('./entity');
const protectedSessionService = require('../services/protected_session'); const protectedSessionService = require('../services/protected_session');
const repository = require('../services/repository');
const utils = require('../services/utils'); const utils = require('../services/utils');
const sql = require('../services/sql'); const sql = require('../services/sql');
const dateUtils = require('../services/date_utils'); const dateUtils = require('../services/date_utils');
@ -47,8 +46,8 @@ class NoteRevision extends Entity {
} }
} }
async getNote() { getNote() {
return await repository.getNote(this.noteId); return this.repository.getNote(this.noteId);
} }
/** @returns {boolean} true if the note has string content (not binary) */ /** @returns {boolean} true if the note has string content (not binary) */
@ -66,9 +65,9 @@ class NoteRevision extends Entity {
*/ */
/** @returns {Promise<*>} */ /** @returns {Promise<*>} */
async getContent(silentNotFoundError = false) { getContent(silentNotFoundError = false) {
if (this.content === undefined) { if (this.content === undefined) {
const res = await sql.getRow(`SELECT content, hash FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]); const res = sql.getRow(`SELECT content, hash FROM note_revision_contents WHERE noteRevisionId = ?`, [this.noteRevisionId]);
if (!res) { if (!res) {
if (silentNotFoundError) { if (silentNotFoundError) {
@ -102,11 +101,11 @@ class NoteRevision extends Entity {
} }
/** @returns {Promise} */ /** @returns {Promise} */
async setContent(content) { setContent(content) {
// force updating note itself so that utcDateModified is represented correctly even for the content // force updating note itself so that utcDateModified is represented correctly even for the content
this.forcedChange = true; this.forcedChange = true;
this.contentLength = content === null ? 0 : content.length; this.contentLength = content === null ? 0 : content.length;
await this.save(); this.save();
this.content = content; this.content = content;
@ -126,9 +125,9 @@ class NoteRevision extends Entity {
} }
} }
await sql.upsert("note_revision_contents", "noteRevisionId", pojo); sql.upsert("note_revision_contents", "noteRevisionId", pojo);
await syncTableService.addNoteRevisionContentSync(this.noteRevisionId); syncTableService.addNoteRevisionContentSync(this.noteRevisionId);
} }
beforeSaving() { beforeSaving() {

View File

@ -2,7 +2,7 @@
const appInfo = require('../../services/app_info'); const appInfo = require('../../services/app_info');
async function getAppInfo() { function getAppInfo() {
return appInfo; return appInfo;
} }

View File

@ -6,19 +6,19 @@ const attributeService = require('../../services/attributes');
const repository = require('../../services/repository'); const repository = require('../../services/repository');
const Attribute = require('../../entities/attribute'); const Attribute = require('../../entities/attribute');
async function getEffectiveNoteAttributes(req) { function getEffectiveNoteAttributes(req) {
const note = await repository.getNote(req.params.noteId); const note = repository.getNote(req.params.noteId);
return await note.getAttributes(); return note.getAttributes();
} }
async function updateNoteAttribute(req) { function updateNoteAttribute(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const body = req.body; const body = req.body;
let attribute; let attribute;
if (body.attributeId) { if (body.attributeId) {
attribute = await repository.getAttribute(body.attributeId); attribute = repository.getAttribute(body.attributeId);
if (attribute.noteId !== noteId) { if (attribute.noteId !== noteId) {
return [400, `Attribute ${body.attributeId} is not owned by ${noteId}`]; return [400, `Attribute ${body.attributeId} is not owned by ${noteId}`];
@ -30,11 +30,11 @@ async function updateNoteAttribute(req) {
if (body.type !== 'relation' || !!body.value.trim()) { if (body.type !== 'relation' || !!body.value.trim()) {
const newAttribute = attribute.createClone(body.type, body.name, body.value); const newAttribute = attribute.createClone(body.type, body.name, body.value);
await newAttribute.save(); newAttribute.save();
} }
attribute.isDeleted = true; attribute.isDeleted = true;
await attribute.save(); attribute.save();
return { return {
attributeId: attribute.attributeId attributeId: attribute.attributeId
@ -60,18 +60,18 @@ async function updateNoteAttribute(req) {
attribute.isDeleted = true; attribute.isDeleted = true;
} }
await attribute.save(); attribute.save();
return { return {
attributeId: attribute.attributeId attributeId: attribute.attributeId
}; };
} }
async function deleteNoteAttribute(req) { function deleteNoteAttribute(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const attributeId = req.params.attributeId; const attributeId = req.params.attributeId;
const attribute = await repository.getAttribute(attributeId); const attribute = repository.getAttribute(attributeId);
if (attribute) { if (attribute) {
if (attribute.noteId !== noteId) { if (attribute.noteId !== noteId) {
@ -79,17 +79,17 @@ async function deleteNoteAttribute(req) {
} }
attribute.isDeleted = true; attribute.isDeleted = true;
await attribute.save(); attribute.save();
} }
} }
async function updateNoteAttributes2(req) { function updateNoteAttributes2(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const incomingAttributes = req.body; const incomingAttributes = req.body;
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
let existingAttrs = await note.getAttributes(); let existingAttrs = note.getAttributes();
let position = 0; let position = 0;
@ -107,14 +107,14 @@ async function updateNoteAttributes2(req) {
if (perfectMatchAttr.position !== position) { if (perfectMatchAttr.position !== position) {
perfectMatchAttr.position = position; perfectMatchAttr.position = position;
await perfectMatchAttr.save(); perfectMatchAttr.save();
} }
continue; // nothing to update continue; // nothing to update
} }
if (incAttr.type === 'relation') { if (incAttr.type === 'relation') {
const targetNote = await repository.getNote(incAttr.value); const targetNote = repository.getNote(incAttr.value);
if (!targetNote || targetNote.isDeleted) { if (!targetNote || targetNote.isDeleted) {
log.error(`Target note of relation ${JSON.stringify(incAttr)} does not exist or is deleted`); log.error(`Target note of relation ${JSON.stringify(incAttr)} does not exist or is deleted`);
@ -130,7 +130,7 @@ async function updateNoteAttributes2(req) {
if (matchedAttr) { if (matchedAttr) {
matchedAttr.value = incAttr.value; matchedAttr.value = incAttr.value;
matchedAttr.position = position; matchedAttr.position = position;
await matchedAttr.save(); matchedAttr.save();
existingAttrs = existingAttrs.filter(attr => attr.attributeId !== matchedAttr.attributeId); existingAttrs = existingAttrs.filter(attr => attr.attributeId !== matchedAttr.attributeId);
continue; continue;
@ -139,17 +139,17 @@ async function updateNoteAttributes2(req) {
// no existing attribute has been matched so we need to create a new one // no existing attribute has been matched so we need to create a new one
// type, name and isInheritable are immutable so even if there is an attribute with matching type & name, we need to create a new one and delete the former one // type, name and isInheritable are immutable so even if there is an attribute with matching type & name, we need to create a new one and delete the former one
await note.addAttribute(incAttr.type, incAttr.name, incAttr.value, incAttr.isInheritable, position); note.addAttribute(incAttr.type, incAttr.name, incAttr.value, incAttr.isInheritable, position);
} }
// all the remaining existing attributes are not defined anymore and should be deleted // all the remaining existing attributes are not defined anymore and should be deleted
for (const toDeleteAttr of existingAttrs) { for (const toDeleteAttr of existingAttrs) {
toDeleteAttr.isDeleted = true; toDeleteAttr.isDeleted = true;
await toDeleteAttr.save(); toDeleteAttr.save();
} }
} }
async function updateNoteAttributes(req) { function updateNoteAttributes(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const attributes = req.body; const attributes = req.body;
@ -157,7 +157,7 @@ async function updateNoteAttributes(req) {
let attributeEntity; let attributeEntity;
if (attribute.attributeId) { if (attribute.attributeId) {
attributeEntity = await repository.getAttribute(attribute.attributeId); attributeEntity = repository.getAttribute(attribute.attributeId);
if (attributeEntity.noteId !== noteId) { if (attributeEntity.noteId !== noteId) {
return [400, `Attribute ${attributeEntity.noteId} is not owned by ${noteId}`]; return [400, `Attribute ${attributeEntity.noteId} is not owned by ${noteId}`];
@ -170,11 +170,11 @@ async function updateNoteAttributes(req) {
if (attribute.type !== 'relation' || !!attribute.value.trim()) { if (attribute.type !== 'relation' || !!attribute.value.trim()) {
const newAttribute = attributeEntity.createClone(attribute.type, attribute.name, attribute.value, attribute.isInheritable); const newAttribute = attributeEntity.createClone(attribute.type, attribute.name, attribute.value, attribute.isInheritable);
await newAttribute.save(); newAttribute.save();
} }
attributeEntity.isDeleted = true; attributeEntity.isDeleted = true;
await attributeEntity.save(); attributeEntity.save();
continue; continue;
} }
@ -203,34 +203,34 @@ async function updateNoteAttributes(req) {
attributeEntity.value = attribute.value; attributeEntity.value = attribute.value;
} }
await attributeEntity.save(); attributeEntity.save();
} }
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
note.invalidateAttributeCache(); note.invalidateAttributeCache();
return await note.getAttributes(); return note.getAttributes();
} }
async function getAttributeNames(req) { function getAttributeNames(req) {
const type = req.query.type; const type = req.query.type;
const query = req.query.query; const query = req.query.query;
return attributeService.getAttributeNames(type, query); return attributeService.getAttributeNames(type, query);
} }
async function getValuesForAttribute(req) { function getValuesForAttribute(req) {
const attributeName = req.params.attributeName; const attributeName = req.params.attributeName;
return await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND type = 'label' AND value != '' ORDER BY value", [attributeName]); return sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND type = 'label' AND value != '' ORDER BY value", [attributeName]);
} }
async function createRelation(req) { function createRelation(req) {
const sourceNoteId = req.params.noteId; const sourceNoteId = req.params.noteId;
const targetNoteId = req.params.targetNoteId; const targetNoteId = req.params.targetNoteId;
const name = req.params.name; const name = req.params.name;
let attribute = await repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? AND type = 'relation' AND name = ? AND value = ?`, [sourceNoteId, name, targetNoteId]); let attribute = repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? AND type = 'relation' AND name = ? AND value = ?`, [sourceNoteId, name, targetNoteId]);
if (!attribute) { if (!attribute) {
attribute = new Attribute(); attribute = new Attribute();
@ -239,22 +239,22 @@ async function createRelation(req) {
attribute.type = 'relation'; attribute.type = 'relation';
attribute.value = targetNoteId; attribute.value = targetNoteId;
await attribute.save(); attribute.save();
} }
return attribute; return attribute;
} }
async function deleteRelation(req) { function deleteRelation(req) {
const sourceNoteId = req.params.noteId; const sourceNoteId = req.params.noteId;
const targetNoteId = req.params.targetNoteId; const targetNoteId = req.params.targetNoteId;
const name = req.params.name; const name = req.params.name;
let attribute = await repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? AND type = 'relation' AND name = ? AND value = ?`, [sourceNoteId, name, targetNoteId]); let attribute = repository.getEntity(`SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? AND type = 'relation' AND name = ? AND value = ?`, [sourceNoteId, name, targetNoteId]);
if (attribute) { if (attribute) {
attribute.isDeleted = true; attribute.isDeleted = true;
await attribute.save(); attribute.save();
} }
} }

View File

@ -7,7 +7,7 @@ const log = require('../../services/log');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const optionService = require('../../services/options'); const optionService = require('../../services/options');
async function getAutocomplete(req) { function getAutocomplete(req) {
const query = req.query.query.trim(); const query = req.query.query.trim();
const activeNoteId = req.query.activeNoteId || 'none'; const activeNoteId = req.query.activeNoteId || 'none';
@ -16,10 +16,10 @@ async function getAutocomplete(req) {
const timestampStarted = Date.now(); const timestampStarted = Date.now();
if (query.length === 0) { if (query.length === 0) {
results = await getRecentNotes(activeNoteId); results = getRecentNotes(activeNoteId);
} }
else { else {
results = await searchService.searchNotesForAutocomplete(query); results = searchService.searchNotesForAutocomplete(query);
} }
const msTaken = Date.now() - timestampStarted; const msTaken = Date.now() - timestampStarted;
@ -31,15 +31,15 @@ async function getAutocomplete(req) {
return results; return results;
} }
async function getRecentNotes(activeNoteId) { function getRecentNotes(activeNoteId) {
let extraCondition = ''; let extraCondition = '';
const hoistedNoteId = await optionService.getOption('hoistedNoteId'); const hoistedNoteId = optionService.getOption('hoistedNoteId');
if (hoistedNoteId !== 'root') { if (hoistedNoteId !== 'root') {
extraCondition = `AND recent_notes.notePath LIKE '%${utils.sanitizeSql(hoistedNoteId)}%'`; extraCondition = `AND recent_notes.notePath LIKE '%${utils.sanitizeSql(hoistedNoteId)}%'`;
} }
const recentNotes = await repository.getEntities(` const recentNotes = repository.getEntities(`
SELECT SELECT
recent_notes.* recent_notes.*
FROM FROM

View File

@ -4,7 +4,7 @@ const fs = require('fs');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const {LOG_DIR} = require('../../services/data_dir.js'); const {LOG_DIR} = require('../../services/data_dir.js');
async function getBackendLog() { function getBackendLog() {
const file = `${LOG_DIR}/trilium-${dateUtils.localNowDate()}.log`; const file = `${LOG_DIR}/trilium-${dateUtils.localNowDate()}.log`;
return fs.readFileSync(file, 'utf8'); return fs.readFileSync(file, 'utf8');

View File

@ -13,11 +13,11 @@ const TaskContext = require('../../services/task_context');
* for not deleted branches. There may be multiple deleted note-parent note relationships. * for not deleted branches. There may be multiple deleted note-parent note relationships.
*/ */
async function moveBranchToParent(req) { function moveBranchToParent(req) {
const {branchId, parentBranchId} = req.params; const {branchId, parentBranchId} = req.params;
const parentBranch = await repository.getBranch(parentBranchId); const parentBranch = repository.getBranch(parentBranchId);
const branchToMove = await repository.getBranch(branchId); const branchToMove = repository.getBranch(branchId);
if (!parentBranch || !branchToMove) { if (!parentBranch || !branchToMove) {
return [400, `One or both branches ${branchId}, ${parentBranchId} have not been found`]; return [400, `One or both branches ${branchId}, ${parentBranchId} have not been found`];
@ -27,35 +27,35 @@ async function moveBranchToParent(req) {
return { success: true }; // no-op return { success: true }; // no-op
} }
const validationResult = await treeService.validateParentChild(parentBranch.noteId, branchToMove.noteId, branchId); const validationResult = treeService.validateParentChild(parentBranch.noteId, branchToMove.noteId, branchId);
if (!validationResult.success) { if (!validationResult.success) {
return [200, validationResult]; return [200, validationResult];
} }
const maxNotePos = await sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentBranch.noteId]); const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [parentBranch.noteId]);
const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10; const newNotePos = maxNotePos === null ? 0 : maxNotePos + 10;
// expanding so that the new placement of the branch is immediately visible // expanding so that the new placement of the branch is immediately visible
parentBranch.isExpanded = true; parentBranch.isExpanded = true;
await parentBranch.save(); parentBranch.save();
const newBranch = branchToMove.createClone(parentBranch.noteId, newNotePos); const newBranch = branchToMove.createClone(parentBranch.noteId, newNotePos);
await newBranch.save(); newBranch.save();
branchToMove.isDeleted = true; branchToMove.isDeleted = true;
await branchToMove.save(); branchToMove.save();
return { success: true }; return { success: true };
} }
async function moveBranchBeforeNote(req) { function moveBranchBeforeNote(req) {
const {branchId, beforeBranchId} = req.params; const {branchId, beforeBranchId} = req.params;
const branchToMove = await repository.getBranch(branchId); const branchToMove = repository.getBranch(branchId);
const beforeNote = await repository.getBranch(beforeBranchId); const beforeNote = repository.getBranch(beforeBranchId);
const validationResult = await treeService.validateParentChild(beforeNote.parentNoteId, branchToMove.noteId, branchId); const validationResult = treeService.validateParentChild(beforeNote.parentNoteId, branchToMove.noteId, branchId);
if (!validationResult.success) { if (!validationResult.success) {
return [200, validationResult]; return [200, validationResult];
@ -63,33 +63,33 @@ async function moveBranchBeforeNote(req) {
// we don't change utcDateModified so other changes are prioritized in case of conflict // we don't change utcDateModified so other changes are prioritized in case of conflict
// also we would have to sync all those modified branches otherwise hash checks would fail // also we would have to sync all those modified branches otherwise hash checks would fail
await sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0", sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0",
[beforeNote.parentNoteId, beforeNote.notePosition]); [beforeNote.parentNoteId, beforeNote.notePosition]);
await syncTableService.addNoteReorderingSync(beforeNote.parentNoteId); syncTableService.addNoteReorderingSync(beforeNote.parentNoteId);
if (branchToMove.parentNoteId === beforeNote.parentNoteId) { if (branchToMove.parentNoteId === beforeNote.parentNoteId) {
branchToMove.notePosition = beforeNote.notePosition; branchToMove.notePosition = beforeNote.notePosition;
await branchToMove.save(); branchToMove.save();
} }
else { else {
const newBranch = branchToMove.createClone(beforeNote.parentNoteId, beforeNote.notePosition); const newBranch = branchToMove.createClone(beforeNote.parentNoteId, beforeNote.notePosition);
await newBranch.save(); newBranch.save();
branchToMove.isDeleted = true; branchToMove.isDeleted = true;
await branchToMove.save(); branchToMove.save();
} }
return { success: true }; return { success: true };
} }
async function moveBranchAfterNote(req) { function moveBranchAfterNote(req) {
const {branchId, afterBranchId} = req.params; const {branchId, afterBranchId} = req.params;
const branchToMove = await repository.getBranch(branchId); const branchToMove = repository.getBranch(branchId);
const afterNote = await repository.getBranch(afterBranchId); const afterNote = repository.getBranch(afterBranchId);
const validationResult = await treeService.validateParentChild(afterNote.parentNoteId, branchToMove.noteId, branchId); const validationResult = treeService.validateParentChild(afterNote.parentNoteId, branchToMove.noteId, branchId);
if (!validationResult.success) { if (!validationResult.success) {
return [200, validationResult]; return [200, validationResult];
@ -97,42 +97,42 @@ async function moveBranchAfterNote(req) {
// we don't change utcDateModified so other changes are prioritized in case of conflict // we don't change utcDateModified so other changes are prioritized in case of conflict
// also we would have to sync all those modified branches otherwise hash checks would fail // also we would have to sync all those modified branches otherwise hash checks would fail
await sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
[afterNote.parentNoteId, afterNote.notePosition]); [afterNote.parentNoteId, afterNote.notePosition]);
await syncTableService.addNoteReorderingSync(afterNote.parentNoteId); syncTableService.addNoteReorderingSync(afterNote.parentNoteId);
const movedNotePosition = afterNote.notePosition + 10; const movedNotePosition = afterNote.notePosition + 10;
if (branchToMove.parentNoteId === afterNote.parentNoteId) { if (branchToMove.parentNoteId === afterNote.parentNoteId) {
branchToMove.notePosition = movedNotePosition; branchToMove.notePosition = movedNotePosition;
await branchToMove.save(); branchToMove.save();
} }
else { else {
const newBranch = branchToMove.createClone(afterNote.parentNoteId, movedNotePosition); const newBranch = branchToMove.createClone(afterNote.parentNoteId, movedNotePosition);
await newBranch.save(); newBranch.save();
branchToMove.isDeleted = true; branchToMove.isDeleted = true;
await branchToMove.save(); branchToMove.save();
} }
return { success: true }; return { success: true };
} }
async function setExpanded(req) { function setExpanded(req) {
const {branchId, expanded} = req.params; const {branchId, expanded} = req.params;
if (branchId !== 'root') { if (branchId !== 'root') {
await sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]); sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]);
// we don't sync expanded label // we don't sync expanded label
// also this does not trigger updates to the frontend, this would trigger too many reloads // also this does not trigger updates to the frontend, this would trigger too many reloads
} }
} }
async function setExpandedForSubtree(req) { function setExpandedForSubtree(req) {
const {branchId, expanded} = req.params; const {branchId, expanded} = req.params;
let branchIds = await sql.getColumn(` let branchIds = sql.getColumn(`
WITH RECURSIVE WITH RECURSIVE
tree(branchId, noteId) AS ( tree(branchId, noteId) AS (
SELECT branchId, noteId FROM branches WHERE branchId = ? SELECT branchId, noteId FROM branches WHERE branchId = ?
@ -146,20 +146,20 @@ async function setExpandedForSubtree(req) {
// root is always expanded // root is always expanded
branchIds = branchIds.filter(branchId => branchId !== 'root'); branchIds = branchIds.filter(branchId => branchId !== 'root');
await sql.executeMany(`UPDATE branches SET isExpanded = ${expanded} WHERE branchId IN (???)`, branchIds); sql.executeMany(`UPDATE branches SET isExpanded = ${expanded} WHERE branchId IN (???)`, branchIds);
return { return {
branchIds branchIds
}; };
} }
async function deleteBranch(req) { function deleteBranch(req) {
const last = req.query.last === 'true'; const last = req.query.last === 'true';
const branch = await repository.getBranch(req.params.branchId); const branch = repository.getBranch(req.params.branchId);
const taskContext = TaskContext.getInstance(req.query.taskId, 'delete-notes'); const taskContext = TaskContext.getInstance(req.query.taskId, 'delete-notes');
const deleteId = utils.randomString(10); const deleteId = utils.randomString(10);
const noteDeleted = await noteService.deleteBranch(branch, deleteId, taskContext); const noteDeleted = noteService.deleteBranch(branch, deleteId, taskContext);
if (last) { if (last) {
taskContext.taskSucceeded(); taskContext.taskSucceeded();
@ -170,13 +170,13 @@ async function deleteBranch(req) {
}; };
} }
async function setPrefix(req) { function setPrefix(req) {
const branchId = req.params.branchId; const branchId = req.params.branchId;
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix; const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
const branch = await repository.getBranch(branchId); const branch = repository.getBranch(branchId);
branch.prefix = prefix; branch.prefix = prefix;
await branch.save(); branch.save();
} }
module.exports = { module.exports = {

View File

@ -12,11 +12,11 @@ const utils = require('../../services/utils');
const path = require('path'); const path = require('path');
const Attribute = require('../../entities/attribute'); const Attribute = require('../../entities/attribute');
async function findClippingNote(todayNote, pageUrl) { function findClippingNote(todayNote, pageUrl) {
const notes = await todayNote.getDescendantNotesWithLabel('pageUrl', pageUrl); const notes = todayNote.getDescendantNotesWithLabel('pageUrl', pageUrl);
for (const note of notes) { for (const note of notes) {
if (await note.getOwnedLabelValue('clipType') === 'clippings') { if (note.getOwnedLabelValue('clipType') === 'clippings') {
return note; return note;
} }
} }
@ -24,76 +24,76 @@ async function findClippingNote(todayNote, pageUrl) {
return null; return null;
} }
async function getClipperInboxNote() { function getClipperInboxNote() {
let clipperInbox = await attributeService.getNoteWithLabel('clipperInbox'); let clipperInbox = attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) { if (!clipperInbox) {
clipperInbox = await dateNoteService.getDateNote(dateUtils.localNowDate()); clipperInbox = dateNoteService.getDateNote(dateUtils.localNowDate());
} }
return clipperInbox; return clipperInbox;
} }
async function addClipping(req) { function addClipping(req) {
const {title, content, pageUrl, images} = req.body; const {title, content, pageUrl, images} = req.body;
const clipperInbox = await getClipperInboxNote(); const clipperInbox = getClipperInboxNote();
let clippingNote = await findClippingNote(clipperInbox, pageUrl); let clippingNote = findClippingNote(clipperInbox, pageUrl);
if (!clippingNote) { if (!clippingNote) {
clippingNote = (await noteService.createNewNote({ clippingNote = (noteService.createNewNote({
parentNoteId: clipperInbox.noteId, parentNoteId: clipperInbox.noteId,
title: title, title: title,
content: '', content: '',
type: 'text' type: 'text'
})).note; })).note;
await clippingNote.setLabel('clipType', 'clippings'); clippingNote.setLabel('clipType', 'clippings');
await clippingNote.setLabel('pageUrl', pageUrl); clippingNote.setLabel('pageUrl', pageUrl);
} }
const rewrittenContent = await addImagesToNote(images, clippingNote, content); const rewrittenContent = addImagesToNote(images, clippingNote, content);
const existingContent = await clippingNote.getContent(); const existingContent = clippingNote.getContent();
await clippingNote.setContent(existingContent + (existingContent.trim() ? "<br/>" : "") + rewrittenContent); clippingNote.setContent(existingContent + (existingContent.trim() ? "<br/>" : "") + rewrittenContent);
return { return {
noteId: clippingNote.noteId noteId: clippingNote.noteId
}; };
} }
async function createNote(req) { function createNote(req) {
const {title, content, pageUrl, images, clipType} = req.body; const {title, content, pageUrl, images, clipType} = req.body;
log.info(`Creating clipped note from ${pageUrl}`); log.info(`Creating clipped note from ${pageUrl}`);
const clipperInbox = await getClipperInboxNote(); const clipperInbox = getClipperInboxNote();
const {note} = await noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId: clipperInbox.noteId, parentNoteId: clipperInbox.noteId,
title, title,
content, content,
type: 'text' type: 'text'
}); });
await note.setLabel('clipType', clipType); note.setLabel('clipType', clipType);
if (pageUrl) { if (pageUrl) {
await note.setLabel('pageUrl', pageUrl); note.setLabel('pageUrl', pageUrl);
} }
const rewrittenContent = await addImagesToNote(images, note, content); const rewrittenContent = addImagesToNote(images, note, content);
await note.setContent(rewrittenContent); note.setContent(rewrittenContent);
return { return {
noteId: note.noteId noteId: note.noteId
}; };
} }
async function addImagesToNote(images, note, content) { function addImagesToNote(images, note, content) {
let rewrittenContent = content; let rewrittenContent = content;
if (images) { if (images) {
@ -107,15 +107,15 @@ async function addImagesToNote(images, note, content) {
const buffer = Buffer.from(dataUrl.split(",")[1], 'base64'); const buffer = Buffer.from(dataUrl.split(",")[1], 'base64');
const {note: imageNote, url} = await imageService.saveImage(note.noteId, buffer, filename, true); const {note: imageNote, url} = imageService.saveImage(note.noteId, buffer, filename, true);
await new Attribute({ new Attribute({
noteId: imageNote.noteId, noteId: imageNote.noteId,
type: 'label', type: 'label',
name: 'hideInAutocomplete' name: 'hideInAutocomplete'
}).save(); }).save();
await new Attribute({ new Attribute({
noteId: note.noteId, noteId: note.noteId,
type: 'relation', type: 'relation',
name: 'imageLink', name: 'imageLink',
@ -131,7 +131,7 @@ async function addImagesToNote(images, note, content) {
return rewrittenContent; return rewrittenContent;
} }
async function openNote(req) { function openNote(req) {
if (utils.isElectron()) { if (utils.isElectron()) {
ws.sendMessageToAllClients({ ws.sendMessageToAllClients({
type: 'open-note', type: 'open-note',
@ -149,7 +149,7 @@ async function openNote(req) {
} }
} }
async function handshake() { function handshake() {
return { return {
appName: "trilium", appName: "trilium",
protocolVersion: appInfo.clipperProtocolVersion protocolVersion: appInfo.clipperProtocolVersion

View File

@ -2,17 +2,17 @@
const cloningService = require('../../services/cloning'); const cloningService = require('../../services/cloning');
async function cloneNoteToParent(req) { function cloneNoteToParent(req) {
const {noteId, parentBranchId} = req.params; const {noteId, parentBranchId} = req.params;
const {prefix} = req.body; const {prefix} = req.body;
return await cloningService.cloneNoteToParent(noteId, parentBranchId, prefix); return cloningService.cloneNoteToParent(noteId, parentBranchId, prefix);
} }
async function cloneNoteAfter(req) { function cloneNoteAfter(req) {
const {noteId, afterBranchId} = req.params; const {noteId, afterBranchId} = req.params;
return await cloningService.cloneNoteAfter(noteId, afterBranchId); return cloningService.cloneNoteAfter(noteId, afterBranchId);
} }
module.exports = { module.exports = {

View File

@ -5,24 +5,24 @@ const log = require('../../services/log');
const backupService = require('../../services/backup'); const backupService = require('../../services/backup');
const consistencyChecksService = require('../../services/consistency_checks'); const consistencyChecksService = require('../../services/consistency_checks');
async function anonymize() { function anonymize() {
return await backupService.anonymize(); return backupService.anonymize();
} }
async function backupDatabase() { function backupDatabase() {
return { return {
backupFile: await backupService.backupNow("now") backupFile: backupService.backupNow("now")
}; };
} }
async function vacuumDatabase() { function vacuumDatabase() {
await sql.execute("VACUUM"); sql.execute("VACUUM");
log.info("Database has been vacuumed."); log.info("Database has been vacuumed.");
} }
async function findAndFixConsistencyIssues() { function findAndFixConsistencyIssues() {
await consistencyChecksService.runOnDemandChecks(true); consistencyChecksService.runOnDemandChecks(true);
} }
module.exports = { module.exports = {

View File

@ -5,19 +5,19 @@ const sql = require('../../services/sql');
const dateUtils = require('../../services/date_utils'); const dateUtils = require('../../services/date_utils');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
async function getDateNote(req) { function getDateNote(req) {
return await dateNoteService.getDateNote(req.params.date); return dateNoteService.getDateNote(req.params.date);
} }
async function getMonthNote(req) { function getMonthNote(req) {
return await dateNoteService.getMonthNote(req.params.month); return dateNoteService.getMonthNote(req.params.month);
} }
async function getYearNote(req) { function getYearNote(req) {
return await dateNoteService.getYearNote(req.params.year); return dateNoteService.getYearNote(req.params.year);
} }
async function getDateNotesForMonth(req) { function getDateNotesForMonth(req) {
const month = req.params.month; const month = req.params.month;
return sql.getMap(` return sql.getMap(`
@ -33,12 +33,12 @@ async function getDateNotesForMonth(req) {
AND attr.value LIKE '${month}%'`); AND attr.value LIKE '${month}%'`);
} }
async function createSqlConsole() { function createSqlConsole() {
const today = dateUtils.localNowDate(); const today = dateUtils.localNowDate();
const todayNote = await dateNoteService.getDateNote(today); const todayNote = dateNoteService.getDateNote(today);
const {note} = await noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId: todayNote.noteId, parentNoteId: todayNote.noteId,
title: 'SQL Console', title: 'SQL Console',
content: "SELECT title, isDeleted, isProtected FROM notes WHERE noteId = ''\n\n\n\n", content: "SELECT title, isDeleted, isProtected FROM notes WHERE noteId = ''\n\n\n\n",
@ -46,7 +46,7 @@ async function createSqlConsole() {
mime: 'text/x-sqlite;schema=trilium' mime: 'text/x-sqlite;schema=trilium'
}); });
await note.setLabel("sqlConsole", today); note.setLabel("sqlConsole", today);
return note; return note;
} }

View File

@ -7,9 +7,9 @@ const repository = require("../../services/repository");
const TaskContext = require("../../services/task_context"); const TaskContext = require("../../services/task_context");
const log = require("../../services/log"); const log = require("../../services/log");
async function exportBranch(req, res) { function exportBranch(req, res) {
const {branchId, type, format, version, taskId} = req.params; const {branchId, type, format, version, taskId} = req.params;
const branch = await repository.getBranch(branchId); const branch = repository.getBranch(branchId);
if (!branch) { if (!branch) {
const message = `Cannot export branch ${branchId} since it does not exist.`; const message = `Cannot export branch ${branchId} since it does not exist.`;
@ -25,15 +25,15 @@ async function exportBranch(req, res) {
if (type === 'subtree' && (format === 'html' || format === 'markdown')) { if (type === 'subtree' && (format === 'html' || format === 'markdown')) {
const start = Date.now(); const start = Date.now();
await zipExportService.exportToZip(taskContext, branch, format, res); zipExportService.exportToZip(taskContext, branch, format, res);
console.log("Export took", Date.now() - start, "ms"); console.log("Export took", Date.now() - start, "ms");
} }
else if (type === 'single') { else if (type === 'single') {
await singleExportService.exportSingleNote(taskContext, branch, format, res); singleExportService.exportSingleNote(taskContext, branch, format, res);
} }
else if (format === 'opml') { else if (format === 'opml') {
await opmlExportService.exportToOpml(taskContext, branch, version, res); opmlExportService.exportToOpml(taskContext, branch, version, res);
} }
else { else {
return [404, "Unrecognized export format " + format]; return [404, "Unrecognized export format " + format];

View File

@ -5,33 +5,33 @@ const repository = require('../../services/repository');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const noteRevisionService = require('../../services/note_revisions'); const noteRevisionService = require('../../services/note_revisions');
async function updateFile(req) { function updateFile(req) {
const {noteId} = req.params; const {noteId} = req.params;
const file = req.file; const file = req.file;
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (!note) { if (!note) {
return [404, `Note ${noteId} doesn't exist.`]; return [404, `Note ${noteId} doesn't exist.`];
} }
await noteRevisionService.createNoteRevision(note); noteRevisionService.createNoteRevision(note);
note.mime = file.mimetype.toLowerCase(); note.mime = file.mimetype.toLowerCase();
await note.setContent(file.buffer); note.setContent(file.buffer);
await note.setLabel('originalFileName', file.originalname); note.setLabel('originalFileName', file.originalname);
await noteRevisionService.protectNoteRevisions(note); noteRevisionService.protectNoteRevisions(note);
return { return {
uploaded: true uploaded: true
}; };
} }
async function downloadNoteFile(noteId, res, contentDisposition = true) { function downloadNoteFile(noteId, res, contentDisposition = true) {
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (!note) { if (!note) {
return res.status(404).send(`Note ${noteId} doesn't exist.`); return res.status(404).send(`Note ${noteId} doesn't exist.`);
@ -51,19 +51,19 @@ async function downloadNoteFile(noteId, res, contentDisposition = true) {
res.setHeader('Content-Type', note.mime); res.setHeader('Content-Type', note.mime);
res.send(await note.getContent()); res.send(note.getContent());
} }
async function downloadFile(req, res) { function downloadFile(req, res) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
return await downloadNoteFile(noteId, res); return downloadNoteFile(noteId, res);
} }
async function openFile(req, res) { function openFile(req, res) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
return await downloadNoteFile(noteId, res, false); return downloadNoteFile(noteId, res, false);
} }
module.exports = { module.exports = {

View File

@ -5,8 +5,8 @@ const repository = require('../../services/repository');
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const fs = require('fs'); const fs = require('fs');
async function returnImage(req, res) { function returnImage(req, res) {
const image = await repository.getNote(req.params.noteId); const image = repository.getNote(req.params.noteId);
if (!image) { if (!image) {
return res.sendStatus(404); return res.sendStatus(404);
@ -21,14 +21,14 @@ async function returnImage(req, res) {
res.set('Content-Type', image.mime); res.set('Content-Type', image.mime);
res.send(await image.getContent()); res.send(image.getContent());
} }
async function uploadImage(req) { function uploadImage(req) {
const {noteId} = req.query; const {noteId} = req.query;
const {file} = req; const {file} = req;
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (!note) { if (!note) {
return [404, `Note ${noteId} doesn't exist.`]; return [404, `Note ${noteId} doesn't exist.`];
@ -38,7 +38,7 @@ async function uploadImage(req) {
return [400, "Unknown image type: " + file.mimetype]; return [400, "Unknown image type: " + file.mimetype];
} }
const {url} = await imageService.saveImage(noteId, file.buffer, file.originalname, true); const {url} = imageService.saveImage(noteId, file.buffer, file.originalname, true);
return { return {
uploaded: true, uploaded: true,
@ -46,11 +46,11 @@ async function uploadImage(req) {
}; };
} }
async function updateImage(req) { function updateImage(req) {
const {noteId} = req.params; const {noteId} = req.params;
const {file} = req; const {file} = req;
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (!note) { if (!note) {
return [404, `Note ${noteId} doesn't exist.`]; return [404, `Note ${noteId} doesn't exist.`];
@ -63,7 +63,7 @@ async function updateImage(req) {
}; };
} }
await imageService.updateImage(noteId, file.buffer, file.originalname); imageService.updateImage(noteId, file.buffer, file.originalname);
return { uploaded: true }; return { uploaded: true };
} }

View File

@ -12,7 +12,7 @@ const noteCacheService = require('../../services/note_cache/note_cache.js');
const log = require('../../services/log'); const log = require('../../services/log');
const TaskContext = require('../../services/task_context.js'); const TaskContext = require('../../services/task_context.js');
async function importToBranch(req) { function importToBranch(req) {
const {parentNoteId} = req.params; const {parentNoteId} = req.params;
const {taskId, last} = req.body; const {taskId, last} = req.body;
@ -31,7 +31,7 @@ async function importToBranch(req) {
return [400, "No file has been uploaded"]; return [400, "No file has been uploaded"];
} }
const parentNote = await repository.getNote(parentNoteId); const parentNote = repository.getNote(parentNoteId);
if (!parentNote) { if (!parentNote) {
return [404, `Note ${parentNoteId} doesn't exist.`]; return [404, `Note ${parentNoteId} doesn't exist.`];
@ -49,19 +49,19 @@ async function importToBranch(req) {
try { try {
if (extension === '.tar' && options.explodeArchives) { if (extension === '.tar' && options.explodeArchives) {
note = await tarImportService.importTar(taskContext, file.buffer, parentNote); note = tarImportService.importTar(taskContext, file.buffer, parentNote);
} else if (extension === '.zip' && options.explodeArchives) { } else if (extension === '.zip' && options.explodeArchives) {
const start = Date.now(); const start = Date.now();
note = await zipImportService.importZip(taskContext, file.buffer, parentNote); note = zipImportService.importZip(taskContext, file.buffer, parentNote);
console.log("Import took", Date.now() - start, "ms"); console.log("Import took", Date.now() - start, "ms");
} else if (extension === '.opml' && options.explodeArchives) { } else if (extension === '.opml' && options.explodeArchives) {
note = await opmlImportService.importOpml(taskContext, file.buffer, parentNote); note = opmlImportService.importOpml(taskContext, file.buffer, parentNote);
} else if (extension === '.enex' && options.explodeArchives) { } else if (extension === '.enex' && options.explodeArchives) {
note = await enexImportService.importEnex(taskContext, file, parentNote); note = enexImportService.importEnex(taskContext, file, parentNote);
} else { } else {
note = await singleImportService.importSingleFile(taskContext, file, parentNote); note = singleImportService.importSingleFile(taskContext, file, parentNote);
} }
} }
catch (e) { catch (e) {

View File

@ -3,12 +3,12 @@
const keyboardActions = require('../../services/keyboard_actions'); const keyboardActions = require('../../services/keyboard_actions');
const sql = require('../../services/sql'); const sql = require('../../services/sql');
async function getKeyboardActions() { function getKeyboardActions() {
return await keyboardActions.getKeyboardActions(); return keyboardActions.getKeyboardActions();
} }
async function getShortcutsForNotes() { function getShortcutsForNotes() {
return await sql.getMap(` return sql.getMap(`
SELECT value, noteId SELECT value, noteId
FROM attributes FROM attributes
WHERE isDeleted = 0 WHERE isDeleted = 0

View File

@ -2,8 +2,8 @@
const sql = require('../../services/sql'); const sql = require('../../services/sql');
async function getRelations(noteIds) { function getRelations(noteIds) {
return (await sql.getManyRows(` return (sql.getManyRows(`
SELECT noteId, name, value AS targetNoteId SELECT noteId, name, value AS targetNoteId
FROM attributes FROM attributes
WHERE (noteId IN (???) OR value IN (???)) WHERE (noteId IN (???) OR value IN (???))
@ -14,7 +14,7 @@ async function getRelations(noteIds) {
`, Array.from(noteIds))); `, Array.from(noteIds)));
} }
async function getLinkMap(req) { function getLinkMap(req) {
const {noteId} = req.params; const {noteId} = req.params;
const {maxNotes, maxDepth} = req.body; const {maxNotes, maxDepth} = req.body;
@ -24,7 +24,7 @@ async function getLinkMap(req) {
let depth = 0; let depth = 0;
while (noteIds.size < maxNotes) { while (noteIds.size < maxNotes) {
relations = await getRelations(noteIds); relations = getRelations(noteIds);
if (depth === maxDepth) { if (depth === maxDepth) {
break; break;

View File

@ -14,8 +14,8 @@ const sql = require('../../services/sql');
const optionService = require('../../services/options'); const optionService = require('../../services/options');
const ApiToken = require('../../entities/api_token'); const ApiToken = require('../../entities/api_token');
async function loginSync(req) { function loginSync(req) {
if (!await sqlInit.schemaExists()) { if (!sqlInit.schemaExists()) {
return [500, { message: "DB schema does not exist, can't sync." }]; return [500, { message: "DB schema does not exist, can't sync." }];
} }
@ -36,7 +36,7 @@ async function loginSync(req) {
return [400, { message: `Non-matching sync versions, local is version ${appInfo.syncVersion}, remote is ${syncVersion}. It is recommended to run same version of Trilium on both sides of sync.` }]; return [400, { message: `Non-matching sync versions, local is version ${appInfo.syncVersion}, remote is ${syncVersion}. It is recommended to run same version of Trilium on both sides of sync.` }];
} }
const documentSecret = await options.getOption('documentSecret'); const documentSecret = options.getOption('documentSecret');
const expectedHash = utils.hmac(documentSecret, timestampStr); const expectedHash = utils.hmac(documentSecret, timestampStr);
const givenHash = req.body.hash; const givenHash = req.body.hash;
@ -49,28 +49,28 @@ async function loginSync(req) {
return { return {
sourceId: sourceIdService.getCurrentSourceId(), sourceId: sourceIdService.getCurrentSourceId(),
maxSyncId: await sql.getValue("SELECT MAX(id) FROM sync WHERE isSynced = 1") maxSyncId: sql.getValue("SELECT MAX(id) FROM sync WHERE isSynced = 1")
}; };
} }
async function loginToProtectedSession(req) { function loginToProtectedSession(req) {
const password = req.body.password; const password = req.body.password;
if (!await passwordEncryptionService.verifyPassword(password)) { if (!passwordEncryptionService.verifyPassword(password)) {
return { return {
success: false, success: false,
message: "Given current password doesn't match hash" message: "Given current password doesn't match hash"
}; };
} }
const decryptedDataKey = await passwordEncryptionService.getDataKey(password); const decryptedDataKey = passwordEncryptionService.getDataKey(password);
const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey); const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey);
// this is set here so that event handlers have access to the protected session // this is set here so that event handlers have access to the protected session
cls.set('protectedSessionId', protectedSessionId); cls.set('protectedSessionId', protectedSessionId);
await eventService.emit(eventService.ENTER_PROTECTED_SESSION); eventService.emit(eventService.ENTER_PROTECTED_SESSION);
return { return {
success: true, success: true,
@ -78,18 +78,18 @@ async function loginToProtectedSession(req) {
}; };
} }
async function token(req) { function token(req) {
const username = req.body.username; const username = req.body.username;
const password = req.body.password; const password = req.body.password;
const isUsernameValid = username === await optionService.getOption('username'); const isUsernameValid = username === optionService.getOption('username');
const isPasswordValid = await passwordEncryptionService.verifyPassword(password); const isPasswordValid = passwordEncryptionService.verifyPassword(password);
if (!isUsernameValid || !isPasswordValid) { if (!isUsernameValid || !isPasswordValid) {
return [401, "Incorrect username/password"]; return [401, "Incorrect username/password"];
} }
const apiToken = await new ApiToken({ const apiToken = new ApiToken({
token: utils.randomSecureToken() token: utils.randomSecureToken()
}).save(); }).save();

View File

@ -7,23 +7,23 @@ const noteRevisionService = require('../../services/note_revisions');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const path = require('path'); const path = require('path');
async function getNoteRevisions(req) { function getNoteRevisions(req) {
return await repository.getEntities(` return repository.getEntities(`
SELECT * FROM note_revisions SELECT * FROM note_revisions
WHERE noteId = ? AND isErased = 0 WHERE noteId = ? AND isErased = 0
ORDER BY utcDateCreated DESC`, [req.params.noteId]); ORDER BY utcDateCreated DESC`, [req.params.noteId]);
} }
async function getNoteRevision(req) { function getNoteRevision(req) {
const noteRevision = await repository.getNoteRevision(req.params.noteRevisionId); const noteRevision = repository.getNoteRevision(req.params.noteRevisionId);
if (noteRevision.type === 'file') { if (noteRevision.type === 'file') {
if (noteRevision.isStringNote()) { if (noteRevision.isStringNote()) {
noteRevision.content = (await noteRevision.getContent()).substr(0, 10000); noteRevision.content = (noteRevision.getContent()).substr(0, 10000);
} }
} }
else { else {
noteRevision.content = await noteRevision.getContent(); noteRevision.content = noteRevision.getContent();
if (noteRevision.content && noteRevision.type === 'image') { if (noteRevision.content && noteRevision.type === 'image') {
noteRevision.content = noteRevision.content.toString('base64'); noteRevision.content = noteRevision.content.toString('base64');
@ -57,8 +57,8 @@ function getRevisionFilename(noteRevision) {
return filename; return filename;
} }
async function downloadNoteRevision(req, res) { function downloadNoteRevision(req, res) {
const noteRevision = await repository.getNoteRevision(req.params.noteRevisionId); const noteRevision = repository.getNoteRevision(req.params.noteRevisionId);
if (noteRevision.noteId !== req.params.noteId) { if (noteRevision.noteId !== req.params.noteId) {
return res.status(400).send(`Note revision ${req.params.noteRevisionId} does not belong to note ${req.params.noteId}`); return res.status(400).send(`Note revision ${req.params.noteRevisionId} does not belong to note ${req.params.noteId}`);
@ -73,55 +73,55 @@ async function downloadNoteRevision(req, res) {
res.setHeader('Content-Disposition', utils.getContentDisposition(filename)); res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
res.setHeader('Content-Type', noteRevision.mime); res.setHeader('Content-Type', noteRevision.mime);
res.send(await noteRevision.getContent()); res.send(noteRevision.getContent());
} }
/** /**
* @param {NoteRevision} noteRevision * @param {NoteRevision} noteRevision
*/ */
async function eraseOneNoteRevision(noteRevision) { function eraseOneNoteRevision(noteRevision) {
noteRevision.isErased = true; noteRevision.isErased = true;
noteRevision.title = null; noteRevision.title = null;
await noteRevision.setContent(null); noteRevision.setContent(null);
await noteRevision.save(); noteRevision.save();
} }
async function eraseAllNoteRevisions(req) { function eraseAllNoteRevisions(req) {
const noteRevisionsToErase = await repository.getEntities( const noteRevisionsToErase = repository.getEntities(
'SELECT * FROM note_revisions WHERE isErased = 0 AND noteId = ?', 'SELECT * FROM note_revisions WHERE isErased = 0 AND noteId = ?',
[req.params.noteId]); [req.params.noteId]);
for (const noteRevision of noteRevisionsToErase) { for (const noteRevision of noteRevisionsToErase) {
await eraseOneNoteRevision(noteRevision); eraseOneNoteRevision(noteRevision);
} }
} }
async function eraseNoteRevision(req) { function eraseNoteRevision(req) {
const noteRevision = await repository.getNoteRevision(req.params.noteRevisionId); const noteRevision = repository.getNoteRevision(req.params.noteRevisionId);
if (noteRevision && !noteRevision.isErased) { if (noteRevision && !noteRevision.isErased) {
await eraseOneNoteRevision(noteRevision); eraseOneNoteRevision(noteRevision);
} }
} }
async function restoreNoteRevision(req) { function restoreNoteRevision(req) {
const noteRevision = await repository.getNoteRevision(req.params.noteRevisionId); const noteRevision = repository.getNoteRevision(req.params.noteRevisionId);
if (noteRevision && !noteRevision.isErased) { if (noteRevision && !noteRevision.isErased) {
const note = await noteRevision.getNote(); const note = noteRevision.getNote();
await noteRevisionService.createNoteRevision(note); noteRevisionService.createNoteRevision(note);
note.title = noteRevision.title; note.title = noteRevision.title;
await note.setContent(await noteRevision.getContent()); note.setContent(noteRevision.getContent());
await note.save(); note.save();
} }
} }
async function getEditedNotesOnDate(req) { function getEditedNotesOnDate(req) {
const date = utils.sanitizeSql(req.params.date); const date = utils.sanitizeSql(req.params.date);
const notes = await repository.getEntities(` const notes = repository.getEntities(`
SELECT notes.* SELECT notes.*
FROM notes FROM notes
WHERE noteId IN ( WHERE noteId IN (

View File

@ -6,16 +6,16 @@ const repository = require('../../services/repository');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
const TaskContext = require('../../services/task_context'); const TaskContext = require('../../services/task_context');
async function getNote(req) { function getNote(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (!note) { if (!note) {
return [404, "Note " + noteId + " has not been found."]; return [404, "Note " + noteId + " has not been found."];
} }
if (note.isStringNote()) { if (note.isStringNote()) {
note.content = await note.getContent(); note.content = note.getContent();
if (note.type === 'file') { if (note.type === 'file') {
note.content = note.content.substr(0, 10000); note.content = note.content.substr(0, 10000);
@ -25,13 +25,13 @@ async function getNote(req) {
return note; return note;
} }
async function createNote(req) { function createNote(req) {
const params = Object.assign({}, req.body); // clone const params = Object.assign({}, req.body); // clone
params.parentNoteId = req.params.parentNoteId; params.parentNoteId = req.params.parentNoteId;
const { target, targetBranchId } = req.query; const { target, targetBranchId } = req.query;
const { note, branch } = await noteService.createNewNoteWithTarget(target, targetBranchId, params); const { note, branch } = noteService.createNewNoteWithTarget(target, targetBranchId, params);
return { return {
note, note,
@ -39,14 +39,14 @@ async function createNote(req) {
}; };
} }
async function updateNote(req) { function updateNote(req) {
const note = req.body; const note = req.body;
const noteId = req.params.noteId; const noteId = req.params.noteId;
return await noteService.updateNote(noteId, note); return noteService.updateNote(noteId, note);
} }
async function deleteNote(req) { function deleteNote(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const taskId = req.query.taskId; const taskId = req.query.taskId;
const last = req.query.last === 'true'; const last = req.query.last === 'true';
@ -54,61 +54,61 @@ async function deleteNote(req) {
// note how deleteId is separate from taskId - single taskId produces separate deleteId for each "top level" deleted note // note how deleteId is separate from taskId - single taskId produces separate deleteId for each "top level" deleted note
const deleteId = utils.randomString(10); const deleteId = utils.randomString(10);
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
const taskContext = TaskContext.getInstance(taskId, 'delete-notes'); const taskContext = TaskContext.getInstance(taskId, 'delete-notes');
for (const branch of await note.getBranches()) { for (const branch of note.getBranches()) {
await noteService.deleteBranch(branch, deleteId, taskContext); noteService.deleteBranch(branch, deleteId, taskContext);
} }
if (last) { if (last) {
await taskContext.taskSucceeded(); taskContext.taskSucceeded();
} }
} }
async function undeleteNote(req) { function undeleteNote(req) {
const note = await repository.getNote(req.params.noteId); const note = repository.getNote(req.params.noteId);
const taskContext = TaskContext.getInstance(utils.randomString(10), 'undelete-notes'); const taskContext = TaskContext.getInstance(utils.randomString(10), 'undelete-notes');
await noteService.undeleteNote(note, note.deleteId, taskContext); noteService.undeleteNote(note, note.deleteId, taskContext);
await taskContext.taskSucceeded(); taskContext.taskSucceeded();
} }
async function sortNotes(req) { function sortNotes(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
await treeService.sortNotesAlphabetically(noteId); treeService.sortNotesAlphabetically(noteId);
} }
async function protectNote(req) { function protectNote(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
const protect = !!parseInt(req.params.isProtected); const protect = !!parseInt(req.params.isProtected);
const includingSubTree = !!parseInt(req.query.subtree); const includingSubTree = !!parseInt(req.query.subtree);
const taskContext = new TaskContext(utils.randomString(10), 'protect-notes', {protect}); const taskContext = new TaskContext(utils.randomString(10), 'protect-notes', {protect});
await noteService.protectNoteRecursively(note, protect, includingSubTree, taskContext); noteService.protectNoteRecursively(note, protect, includingSubTree, taskContext);
taskContext.taskSucceeded(); taskContext.taskSucceeded();
} }
async function setNoteTypeMime(req) { function setNoteTypeMime(req) {
// can't use [] destructuring because req.params is not iterable // can't use [] destructuring because req.params is not iterable
const noteId = req.params[0]; const noteId = req.params[0];
const type = req.params[1]; const type = req.params[1];
const mime = req.params[2]; const mime = req.params[2];
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
note.type = type; note.type = type;
note.mime = mime; note.mime = mime;
await note.save(); note.save();
} }
async function getRelationMap(req) { function getRelationMap(req) {
const noteIds = req.body.noteIds; const noteIds = req.body.noteIds;
const resp = { const resp = {
// noteId => title // noteId => title
@ -126,12 +126,12 @@ async function getRelationMap(req) {
const questionMarks = noteIds.map(noteId => '?').join(','); const questionMarks = noteIds.map(noteId => '?').join(',');
const notes = await repository.getEntities(`SELECT * FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); const notes = repository.getEntities(`SELECT * FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
for (const note of notes) { for (const note of notes) {
resp.noteTitles[note.noteId] = note.title; resp.noteTitles[note.noteId] = note.title;
resp.relations = resp.relations.concat((await note.getRelations()) resp.relations = resp.relations.concat((note.getRelations())
.filter(relation => noteIds.includes(relation.value)) .filter(relation => noteIds.includes(relation.value))
.map(relation => ({ .map(relation => ({
attributeId: relation.attributeId, attributeId: relation.attributeId,
@ -140,7 +140,7 @@ async function getRelationMap(req) {
name: relation.name name: relation.name
}))); })));
for (const relationDefinition of await note.getRelationDefinitions()) { for (const relationDefinition of note.getRelationDefinitions()) {
if (relationDefinition.value.inverseRelation) { if (relationDefinition.value.inverseRelation) {
resp.inverseRelations[relationDefinition.name] = relationDefinition.value.inverseRelation; resp.inverseRelations[relationDefinition.name] = relationDefinition.value.inverseRelation;
} }
@ -150,11 +150,11 @@ async function getRelationMap(req) {
return resp; return resp;
} }
async function changeTitle(req) { function changeTitle(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const title = req.body.title; const title = req.body.title;
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (!note) { if (!note) {
return [404, `Note ${noteId} has not been found`]; return [404, `Note ${noteId} has not been found`];
@ -166,15 +166,15 @@ async function changeTitle(req) {
note.title = title; note.title = title;
await note.save(); note.save();
return note; return note;
} }
async function duplicateNote(req) { function duplicateNote(req) {
const {noteId, parentNoteId} = req.params; const {noteId, parentNoteId} = req.params;
return await noteService.duplicateNote(noteId, parentNoteId); return noteService.duplicateNote(noteId, parentNoteId);
} }
module.exports = { module.exports = {

View File

@ -40,8 +40,8 @@ const ALLOWED_OPTIONS = new Set([
'nativeTitleBarVisible' 'nativeTitleBarVisible'
]); ]);
async function getOptions() { function getOptions() {
const optionMap = await optionService.getOptionsMap(); const optionMap = optionService.getOptionsMap();
const resultMap = {}; const resultMap = {};
for (const optionName in optionMap) { for (const optionName in optionMap) {
@ -53,17 +53,17 @@ async function getOptions() {
return resultMap; return resultMap;
} }
async function updateOption(req) { function updateOption(req) {
const {name, value} = req.params; const {name, value} = req.params;
if (!await update(name, value)) { if (!update(name, value)) {
return [400, "not allowed option to change"]; return [400, "not allowed option to change"];
} }
} }
async function updateOptions(req) { function updateOptions(req) {
for (const optionName in req.body) { for (const optionName in req.body) {
if (!await update(optionName, req.body[optionName])) { if (!update(optionName, req.body[optionName])) {
// this should be improved // this should be improved
// it should return 400 instead of current 500, but at least it now rollbacks transaction // it should return 400 instead of current 500, but at least it now rollbacks transaction
throw new Error(`${optionName} is not allowed to change`); throw new Error(`${optionName} is not allowed to change`);
@ -71,7 +71,7 @@ async function updateOptions(req) {
} }
} }
async function update(name, value) { function update(name, value) {
if (!isAllowed(name)) { if (!isAllowed(name)) {
return false; return false;
} }
@ -80,18 +80,18 @@ async function update(name, value) {
log.info(`Updating option ${name} to ${value}`); log.info(`Updating option ${name} to ${value}`);
} }
await optionService.setOption(name, value); optionService.setOption(name, value);
return true; return true;
} }
async function getUserThemes() { function getUserThemes() {
const notes = await attributes.getNotesWithLabel('appTheme'); const notes = attributes.getNotesWithLabel('appTheme');
const ret = []; const ret = [];
for (const note of notes) { for (const note of notes) {
let value = await note.getOwnedLabelValue('appTheme'); let value = note.getOwnedLabelValue('appTheme');
if (!value) { if (!value) {
value = note.title.toLowerCase().replace(/[^a-z0-9]/gi, '-'); value = note.title.toLowerCase().replace(/[^a-z0-9]/gi, '-');

View File

@ -2,8 +2,8 @@
const changePasswordService = require('../../services/change_password'); const changePasswordService = require('../../services/change_password');
async function changePassword(req) { function changePassword(req) {
return await changePasswordService.changePassword(req.body.current_password, req.body.new_password); return changePasswordService.changePassword(req.body.current_password, req.body.new_password);
} }
module.exports = { module.exports = {

View File

@ -5,12 +5,12 @@ const protectedSessionService = require('../../services/protected_session');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const noteCacheService = require('../../services/note_cache/note_cache.js'); const noteCacheService = require('../../services/note_cache/note_cache.js');
async function getRecentChanges(req) { function getRecentChanges(req) {
const {ancestorNoteId} = req.params; const {ancestorNoteId} = req.params;
let recentChanges = []; let recentChanges = [];
const noteRevisions = await sql.getRows(` const noteRevisions = sql.getRows(`
SELECT SELECT
notes.noteId, notes.noteId,
notes.isDeleted AS current_isDeleted, notes.isDeleted AS current_isDeleted,
@ -31,7 +31,7 @@ async function getRecentChanges(req) {
} }
} }
const notes = await sql.getRows(` const notes = sql.getRows(`
SELECT SELECT
notes.noteId, notes.noteId,
notes.isDeleted AS current_isDeleted, notes.isDeleted AS current_isDeleted,
@ -75,7 +75,7 @@ async function getRecentChanges(req) {
else { else {
const deleteId = change.current_deleteId; const deleteId = change.current_deleteId;
const undeletedParentBranches = await noteService.getUndeletedParentBranches(change.noteId, deleteId); const undeletedParentBranches = noteService.getUndeletedParentBranches(change.noteId, deleteId);
// note (and the subtree) can be undeleted if there's at least one undeleted parent (whose branch would be undeleted by this op) // note (and the subtree) can be undeleted if there's at least one undeleted parent (whose branch would be undeleted by this op)
change.canBeUndeleted = undeletedParentBranches.length > 0; change.canBeUndeleted = undeletedParentBranches.length > 0;

View File

@ -2,8 +2,8 @@
const RecentNote = require('../../entities/recent_note'); const RecentNote = require('../../entities/recent_note');
async function addRecentNote(req) { function addRecentNote(req) {
await new RecentNote({ new RecentNote({
noteId: req.body.noteId, noteId: req.body.noteId,
notePath: req.body.notePath notePath: req.body.notePath
}).save(); }).save();

View File

@ -5,15 +5,15 @@ const attributeService = require('../../services/attributes');
const repository = require('../../services/repository'); const repository = require('../../services/repository');
const syncService = require('../../services/sync'); const syncService = require('../../services/sync');
async function exec(req) { function exec(req) {
try { try {
const result = await scriptService.executeScript(req.body.script, req.body.params, req.body.startNoteId, const result = scriptService.executeScript(req.body.script, req.body.params, req.body.startNoteId,
req.body.currentNoteId, req.body.originEntityName, req.body.originEntityId); req.body.currentNoteId, req.body.originEntityName, req.body.originEntityId);
return { return {
success: true, success: true,
executionResult: result, executionResult: result,
maxSyncId: await syncService.getMaxSyncId() maxSyncId: syncService.getMaxSyncId()
}; };
} }
catch (e) { catch (e) {
@ -21,21 +21,21 @@ async function exec(req) {
} }
} }
async function run(req) { function run(req) {
const note = await repository.getNote(req.params.noteId); const note = repository.getNote(req.params.noteId);
const result = await scriptService.executeNote(note, { originEntity: note }); const result = scriptService.executeNote(note, { originEntity: note });
return { executionResult: result }; return { executionResult: result };
} }
async function getBundlesWithLabel(label, value) { function getBundlesWithLabel(label, value) {
const notes = await attributeService.getNotesWithLabel(label, value); const notes = attributeService.getNotesWithLabel(label, value);
const bundles = []; const bundles = [];
for (const note of notes) { for (const note of notes) {
const bundle = await scriptService.getScriptBundleForFrontend(note); const bundle = scriptService.getScriptBundleForFrontend(note);
if (bundle) { if (bundle) {
bundles.push(bundle); bundles.push(bundle);
@ -45,20 +45,20 @@ async function getBundlesWithLabel(label, value) {
return bundles; return bundles;
} }
async function getStartupBundles() { function getStartupBundles() {
return await getBundlesWithLabel("run", "frontendStartup"); return getBundlesWithLabel("run", "frontendStartup");
} }
async function getWidgetBundles() { function getWidgetBundles() {
return await getBundlesWithLabel("widget"); return getBundlesWithLabel("widget");
} }
async function getRelationBundles(req) { function getRelationBundles(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
const relationName = req.params.relationName; const relationName = req.params.relationName;
const attributes = await note.getAttributes(); const attributes = note.getAttributes();
const filtered = attributes.filter(attr => attr.type === 'relation' && attr.name === relationName); const filtered = attributes.filter(attr => attr.type === 'relation' && attr.name === relationName);
const targetNoteIds = filtered.map(relation => relation.value); const targetNoteIds = filtered.map(relation => relation.value);
const uniqueNoteIds = Array.from(new Set(targetNoteIds)); const uniqueNoteIds = Array.from(new Set(targetNoteIds));
@ -66,13 +66,13 @@ async function getRelationBundles(req) {
const bundles = []; const bundles = [];
for (const noteId of uniqueNoteIds) { for (const noteId of uniqueNoteIds) {
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (!note.isJavaScript() || note.getScriptEnv() !== 'frontend') { if (!note.isJavaScript() || note.getScriptEnv() !== 'frontend') {
continue; continue;
} }
const bundle = await scriptService.getScriptBundleForFrontend(note); const bundle = scriptService.getScriptBundleForFrontend(note);
if (bundle) { if (bundle) {
bundles.push(bundle); bundles.push(bundle);
@ -82,10 +82,10 @@ async function getRelationBundles(req) {
return bundles; return bundles;
} }
async function getBundle(req) { function getBundle(req) {
const note = await repository.getNote(req.params.noteId); const note = repository.getNote(req.params.noteId);
return await scriptService.getScriptBundleForFrontend(note); return scriptService.getScriptBundleForFrontend(note);
} }
module.exports = { module.exports = {

View File

@ -6,8 +6,8 @@ const log = require('../../services/log');
const scriptService = require('../../services/script'); const scriptService = require('../../services/script');
const searchService = require('../../services/search/search'); const searchService = require('../../services/search/search');
async function searchNotes(req) { function searchNotes(req) {
const {count, results} = await searchService.searchNotes(req.params.searchString); const {count, results} = searchService.searchNotes(req.params.searchString);
try { try {
return { return {
@ -23,8 +23,8 @@ async function searchNotes(req) {
} }
} }
async function searchFromNote(req) { function searchFromNote(req) {
const note = await repository.getNote(req.params.noteId); const note = repository.getNote(req.params.noteId);
if (!note) { if (!note) {
return [404, `Note ${req.params.noteId} has not been found.`]; return [404, `Note ${req.params.noteId} has not been found.`];
@ -38,7 +38,7 @@ async function searchFromNote(req) {
return [400, `Note ${req.params.noteId} is not search note.`] return [400, `Note ${req.params.noteId} is not search note.`]
} }
const json = await note.getJsonContent(); const json = note.getJsonContent();
if (!json || !json.searchString) { if (!json || !json.searchString) {
return []; return [];
@ -50,9 +50,9 @@ async function searchFromNote(req) {
if (json.searchString.startsWith('=')) { if (json.searchString.startsWith('=')) {
const relationName = json.searchString.substr(1).trim(); const relationName = json.searchString.substr(1).trim();
noteIds = await searchFromRelation(note, relationName); noteIds = searchFromRelation(note, relationName);
} else { } else {
noteIds = await searchService.searchForNoteIds(json.searchString); noteIds = searchService.searchForNoteIds(json.searchString);
} }
} }
catch (e) { catch (e) {
@ -71,8 +71,8 @@ async function searchFromNote(req) {
return noteIds.map(noteCacheService.getNotePath).filter(res => !!res); return noteIds.map(noteCacheService.getNotePath).filter(res => !!res);
} }
async function searchFromRelation(note, relationName) { function searchFromRelation(note, relationName) {
const scriptNote = await note.getRelationTarget(relationName); const scriptNote = note.getRelationTarget(relationName);
if (!scriptNote) { if (!scriptNote) {
log.info(`Search note's relation ${relationName} has not been found.`); log.info(`Search note's relation ${relationName} has not been found.`);
@ -92,7 +92,7 @@ async function searchFromRelation(note, relationName) {
return []; return [];
} }
const result = await scriptService.executeNote(scriptNote, { originEntity: note }); const result = scriptService.executeNote(scriptNote, { originEntity: note });
if (!Array.isArray(result)) { if (!Array.isArray(result)) {
log.info(`Result from ${scriptNote.noteId} is not an array.`); log.info(`Result from ${scriptNote.noteId} is not an array.`);

View File

@ -5,7 +5,7 @@ const imageService = require('../../services/image');
const dateNoteService = require('../../services/date_notes'); const dateNoteService = require('../../services/date_notes');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
async function uploadImage(req) { function uploadImage(req) {
const file = req.file; const file = req.file;
if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) { if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) {
@ -14,19 +14,19 @@ async function uploadImage(req) {
const originalName = "Sender image." + imageType(file.buffer).ext; const originalName = "Sender image." + imageType(file.buffer).ext;
const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']); const parentNote = dateNoteService.getDateNote(req.headers['x-local-date']);
const {noteId} = await imageService.saveImage(parentNote.noteId, file.buffer, originalName, true); const {noteId} = imageService.saveImage(parentNote.noteId, file.buffer, originalName, true);
return { return {
noteId: noteId noteId: noteId
}; };
} }
async function saveNote(req) { function saveNote(req) {
const parentNote = await dateNoteService.getDateNote(req.headers['x-local-date']); const parentNote = dateNoteService.getDateNote(req.headers['x-local-date']);
const {note, branch} = await noteService.createNewNote({ const {note, branch} = noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title: req.body.title, title: req.body.title,
content: req.body.content, content: req.body.content,

View File

@ -5,27 +5,27 @@ const setupService = require('../../services/setup');
const log = require('../../services/log'); const log = require('../../services/log');
const appInfo = require('../../services/app_info'); const appInfo = require('../../services/app_info');
async function getStatus() { function getStatus() {
return { return {
isInitialized: await sqlInit.isDbInitialized(), isInitialized: sqlInit.isDbInitialized(),
schemaExists: await sqlInit.schemaExists(), schemaExists: sqlInit.schemaExists(),
syncVersion: appInfo.syncVersion syncVersion: appInfo.syncVersion
}; };
} }
async function setupNewDocument(req) { function setupNewDocument(req) {
const { username, password, theme } = req.body; const { username, password, theme } = req.body;
await sqlInit.createInitialDatabase(username, password, theme); sqlInit.createInitialDatabase(username, password, theme);
} }
async function setupSyncFromServer(req) { function setupSyncFromServer(req) {
const { syncServerHost, syncProxy, username, password } = req.body; const { syncServerHost, syncProxy, username, password } = req.body;
return await setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, username, password); return setupService.setupSyncFromSyncServer(syncServerHost, syncProxy, username, password);
} }
async function saveSyncSeed(req) { function saveSyncSeed(req) {
const {options, syncVersion} = req.body; const {options, syncVersion} = req.body;
if (appInfo.syncVersion !== syncVersion) { if (appInfo.syncVersion !== syncVersion) {
@ -38,14 +38,14 @@ async function saveSyncSeed(req) {
}] }]
} }
await sqlInit.createDatabaseForSync(options); sqlInit.createDatabaseForSync(options);
} }
async function getSyncSeed() { function getSyncSeed() {
log.info("Serving sync seed."); log.info("Serving sync seed.");
return { return {
options: await setupService.getSyncSeedOptions(), options: setupService.getSyncSeedOptions(),
syncVersion: appInfo.syncVersion syncVersion: appInfo.syncVersion
}; };
} }

View File

@ -3,16 +3,16 @@
const noteCacheService = require('../../services/note_cache/note_cache_service'); const noteCacheService = require('../../services/note_cache/note_cache_service');
const repository = require('../../services/repository'); const repository = require('../../services/repository');
async function getSimilarNotes(req) { function getSimilarNotes(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (!note) { if (!note) {
return [404, `Note ${noteId} not found.`]; return [404, `Note ${noteId} not found.`];
} }
const results = await noteCacheService.findSimilarNotes(noteId); const results = noteCacheService.findSimilarNotes(noteId);
return results return results
.filter(note => note.noteId !== noteId); .filter(note => note.noteId !== noteId);

View File

@ -2,21 +2,21 @@
const sql = require('../../services/sql'); const sql = require('../../services/sql');
async function getSchema() { function getSchema() {
const tableNames = await sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`); const tableNames = sql.getColumn(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name`);
const tables = []; const tables = [];
for (const tableName of tableNames) { for (const tableName of tableNames) {
tables.push({ tables.push({
name: tableName, name: tableName,
columns: await sql.getRows(`PRAGMA table_info(${tableName})`) columns: sql.getRows(`PRAGMA table_info(${tableName})`)
}); });
} }
return tables; return tables;
} }
async function execute(req) { function execute(req) {
const queries = req.body.query.split("\n---"); const queries = req.body.query.split("\n---");
try { try {
@ -27,7 +27,7 @@ async function execute(req) {
continue; continue;
} }
results.push(await sql.getRows(query)); results.push(sql.getRows(query));
} }
return { return {

View File

@ -13,13 +13,13 @@ const dateUtils = require('../../services/date_utils');
const entityConstructor = require('../../entities/entity_constructor'); const entityConstructor = require('../../entities/entity_constructor');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
async function testSync() { function testSync() {
try { try {
if (!await syncOptions.isSyncSetup()) { if (!syncOptions.isSyncSetup()) {
return { success: false, message: "Sync server host is not configured. Please configure sync first." }; return { success: false, message: "Sync server host is not configured. Please configure sync first." };
} }
await syncService.login(); syncService.login();
// login was successful so we'll kick off sync now // login was successful so we'll kick off sync now
// this is important in case when sync server has been just initialized // this is important in case when sync server has been just initialized
@ -35,40 +35,40 @@ async function testSync() {
} }
} }
async function getStats() { function getStats() {
if (!await sqlInit.schemaExists()) { if (!sqlInit.schemaExists()) {
// fail silently but prevent errors from not existing options table // fail silently but prevent errors from not existing options table
return {}; return {};
} }
return { return {
initialized: await optionService.getOption('initialized') === 'true', initialized: optionService.getOption('initialized') === 'true',
stats: syncService.stats stats: syncService.stats
}; };
} }
async function checkSync() { function checkSync() {
return { return {
entityHashes: await contentHashService.getEntityHashes(), entityHashes: contentHashService.getEntityHashes(),
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync WHERE isSynced = 1') maxSyncId: sql.getValue('SELECT MAX(id) FROM sync WHERE isSynced = 1')
}; };
} }
async function syncNow() { function syncNow() {
log.info("Received request to trigger sync now."); log.info("Received request to trigger sync now.");
return await syncService.sync(); return syncService.sync();
} }
async function fillSyncRows() { function fillSyncRows() {
await syncTableService.fillAllSyncRows(); syncTableService.fillAllSyncRows();
log.info("Sync rows have been filled."); log.info("Sync rows have been filled.");
} }
async function forceFullSync() { function forceFullSync() {
await optionService.setOption('lastSyncedPull', 0); optionService.setOption('lastSyncedPull', 0);
await optionService.setOption('lastSyncedPush', 0); optionService.setOption('lastSyncedPush', 0);
log.info("Forcing full sync."); log.info("Forcing full sync.");
@ -76,38 +76,38 @@ async function forceFullSync() {
syncService.sync(); syncService.sync();
} }
async function forceNoteSync(req) { function forceNoteSync(req) {
const noteId = req.params.noteId; const noteId = req.params.noteId;
const now = dateUtils.utcNowDateTime(); const now = dateUtils.utcNowDateTime();
await sql.execute(`UPDATE notes SET utcDateModified = ? WHERE noteId = ?`, [now, noteId]); sql.execute(`UPDATE notes SET utcDateModified = ? WHERE noteId = ?`, [now, noteId]);
await syncTableService.addNoteSync(noteId); syncTableService.addNoteSync(noteId);
await sql.execute(`UPDATE note_contents SET utcDateModified = ? WHERE noteId = ?`, [now, noteId]); sql.execute(`UPDATE note_contents SET utcDateModified = ? WHERE noteId = ?`, [now, noteId]);
await syncTableService.addNoteContentSync(noteId); syncTableService.addNoteContentSync(noteId);
for (const branchId of await sql.getColumn("SELECT branchId FROM branches WHERE noteId = ?", [noteId])) { for (const branchId of sql.getColumn("SELECT branchId FROM branches WHERE noteId = ?", [noteId])) {
await sql.execute(`UPDATE branches SET utcDateModified = ? WHERE branchId = ?`, [now, branchId]); sql.execute(`UPDATE branches SET utcDateModified = ? WHERE branchId = ?`, [now, branchId]);
await syncTableService.addBranchSync(branchId); syncTableService.addBranchSync(branchId);
} }
for (const attributeId of await sql.getColumn("SELECT attributeId FROM attributes WHERE noteId = ?", [noteId])) { for (const attributeId of sql.getColumn("SELECT attributeId FROM attributes WHERE noteId = ?", [noteId])) {
await sql.execute(`UPDATE attributes SET utcDateModified = ? WHERE attributeId = ?`, [now, attributeId]); sql.execute(`UPDATE attributes SET utcDateModified = ? WHERE attributeId = ?`, [now, attributeId]);
await syncTableService.addAttributeSync(attributeId); syncTableService.addAttributeSync(attributeId);
} }
for (const noteRevisionId of await sql.getColumn("SELECT noteRevisionId FROM note_revisions WHERE noteId = ?", [noteId])) { for (const noteRevisionId of sql.getColumn("SELECT noteRevisionId FROM note_revisions WHERE noteId = ?", [noteId])) {
await sql.execute(`UPDATE note_revisions SET utcDateModified = ? WHERE noteRevisionId = ?`, [now, noteRevisionId]); sql.execute(`UPDATE note_revisions SET utcDateModified = ? WHERE noteRevisionId = ?`, [now, noteRevisionId]);
await syncTableService.addNoteRevisionSync(noteRevisionId); syncTableService.addNoteRevisionSync(noteRevisionId);
await sql.execute(`UPDATE note_revision_contents SET utcDateModified = ? WHERE noteRevisionId = ?`, [now, noteRevisionId]); sql.execute(`UPDATE note_revision_contents SET utcDateModified = ? WHERE noteRevisionId = ?`, [now, noteRevisionId]);
await syncTableService.addNoteRevisionContentSync(noteRevisionId); syncTableService.addNoteRevisionContentSync(noteRevisionId);
} }
await syncTableService.addRecentNoteSync(noteId); syncTableService.addRecentNoteSync(noteId);
log.info("Forcing note sync for " + noteId); log.info("Forcing note sync for " + noteId);
@ -115,16 +115,16 @@ async function forceNoteSync(req) {
syncService.sync(); syncService.sync();
} }
async function getChanged(req) { function getChanged(req) {
const startTime = Date.now(); const startTime = Date.now();
const lastSyncId = parseInt(req.query.lastSyncId); const lastSyncId = parseInt(req.query.lastSyncId);
const syncs = await sql.getRows("SELECT * FROM sync WHERE isSynced = 1 AND id > ? LIMIT 1000", [lastSyncId]); const syncs = sql.getRows("SELECT * FROM sync WHERE isSynced = 1 AND id > ? LIMIT 1000", [lastSyncId]);
const ret = { const ret = {
syncs: await syncService.getSyncRecords(syncs), syncs: syncService.getSyncRecords(syncs),
maxSyncId: await sql.getValue('SELECT MAX(id) FROM sync WHERE isSynced = 1') maxSyncId: sql.getValue('SELECT MAX(id) FROM sync WHERE isSynced = 1')
}; };
if (ret.syncs.length > 0) { if (ret.syncs.length > 0) {
@ -134,28 +134,28 @@ async function getChanged(req) {
return ret; return ret;
} }
async function update(req) { function update(req) {
const sourceId = req.body.sourceId; const sourceId = req.body.sourceId;
const entities = req.body.entities; const entities = req.body.entities;
for (const {sync, entity} of entities) { for (const {sync, entity} of entities) {
await syncUpdateService.updateEntity(sync, entity, sourceId); syncUpdateService.updateEntity(sync, entity, sourceId);
} }
} }
async function syncFinished() { function syncFinished() {
// after first sync finishes, the application is ready to be used // after first sync finishes, the application is ready to be used
// this is meaningless but at the same time harmless (idempotent) for further syncs // this is meaningless but at the same time harmless (idempotent) for further syncs
await sqlInit.dbInitialized(); sqlInit.dbInitialized();
} }
async function queueSector(req) { function queueSector(req) {
const entityName = utils.sanitizeSqlIdentifier(req.params.entityName); const entityName = utils.sanitizeSqlIdentifier(req.params.entityName);
const sector = utils.sanitizeSqlIdentifier(req.params.sector); const sector = utils.sanitizeSqlIdentifier(req.params.sector);
const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
await syncTableService.addEntitySyncsForSector(entityName, entityPrimaryKey, sector); syncTableService.addEntitySyncsForSector(entityName, entityPrimaryKey, sector);
} }
module.exports = { module.exports = {

View File

@ -4,15 +4,15 @@ const sql = require('../../services/sql');
const optionService = require('../../services/options'); const optionService = require('../../services/options');
const treeService = require('../../services/tree'); const treeService = require('../../services/tree');
async function getNotesAndBranchesAndAttributes(noteIds) { function getNotesAndBranchesAndAttributes(noteIds) {
noteIds = Array.from(new Set(noteIds)); noteIds = Array.from(new Set(noteIds));
const notes = await treeService.getNotes(noteIds); const notes = treeService.getNotes(noteIds);
noteIds = notes.map(note => note.noteId); noteIds = notes.map(note => note.noteId);
// joining child note to filter out not completely synchronised notes which would then cause errors later // joining child note to filter out not completely synchronised notes which would then cause errors later
// cannot do that with parent because of root note's 'none' parent // cannot do that with parent because of root note's 'none' parent
const branches = await sql.getManyRows(` const branches = sql.getManyRows(`
SELECT SELECT
branches.branchId, branches.branchId,
branches.noteId, branches.noteId,
@ -28,7 +28,7 @@ async function getNotesAndBranchesAndAttributes(noteIds) {
// sorting in memory is faster // sorting in memory is faster
branches.sort((a, b) => a.notePosition - b.notePosition < 0 ? -1 : 1); branches.sort((a, b) => a.notePosition - b.notePosition < 0 ? -1 : 1);
const attributes = await sql.getManyRows(` const attributes = sql.getManyRows(`
SELECT SELECT
attributeId, attributeId,
noteId, noteId,
@ -50,12 +50,12 @@ async function getNotesAndBranchesAndAttributes(noteIds) {
}; };
} }
async function getTree() { function getTree() {
const hoistedNoteId = await optionService.getOption('hoistedNoteId'); const hoistedNoteId = optionService.getOption('hoistedNoteId');
// we fetch all branches of notes, even if that particular branch isn't visible // 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 // this allows us to e.g. detect and properly display clones
const noteIds = await sql.getColumn(` const noteIds = sql.getColumn(`
WITH RECURSIVE WITH RECURSIVE
tree(branchId, noteId, isExpanded) AS ( tree(branchId, noteId, isExpanded) AS (
SELECT branchId, noteId, isExpanded FROM branches WHERE noteId = ? SELECT branchId, noteId, isExpanded FROM branches WHERE noteId = ?
@ -68,11 +68,11 @@ async function getTree() {
noteIds.push('root'); noteIds.push('root');
return await getNotesAndBranchesAndAttributes(noteIds); return getNotesAndBranchesAndAttributes(noteIds);
} }
async function load(req) { function load(req) {
return await getNotesAndBranchesAndAttributes(req.body.noteIds); return getNotesAndBranchesAndAttributes(req.body.noteIds);
} }
module.exports = { module.exports = {

View File

@ -6,11 +6,11 @@ const scriptService = require('../services/script');
function register(router) { function register(router) {
// explicitly no CSRF middleware since it's meant to allow integration from external services // explicitly no CSRF middleware since it's meant to allow integration from external services
router.all('/custom/:path*', async (req, res, next) => { router.all('/custom/:path*', (req, res, next) => {
// express puts content after first slash into 0 index element // express puts content after first slash into 0 index element
const path = req.params.path + req.params[0]; const path = req.params.path + req.params[0];
const attrs = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')"); const attrs = repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");
for (const attr of attrs) { for (const attr of attrs) {
const regex = new RegExp(attr.value); const regex = new RegExp(attr.value);
@ -29,12 +29,12 @@ function register(router) {
} }
if (attr.name === 'customRequestHandler') { if (attr.name === 'customRequestHandler') {
const note = await attr.getNote(); const note = attr.getNote();
log.info(`Handling custom request "${path}" with note ${note.noteId}`); log.info(`Handling custom request "${path}" with note ${note.noteId}`);
try { try {
await scriptService.executeNote(note, { scriptService.executeNote(note, {
pathParams: match.slice(1), pathParams: match.slice(1),
req, req,
res res
@ -47,7 +47,7 @@ function register(router) {
} }
} }
else if (attr.name === 'customResourceProvider') { else if (attr.name === 'customResourceProvider') {
await fileUploadService.downloadNoteFile(attr.noteId, res); fileUploadService.downloadNoteFile(attr.noteId, res);
} }
else { else {
throw new Error("Unrecognized attribute name " + attr.name); throw new Error("Unrecognized attribute name " + attr.name);

View File

@ -8,8 +8,8 @@ const optionService = require('../services/options');
const log = require('../services/log'); const log = require('../services/log');
const env = require('../services/env'); const env = require('../services/env');
async function index(req, res) { function index(req, res) {
const options = await optionService.getOptionsMap(); const options = optionService.getOptionsMap();
let view = req.cookies['trilium-device'] === 'mobile' ? 'mobile' : 'desktop'; let view = req.cookies['trilium-device'] === 'mobile' ? 'mobile' : 'desktop';
@ -22,17 +22,17 @@ async function index(req, res) {
mainFontSize: parseInt(options.mainFontSize), mainFontSize: parseInt(options.mainFontSize),
treeFontSize: parseInt(options.treeFontSize), treeFontSize: parseInt(options.treeFontSize),
detailFontSize: parseInt(options.detailFontSize), detailFontSize: parseInt(options.detailFontSize),
sourceId: await sourceIdService.generateSourceId(), sourceId: sourceIdService.generateSourceId(),
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"), maxSyncIdAtLoad: sql.getValue("SELECT MAX(id) FROM sync"),
instanceName: config.General ? config.General.instanceName : null, instanceName: config.General ? config.General.instanceName : null,
appCssNoteIds: await getAppCssNoteIds(), appCssNoteIds: getAppCssNoteIds(),
isDev: env.isDev(), isDev: env.isDev(),
isMainWindow: !req.query.extra isMainWindow: !req.query.extra
}); });
} }
async function getAppCssNoteIds() { function getAppCssNoteIds() {
return (await attributeService.getNotesWithLabels(['appCss', 'appTheme'])) return (attributeService.getNotesWithLabels(['appCss', 'appTheme']))
.map(note => note.noteId); .map(note => note.noteId);
} }

View File

@ -8,12 +8,12 @@ function loginPage(req, res) {
res.render('login', { failedAuth: false }); res.render('login', { failedAuth: false });
} }
async function login(req, res) { function login(req, res) {
const userName = await optionService.getOption('username'); const userName = optionService.getOption('username');
const guessedPassword = req.body.password; const guessedPassword = req.body.password;
if (req.body.username === userName && await verifyPassword(guessedPassword)) { if (req.body.username === userName && verifyPassword(guessedPassword)) {
const rememberMe = req.body.remember_me; const rememberMe = req.body.remember_me;
req.session.regenerate(() => { req.session.regenerate(() => {
@ -32,10 +32,10 @@ async function login(req, res) {
} }
} }
async function verifyPassword(guessedPassword) { function verifyPassword(guessedPassword) {
const hashed_password = utils.fromBase64(await optionService.getOption('passwordVerificationHash')); const hashed_password = utils.fromBase64(optionService.getOption('passwordVerificationHash'));
const guess_hashed = await myScryptService.getVerificationHash(guessedPassword); const guess_hashed = myScryptService.getVerificationHash(guessedPassword);
return guess_hashed.equals(hashed_password); return guess_hashed.equals(hashed_password);
} }

View File

@ -79,23 +79,23 @@ function apiRoute(method, path, routeHandler) {
} }
function route(method, path, middleware, routeHandler, resultHandler, transactional = true) { function route(method, path, middleware, routeHandler, resultHandler, transactional = true) {
router[method](path, ...middleware, async (req, res, next) => { router[method](path, ...middleware, (req, res, next) => {
try { try {
cls.namespace.bindEmitter(req); cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res); cls.namespace.bindEmitter(res);
const result = await cls.init(async () => { const result = cls.init(() => {
cls.set('sourceId', req.headers['trilium-source-id']); cls.set('sourceId', req.headers['trilium-source-id']);
cls.set('localNowDateTime', req.headers['`trilium-local-now-datetime`']); cls.set('localNowDateTime', req.headers['`trilium-local-now-datetime`']);
protectedSessionService.setProtectedSessionId(req); protectedSessionService.setProtectedSessionId(req);
if (transactional) { if (transactional) {
return await sql.transactional(async () => { return sql.transactional(() => {
return await routeHandler(req, res, next); return routeHandler(req, res, next);
}); });
} }
else { else {
return await routeHandler(req, res, next); return routeHandler(req, res, next);
} }
}); });

View File

@ -4,11 +4,11 @@ const sqlInit = require('../services/sql_init');
const setupService = require('../services/setup'); const setupService = require('../services/setup');
const utils = require('../services/utils'); const utils = require('../services/utils');
async function setupPage(req, res) { function setupPage(req, res) {
if (await sqlInit.isDbInitialized()) { if (sqlInit.isDbInitialized()) {
if (utils.isElectron()) { if (utils.isElectron()) {
const windowService = require('../services/window'); const windowService = require('../services/window');
await windowService.createMainWindow(); windowService.createMainWindow();
windowService.closeSetupWindow(); windowService.closeSetupWindow();
} }
else { else {
@ -18,7 +18,7 @@ async function setupPage(req, res) {
// we got here because DB is not completely initialized so if schema exists // we got here because DB is not completely initialized so if schema exists
// it means we're in sync in progress state. // it means we're in sync in progress state.
const syncInProgress = await sqlInit.schemaExists(); const syncInProgress = sqlInit.schemaExists();
if (syncInProgress) { if (syncInProgress) {
// trigger sync if it's not already running // trigger sync if it's not already running

View File

@ -41,7 +41,7 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'relation', name: 'renderNote', isDangerous: true } { type: 'relation', name: 'renderNote', isDangerous: true }
]; ];
async function getNotesWithLabel(name, value) { function getNotesWithLabel(name, value) {
let valueCondition = ""; let valueCondition = "";
let params = [name]; let params = [name];
@ -50,25 +50,25 @@ async function getNotesWithLabel(name, value) {
params.push(value); params.push(value);
} }
return await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) return repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? ${valueCondition} ORDER BY position`, params); WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? ${valueCondition} ORDER BY position`, params);
} }
async function getNotesWithLabels(names) { function getNotesWithLabels(names) {
const questionMarks = names.map(() => "?").join(", "); const questionMarks = names.map(() => "?").join(", ");
return await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) return repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name IN (${questionMarks}) ORDER BY position`, names); WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name IN (${questionMarks}) ORDER BY position`, names);
} }
async function getNoteWithLabel(name, value) { function getNoteWithLabel(name, value) {
const notes = await getNotesWithLabel(name, value); const notes = getNotesWithLabel(name, value);
return notes.length > 0 ? notes[0] : null; return notes.length > 0 ? notes[0] : null;
} }
async function createLabel(noteId, name, value = "") { function createLabel(noteId, name, value = "") {
return await createAttribute({ return createAttribute({
noteId: noteId, noteId: noteId,
type: 'label', type: 'label',
name: name, name: name,
@ -76,8 +76,8 @@ async function createLabel(noteId, name, value = "") {
}); });
} }
async function createRelation(noteId, name, targetNoteId) { function createRelation(noteId, name, targetNoteId) {
return await createAttribute({ return createAttribute({
noteId: noteId, noteId: noteId,
type: 'relation', type: 'relation',
name: name, name: name,
@ -85,14 +85,14 @@ async function createRelation(noteId, name, targetNoteId) {
}); });
} }
async function createAttribute(attribute) { function createAttribute(attribute) {
return await new Attribute(attribute).save(); return new Attribute(attribute).save();
} }
async function getAttributeNames(type, nameLike) { function getAttributeNames(type, nameLike) {
nameLike = nameLike.toLowerCase(); nameLike = nameLike.toLowerCase();
const names = await sql.getColumn( const names = sql.getColumn(
`SELECT DISTINCT name `SELECT DISTINCT name
FROM attributes FROM attributes
WHERE isDeleted = 0 WHERE isDeleted = 0

View File

@ -7,8 +7,8 @@ const utils = require('./utils');
const passwordEncryptionService = require('./password_encryption'); const passwordEncryptionService = require('./password_encryption');
const optionService = require('./options'); const optionService = require('./options');
async function checkAuth(req, res, next) { function checkAuth(req, res, next) {
if (!await sqlInit.isDbInitialized()) { if (!sqlInit.isDbInitialized()) {
res.redirect("setup"); res.redirect("setup");
} }
else if (!req.session.loggedIn && !utils.isElectron()) { else if (!req.session.loggedIn && !utils.isElectron()) {
@ -21,7 +21,7 @@ async function checkAuth(req, res, next) {
// for electron things which need network stuff // for electron things which need network stuff
// currently we're doing that for file upload because handling form data seems to be difficult // currently we're doing that for file upload because handling form data seems to be difficult
async function checkApiAuthOrElectron(req, res, next) { function checkApiAuthOrElectron(req, res, next) {
if (!req.session.loggedIn && !utils.isElectron()) { if (!req.session.loggedIn && !utils.isElectron()) {
reject(req, res, "Not authorized"); reject(req, res, "Not authorized");
} }
@ -30,7 +30,7 @@ async function checkApiAuthOrElectron(req, res, next) {
} }
} }
async function checkApiAuth(req, res, next) { function checkApiAuth(req, res, next) {
if (!req.session.loggedIn) { if (!req.session.loggedIn) {
reject(req, res, "Not authorized"); reject(req, res, "Not authorized");
} }
@ -39,8 +39,8 @@ async function checkApiAuth(req, res, next) {
} }
} }
async function checkAppInitialized(req, res, next) { function checkAppInitialized(req, res, next) {
if (!await sqlInit.isDbInitialized()) { if (!sqlInit.isDbInitialized()) {
res.redirect("setup"); res.redirect("setup");
} }
else { else {
@ -48,8 +48,8 @@ async function checkAppInitialized(req, res, next) {
} }
} }
async function checkAppNotInitialized(req, res, next) { function checkAppNotInitialized(req, res, next) {
if (await sqlInit.isDbInitialized()) { if (sqlInit.isDbInitialized()) {
reject(req, res, "App already initialized."); reject(req, res, "App already initialized.");
} }
else { else {
@ -57,10 +57,10 @@ async function checkAppNotInitialized(req, res, next) {
} }
} }
async function checkToken(req, res, next) { function checkToken(req, res, next) {
const token = req.headers.authorization; const token = req.headers.authorization;
if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) { if (sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) {
reject(req, res, "Not authorized"); reject(req, res, "Not authorized");
} }
else { else {
@ -74,15 +74,15 @@ function reject(req, res, message) {
res.status(401).send(message); res.status(401).send(message);
} }
async function checkBasicAuth(req, res, next) { function checkBasicAuth(req, res, next) {
const header = req.headers.authorization || ''; const header = req.headers.authorization || '';
const token = header.split(/\s+/).pop() || ''; const token = header.split(/\s+/).pop() || '';
const auth = new Buffer.from(token, 'base64').toString(); const auth = new Buffer.from(token, 'base64').toString();
const [username, password] = auth.split(/:/); const [username, password] = auth.split(/:/);
const dbUsername = await optionService.getOption('username'); const dbUsername = optionService.getOption('username');
if (dbUsername !== username || !await passwordEncryptionService.verifyPassword(password)) { if (dbUsername !== username || !passwordEncryptionService.verifyPassword(password)) {
res.status(401).send('Incorrect username and/or password'); res.status(401).send('Incorrect username and/or password');
} }
else { else {

View File

@ -111,8 +111,8 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {string} searchString * @param {string} searchString
* @returns {Promise<Note|null>} * @returns {Promise<Note|null>}
*/ */
this.searchForNote = async searchString => { this.searchForNote = searchString => {
const notes = await searchService.searchForNotes(searchString); const notes = searchService.searchForNotes(searchString);
return notes.length > 0 ? notes[0] : null; return notes.length > 0 ? notes[0] : null;
}; };
@ -185,7 +185,7 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {string} content * @param {string} content
* @return {Promise<{note: Note, branch: Branch}>} * @return {Promise<{note: Note, branch: Branch}>}
*/ */
this.createTextNote = async (parentNoteId, title, content = '') => await noteService.createNewNote({ this.createTextNote = (parentNoteId, title, content = '') => noteService.createNewNote({
parentNoteId, parentNoteId,
title, title,
content, content,
@ -201,7 +201,7 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {object} content * @param {object} content
* @return {Promise<{note: Note, branch: Branch}>} * @return {Promise<{note: Note, branch: Branch}>}
*/ */
this.createDataNote = async (parentNoteId, title, content = {}) => await noteService.createNewNote({ this.createDataNote = (parentNoteId, title, content = {}) => noteService.createNewNote({
parentNoteId, parentNoteId,
title, title,
content: JSON.stringify(content, null, '\t'), content: JSON.stringify(content, null, '\t'),
@ -256,11 +256,11 @@ function BackendScriptApi(currentNote, apiParams) {
* @param {CreateNoteExtraOptions} [extraOptions={}] * @param {CreateNoteExtraOptions} [extraOptions={}]
* @returns {Promise<{note: Note, branch: Branch}>} object contains newly created entities note and branch * @returns {Promise<{note: Note, branch: Branch}>} object contains newly created entities note and branch
*/ */
this.createNote = async (parentNoteId, title, content = "", extraOptions= {}) => { this.createNote = (parentNoteId, title, content = "", extraOptions= {}) => {
extraOptions.parentNoteId = parentNoteId; extraOptions.parentNoteId = parentNoteId;
extraOptions.title = title; extraOptions.title = title;
const parentNote = await repository.getNote(parentNoteId); const parentNote = repository.getNote(parentNoteId);
// code note type can be inherited, otherwise text is default // code note type can be inherited, otherwise text is default
extraOptions.type = parentNote.type === 'code' ? 'code' : 'text'; extraOptions.type = parentNote.type === 'code' ? 'code' : 'text';
@ -275,10 +275,10 @@ function BackendScriptApi(currentNote, apiParams) {
extraOptions.content = content; extraOptions.content = content;
} }
const {note, branch} = await noteService.createNewNote(extraOptions); const {note, branch} = noteService.createNewNote(extraOptions);
for (const attr of extraOptions.attributes || []) { for (const attr of extraOptions.attributes || []) {
await attributeService.createAttribute({ attributeService.createAttribute({
noteId: note.noteId, noteId: note.noteId,
type: attr.type, type: attr.type,
name: attr.name, name: attr.name,

View File

@ -11,28 +11,28 @@ const attributeService = require('./attributes');
const cls = require('./cls'); const cls = require('./cls');
const utils = require('./utils'); const utils = require('./utils');
async function regularBackup() { function regularBackup() {
await periodBackup('lastDailyBackupDate', 'daily', 24 * 3600); periodBackup('lastDailyBackupDate', 'daily', 24 * 3600);
await periodBackup('lastWeeklyBackupDate', 'weekly', 7 * 24 * 3600); periodBackup('lastWeeklyBackupDate', 'weekly', 7 * 24 * 3600);
await periodBackup('lastMonthlyBackupDate', 'monthly', 30 * 24 * 3600); periodBackup('lastMonthlyBackupDate', 'monthly', 30 * 24 * 3600);
} }
async function periodBackup(optionName, fileName, periodInSeconds) { function periodBackup(optionName, fileName, periodInSeconds) {
const now = new Date(); const now = new Date();
const lastDailyBackupDate = dateUtils.parseDateTime(await optionService.getOption(optionName)); const lastDailyBackupDate = dateUtils.parseDateTime(optionService.getOption(optionName));
if (now.getTime() - lastDailyBackupDate.getTime() > periodInSeconds * 1000) { if (now.getTime() - lastDailyBackupDate.getTime() > periodInSeconds * 1000) {
await backupNow(fileName); backupNow(fileName);
await optionService.setOption(optionName, dateUtils.utcNowDateTime()); optionService.setOption(optionName, dateUtils.utcNowDateTime());
} }
} }
const COPY_ATTEMPT_COUNT = 50; const COPY_ATTEMPT_COUNT = 50;
async function copyFile(backupFile) { function copyFile(backupFile) {
const sql = require('./sql'); const sql = require('./sql');
try { try {
@ -45,7 +45,7 @@ async function copyFile(backupFile) {
for (; attemptCount < COPY_ATTEMPT_COUNT && !success; attemptCount++) { for (; attemptCount < COPY_ATTEMPT_COUNT && !success; attemptCount++) {
try { try {
await sql.executeWithoutTransaction(`VACUUM INTO '${backupFile}'`); sql.executeWithoutTransaction(`VACUUM INTO '${backupFile}'`);
success = true; success = true;
} catch (e) { } catch (e) {
@ -60,10 +60,10 @@ async function copyFile(backupFile) {
async function backupNow(name) { async function backupNow(name) {
// we don't want to backup DB in the middle of sync with potentially inconsistent DB state // we don't want to backup DB in the middle of sync with potentially inconsistent DB state
return await syncMutexService.doExclusively(async () => { return await syncMutexService.doExclusively(() => {
const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`; const backupFile = `${dataDir.BACKUP_DIR}/backup-${name}.db`;
const success = await copyFile(backupFile); const success = copyFile(backupFile);
if (success) { if (success) {
log.info("Created backup at " + backupFile); log.info("Created backup at " + backupFile);
@ -76,45 +76,45 @@ async function backupNow(name) {
}); });
} }
async function anonymize() { function anonymize() {
if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) { if (!fs.existsSync(dataDir.ANONYMIZED_DB_DIR)) {
fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700); fs.mkdirSync(dataDir.ANONYMIZED_DB_DIR, 0o700);
} }
const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db"; const anonymizedFile = dataDir.ANONYMIZED_DB_DIR + "/" + "anonymized-" + dateUtils.getDateTimeForFile() + ".db";
const success = await copyFile(anonymizedFile); const success = copyFile(anonymizedFile);
if (!success) { if (!success) {
return { success: false }; return { success: false };
} }
const db = await sqlite.open({ const db = sqlite.open({
filename: anonymizedFile, filename: anonymizedFile,
driver: sqlite3.Database driver: sqlite3.Database
}); });
await db.run("UPDATE api_tokens SET token = 'API token value'"); db.run("UPDATE api_tokens SET token = 'API token value'");
await db.run("UPDATE notes SET title = 'title'"); db.run("UPDATE notes SET title = 'title'");
await db.run("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL"); db.run("UPDATE note_contents SET content = 'text' WHERE content IS NOT NULL");
await db.run("UPDATE note_revisions SET title = 'title'"); db.run("UPDATE note_revisions SET title = 'title'");
await db.run("UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL"); db.run("UPDATE note_revision_contents SET content = 'text' WHERE content IS NOT NULL");
// we want to delete all non-builtin attributes because they can contain sensitive names and values // we want to delete all non-builtin attributes because they can contain sensitive names and values
// on the other hand builtin/system attrs should not contain any sensitive info // on the other hand builtin/system attrs should not contain any sensitive info
const builtinAttrs = attributeService.getBuiltinAttributeNames().map(name => "'" + utils.sanitizeSql(name) + "'").join(', '); const builtinAttrs = attributeService.getBuiltinAttributeNames().map(name => "'" + utils.sanitizeSql(name) + "'").join(', ');
await db.run(`UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrs})`); db.run(`UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' AND name NOT IN(${builtinAttrs})`);
await db.run(`UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrs})`); db.run(`UPDATE attributes SET name = 'name' WHERE type = 'relation' AND name NOT IN (${builtinAttrs})`);
await db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL"); db.run("UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL");
await db.run(`UPDATE options SET value = 'anonymized' WHERE name IN db.run(`UPDATE options SET value = 'anonymized' WHERE name IN
('documentId', 'documentSecret', 'encryptedDataKey', ('documentId', 'documentSecret', 'encryptedDataKey',
'passwordVerificationHash', 'passwordVerificationSalt', 'passwordVerificationHash', 'passwordVerificationSalt',
'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy') 'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy')
AND value != ''`); AND value != ''`);
await db.run("VACUUM"); db.run("VACUUM");
await db.close(); db.close();
return { return {
success: true, success: true,
@ -126,12 +126,10 @@ if (!fs.existsSync(dataDir.BACKUP_DIR)) {
fs.mkdirSync(dataDir.BACKUP_DIR, 0o700); fs.mkdirSync(dataDir.BACKUP_DIR, 0o700);
} }
sqlInit.dbReady.then(() => { setInterval(cls.wrap(regularBackup), 4 * 60 * 60 * 1000);
setInterval(cls.wrap(regularBackup), 4 * 60 * 60 * 1000);
// kickoff first backup soon after start up // kickoff first backup soon after start up
setTimeout(cls.wrap(regularBackup), 5 * 60 * 1000); setTimeout(cls.wrap(regularBackup), 5 * 60 * 1000);
});
module.exports = { module.exports = {
backupNow, backupNow,

View File

@ -6,21 +6,21 @@ const myScryptService = require('./my_scrypt');
const utils = require('./utils'); const utils = require('./utils');
const passwordEncryptionService = require('./password_encryption'); const passwordEncryptionService = require('./password_encryption');
async function changePassword(currentPassword, newPassword) { function changePassword(currentPassword, newPassword) {
if (!await passwordEncryptionService.verifyPassword(currentPassword)) { if (!passwordEncryptionService.verifyPassword(currentPassword)) {
return { return {
success: false, success: false,
message: "Given current password doesn't match hash" message: "Given current password doesn't match hash"
}; };
} }
const newPasswordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(newPassword)); const newPasswordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(newPassword));
const decryptedDataKey = await passwordEncryptionService.getDataKey(currentPassword); const decryptedDataKey = passwordEncryptionService.getDataKey(currentPassword);
await sql.transactional(async () => { sql.transactional(() => {
await passwordEncryptionService.setDataKey(newPassword, decryptedDataKey); passwordEncryptionService.setDataKey(newPassword, decryptedDataKey);
await optionService.setOption('passwordVerificationHash', newPasswordVerificationKey); optionService.setOption('passwordVerificationHash', newPasswordVerificationKey);
}); });
return { return {

View File

@ -9,20 +9,20 @@ const Branch = require('../entities/branch');
const TaskContext = require("./task_context.js"); const TaskContext = require("./task_context.js");
const utils = require('./utils'); const utils = require('./utils');
async function cloneNoteToParent(noteId, parentBranchId, prefix) { function cloneNoteToParent(noteId, parentBranchId, prefix) {
const parentBranch = await repository.getBranch(parentBranchId); const parentBranch = repository.getBranch(parentBranchId);
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentBranch.noteId)) { if (isNoteDeleted(noteId) || isNoteDeleted(parentBranch.noteId)) {
return { success: false, message: 'Note is deleted.' }; return { success: false, message: 'Note is deleted.' };
} }
const validationResult = await treeService.validateParentChild(parentBranch.noteId, noteId); const validationResult = treeService.validateParentChild(parentBranch.noteId, noteId);
if (!validationResult.success) { if (!validationResult.success) {
return validationResult; return validationResult;
} }
const branch = await new Branch({ const branch = new Branch({
noteId: noteId, noteId: noteId,
parentNoteId: parentBranch.noteId, parentNoteId: parentBranch.noteId,
prefix: prefix, prefix: prefix,
@ -30,23 +30,23 @@ async function cloneNoteToParent(noteId, parentBranchId, prefix) {
}).save(); }).save();
parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user parentBranch.isExpanded = true; // the new target should be expanded so it immediately shows up to the user
await parentBranch.save(); parentBranch.save();
return { success: true, branchId: branch.branchId }; return { success: true, branchId: branch.branchId };
} }
async function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) { function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
if (await isNoteDeleted(noteId) || await isNoteDeleted(parentNoteId)) { if (isNoteDeleted(noteId) || isNoteDeleted(parentNoteId)) {
return { success: false, message: 'Note is deleted.' }; return { success: false, message: 'Note is deleted.' };
} }
const validationResult = await treeService.validateParentChild(parentNoteId, noteId); const validationResult = treeService.validateParentChild(parentNoteId, noteId);
if (!validationResult.success) { if (!validationResult.success) {
return validationResult; return validationResult;
} }
await new Branch({ new Branch({
noteId: noteId, noteId: noteId,
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
prefix: prefix, prefix: prefix,
@ -54,32 +54,32 @@ async function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) {
}).save(); }).save();
} }
async function ensureNoteIsAbsentFromParent(noteId, parentNoteId) { function ensureNoteIsAbsentFromParent(noteId, parentNoteId) {
const branch = await repository.getEntity(`SELECT * FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]); const branch = repository.getEntity(`SELECT * FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0`, [noteId, parentNoteId]);
if (branch) { if (branch) {
const deleteId = utils.randomString(10); const deleteId = utils.randomString(10);
await noteService.deleteBranch(branch, deleteId, new TaskContext()); noteService.deleteBranch(branch, deleteId, new TaskContext());
} }
} }
async function toggleNoteInParent(present, noteId, parentNoteId, prefix) { function toggleNoteInParent(present, noteId, parentNoteId, prefix) {
if (present) { if (present) {
await ensureNoteIsPresentInParent(noteId, parentNoteId, prefix); ensureNoteIsPresentInParent(noteId, parentNoteId, prefix);
} }
else { else {
await ensureNoteIsAbsentFromParent(noteId, parentNoteId); ensureNoteIsAbsentFromParent(noteId, parentNoteId);
} }
} }
async function cloneNoteAfter(noteId, afterBranchId) { function cloneNoteAfter(noteId, afterBranchId) {
const afterNote = await repository.getBranch(afterBranchId); const afterNote = repository.getBranch(afterBranchId);
if (await isNoteDeleted(noteId) || await isNoteDeleted(afterNote.parentNoteId)) { if (isNoteDeleted(noteId) || isNoteDeleted(afterNote.parentNoteId)) {
return { success: false, message: 'Note is deleted.' }; return { success: false, message: 'Note is deleted.' };
} }
const validationResult = await treeService.validateParentChild(afterNote.parentNoteId, noteId); const validationResult = treeService.validateParentChild(afterNote.parentNoteId, noteId);
if (!validationResult.success) { if (!validationResult.success) {
return validationResult; return validationResult;
@ -87,12 +87,12 @@ async function cloneNoteAfter(noteId, afterBranchId) {
// we don't change utcDateModified so other changes are prioritized in case of conflict // we don't change utcDateModified so other changes are prioritized in case of conflict
// also we would have to sync all those modified branches otherwise hash checks would fail // also we would have to sync all those modified branches otherwise hash checks would fail
await sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0",
[afterNote.parentNoteId, afterNote.notePosition]); [afterNote.parentNoteId, afterNote.notePosition]);
await syncTable.addNoteReorderingSync(afterNote.parentNoteId); syncTable.addNoteReorderingSync(afterNote.parentNoteId);
const branch = await new Branch({ const branch = new Branch({
noteId: noteId, noteId: noteId,
parentNoteId: afterNote.parentNoteId, parentNoteId: afterNote.parentNoteId,
notePosition: afterNote.notePosition + 10, notePosition: afterNote.notePosition + 10,
@ -102,8 +102,8 @@ async function cloneNoteAfter(noteId, afterBranchId) {
return { success: true, branchId: branch.branchId }; return { success: true, branchId: branch.branchId };
} }
async function isNoteDeleted(noteId) { function isNoteDeleted(noteId) {
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
return note.isDeleted; return note.isDeleted;
} }

View File

@ -1,12 +1,12 @@
const clsHooked = require('cls-hooked'); const clsHooked = require('cls-hooked');
const namespace = clsHooked.createNamespace("trilium"); const namespace = clsHooked.createNamespace("trilium");
async function init(callback) { function init(callback) {
return await namespace.runAndReturn(callback); return namespace.runAndReturn(callback);
} }
function wrap(callback) { function wrap(callback) {
return async () => await init(callback); return () => init(callback);
} }
function get(key) { function get(key) {

View File

@ -19,12 +19,12 @@ class ConsistencyChecks {
this.fixedIssues = false; this.fixedIssues = false;
} }
async findAndFixIssues(query, fixerCb) { findAndFixIssues(query, fixerCb) {
const results = await sql.getRows(query); const results = sql.getRows(query);
for (const res of results) { for (const res of results) {
try { try {
await fixerCb(res); fixerCb(res);
if (this.autoFix) { if (this.autoFix) {
this.fixedIssues = true; this.fixedIssues = true;
@ -40,9 +40,9 @@ class ConsistencyChecks {
return results; return results;
} }
async checkTreeCycles() { checkTreeCycles() {
const childToParents = {}; const childToParents = {};
const rows = await sql.getRows("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0"); const rows = sql.getRows("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0");
for (const row of rows) { for (const row of rows) {
const childNoteId = row.noteId; const childNoteId = row.noteId;
@ -90,18 +90,18 @@ class ConsistencyChecks {
} }
} }
async findBrokenReferenceIssues() { findBrokenReferenceIssues() {
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT branchId, branches.noteId SELECT branchId, branches.noteId
FROM branches FROM branches
LEFT JOIN notes USING (noteId) LEFT JOIN notes USING (noteId)
WHERE branches.isDeleted = 0 WHERE branches.isDeleted = 0
AND notes.noteId IS NULL`, AND notes.noteId IS NULL`,
async ({branchId, noteId}) => { ({branchId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branch = await repository.getBranch(branchId); const branch = repository.getBranch(branchId);
branch.isDeleted = true; branch.isDeleted = true;
await branch.save(); branch.save();
logFix(`Branch ${branchId} has been deleted since it references missing note ${noteId}`); logFix(`Branch ${branchId} has been deleted since it references missing note ${noteId}`);
} else { } else {
@ -109,18 +109,18 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT branchId, branches.noteId AS parentNoteId SELECT branchId, branches.noteId AS parentNoteId
FROM branches FROM branches
LEFT JOIN notes ON notes.noteId = branches.parentNoteId LEFT JOIN notes ON notes.noteId = branches.parentNoteId
WHERE branches.isDeleted = 0 WHERE branches.isDeleted = 0
AND branches.branchId != 'root' AND branches.branchId != 'root'
AND notes.noteId IS NULL`, AND notes.noteId IS NULL`,
async ({branchId, parentNoteId}) => { ({branchId, parentNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branch = await repository.getBranch(branchId); const branch = repository.getBranch(branchId);
branch.parentNoteId = 'root'; branch.parentNoteId = 'root';
await branch.save(); branch.save();
logFix(`Branch ${branchId} was set to root parent since it was referencing missing parent note ${parentNoteId}`); logFix(`Branch ${branchId} was set to root parent since it was referencing missing parent note ${parentNoteId}`);
} else { } else {
@ -128,17 +128,17 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT attributeId, attributes.noteId SELECT attributeId, attributes.noteId
FROM attributes FROM attributes
LEFT JOIN notes USING (noteId) LEFT JOIN notes USING (noteId)
WHERE attributes.isDeleted = 0 WHERE attributes.isDeleted = 0
AND notes.noteId IS NULL`, AND notes.noteId IS NULL`,
async ({attributeId, noteId}) => { ({attributeId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = await repository.getAttribute(attributeId); const attribute = repository.getAttribute(attributeId);
attribute.isDeleted = true; attribute.isDeleted = true;
await attribute.save(); attribute.save();
logFix(`Attribute ${attributeId} has been deleted since it references missing source note ${noteId}`); logFix(`Attribute ${attributeId} has been deleted since it references missing source note ${noteId}`);
} else { } else {
@ -146,18 +146,18 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT attributeId, attributes.value AS noteId SELECT attributeId, attributes.value AS noteId
FROM attributes FROM attributes
LEFT JOIN notes ON notes.noteId = attributes.value LEFT JOIN notes ON notes.noteId = attributes.value
WHERE attributes.isDeleted = 0 WHERE attributes.isDeleted = 0
AND attributes.type = 'relation' AND attributes.type = 'relation'
AND notes.noteId IS NULL`, AND notes.noteId IS NULL`,
async ({attributeId, noteId}) => { ({attributeId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = await repository.getAttribute(attributeId); const attribute = repository.getAttribute(attributeId);
attribute.isDeleted = true; attribute.isDeleted = true;
await attribute.save(); attribute.save();
logFix(`Relation ${attributeId} has been deleted since it references missing note ${noteId}`) logFix(`Relation ${attributeId} has been deleted since it references missing note ${noteId}`)
} else { } else {
@ -166,24 +166,24 @@ class ConsistencyChecks {
}); });
} }
async findExistencyIssues() { findExistencyIssues() {
// principle for fixing inconsistencies is that if the note itself is deleted (isDeleted=true) then all related entities should be also deleted (branches, attributes) // principle for fixing inconsistencies is that if the note itself is deleted (isDeleted=true) then all related entities should be also deleted (branches, attributes)
// but if note is not deleted, then at least one branch should exist. // but if note is not deleted, then at least one branch should exist.
// the order here is important - first we might need to delete inconsistent branches and after that // the order here is important - first we might need to delete inconsistent branches and after that
// another check might create missing branch // another check might create missing branch
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT branchId, SELECT branchId,
noteId noteId
FROM branches FROM branches
JOIN notes USING (noteId) JOIN notes USING (noteId)
WHERE notes.isDeleted = 1 WHERE notes.isDeleted = 1
AND branches.isDeleted = 0`, AND branches.isDeleted = 0`,
async ({branchId, noteId}) => { ({branchId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branch = await repository.getBranch(branchId); const branch = repository.getBranch(branchId);
branch.isDeleted = true; branch.isDeleted = true;
await branch.save(); branch.save();
logFix(`Branch ${branchId} has been deleted since associated note ${noteId} is deleted.`); logFix(`Branch ${branchId} has been deleted since associated note ${noteId} is deleted.`);
} else { } else {
@ -191,18 +191,18 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT branchId, SELECT branchId,
parentNoteId parentNoteId
FROM branches FROM branches
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
WHERE parentNote.isDeleted = 1 WHERE parentNote.isDeleted = 1
AND branches.isDeleted = 0 AND branches.isDeleted = 0
`, async ({branchId, parentNoteId}) => { `, ({branchId, parentNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branch = await repository.getBranch(branchId); const branch = repository.getBranch(branchId);
branch.isDeleted = true; branch.isDeleted = true;
await branch.save(); branch.save();
logFix(`Branch ${branchId} has been deleted since associated parent note ${parentNoteId} is deleted.`); logFix(`Branch ${branchId} has been deleted since associated parent note ${parentNoteId} is deleted.`);
} else { } else {
@ -210,15 +210,15 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT DISTINCT notes.noteId SELECT DISTINCT notes.noteId
FROM notes FROM notes
LEFT JOIN branches ON notes.noteId = branches.noteId AND branches.isDeleted = 0 LEFT JOIN branches ON notes.noteId = branches.noteId AND branches.isDeleted = 0
WHERE notes.isDeleted = 0 WHERE notes.isDeleted = 0
AND branches.branchId IS NULL AND branches.branchId IS NULL
`, async ({noteId}) => { `, ({noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branch = await new Branch({ const branch = new Branch({
parentNoteId: 'root', parentNoteId: 'root',
noteId: noteId, noteId: noteId,
prefix: 'recovered' prefix: 'recovered'
@ -231,7 +231,7 @@ class ConsistencyChecks {
}); });
// there should be a unique relationship between note and its parent // there should be a unique relationship between note and its parent
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT noteId, SELECT noteId,
parentNoteId parentNoteId
FROM branches FROM branches
@ -239,9 +239,9 @@ class ConsistencyChecks {
GROUP BY branches.parentNoteId, GROUP BY branches.parentNoteId,
branches.noteId branches.noteId
HAVING COUNT(1) > 1`, HAVING COUNT(1) > 1`,
async ({noteId, parentNoteId}) => { ({noteId, parentNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branches = await repository.getEntities( const branches = repository.getEntities(
`SELECT * `SELECT *
FROM branches FROM branches
WHERE noteId = ? WHERE noteId = ?
@ -254,7 +254,7 @@ class ConsistencyChecks {
// delete all but the first branch // delete all but the first branch
for (const branch of branches.slice(1)) { for (const branch of branches.slice(1)) {
branch.isDeleted = true; branch.isDeleted = true;
await branch.save(); branch.save();
logFix(`Removing branch ${branch.branchId} since it's parent-child duplicate of branch ${origBranch.branchId}`); logFix(`Removing branch ${branch.branchId} since it's parent-child duplicate of branch ${origBranch.branchId}`);
} }
@ -264,17 +264,17 @@ class ConsistencyChecks {
}); });
} }
async findLogicIssues() { findLogicIssues() {
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT noteId, type SELECT noteId, type
FROM notes FROM notes
WHERE isDeleted = 0 WHERE isDeleted = 0
AND type NOT IN ('text', 'code', 'render', 'file', 'image', 'search', 'relation-map', 'book')`, AND type NOT IN ('text', 'code', 'render', 'file', 'image', 'search', 'relation-map', 'book')`,
async ({noteId, type}) => { ({noteId, type}) => {
if (this.autoFix) { if (this.autoFix) {
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
note.type = 'file'; // file is a safe option to recover notes if type is not known note.type = 'file'; // file is a safe option to recover notes if type is not known
await note.save(); note.save();
logFix(`Note ${noteId} type has been change to file since it had invalid type=${type}`) logFix(`Note ${noteId} type has been change to file since it had invalid type=${type}`)
} else { } else {
@ -282,29 +282,29 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT notes.noteId SELECT notes.noteId
FROM notes FROM notes
LEFT JOIN note_contents USING (noteId) LEFT JOIN note_contents USING (noteId)
WHERE note_contents.noteId IS NULL`, WHERE note_contents.noteId IS NULL`,
async ({noteId}) => { ({noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (note.isProtected) { if (note.isProtected) {
// this is wrong for non-erased notes but we cannot set a valid value for protected notes // this is wrong for non-erased notes but we cannot set a valid value for protected notes
await sql.upsert("note_contents", "noteId", { sql.upsert("note_contents", "noteId", {
noteId: noteId, noteId: noteId,
content: null, content: null,
hash: "consistency_checks", hash: "consistency_checks",
utcDateModified: dateUtils.utcNowDateTime() utcDateModified: dateUtils.utcNowDateTime()
}); });
await syncTableService.addNoteContentSync(noteId); syncTableService.addNoteContentSync(noteId);
} }
else { else {
// empty string might be wrong choice for some note types but it's a best guess // empty string might be wrong choice for some note types but it's a best guess
await note.setContent(note.isErased ? null : ''); note.setContent(note.isErased ? null : '');
} }
logFix(`Note ${noteId} content was set to empty string since there was no corresponding row`); logFix(`Note ${noteId} content was set to empty string since there was no corresponding row`);
@ -313,18 +313,18 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT noteId SELECT noteId
FROM notes FROM notes
JOIN note_contents USING (noteId) JOIN note_contents USING (noteId)
WHERE isDeleted = 0 WHERE isDeleted = 0
AND isProtected = 0 AND isProtected = 0
AND content IS NULL`, AND content IS NULL`,
async ({noteId}) => { ({noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
// empty string might be wrong choice for some note types but it's a best guess // empty string might be wrong choice for some note types but it's a best guess
await note.setContent(''); note.setContent('');
logFix(`Note ${noteId} content was set to empty string since it was null even though it is not deleted`); logFix(`Note ${noteId} content was set to empty string since it was null even though it is not deleted`);
} else { } else {
@ -332,13 +332,13 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT noteId SELECT noteId
FROM notes FROM notes
JOIN note_contents USING (noteId) JOIN note_contents USING (noteId)
WHERE isErased = 1 WHERE isErased = 1
AND content IS NOT NULL`, AND content IS NOT NULL`,
async ({noteId}) => { ({noteId}) => {
// we always fix this issue because there does not seem to be a good way to prevent it. // we always fix this issue because there does not seem to be a good way to prevent it.
// Scenario in which this can happen: // Scenario in which this can happen:
@ -355,23 +355,23 @@ class ConsistencyChecks {
// //
// So instead we just fix such cases afterwards here. // So instead we just fix such cases afterwards here.
await sql.execute(`UPDATE note_contents SET content = NULL WHERE noteId = ?`, [noteId]); sql.execute(`UPDATE note_contents SET content = NULL WHERE noteId = ?`, [noteId]);
logFix(`Note ${noteId} content has been set to null since the note is erased`); logFix(`Note ${noteId} content has been set to null since the note is erased`);
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT noteId, noteRevisionId SELECT noteId, noteRevisionId
FROM notes FROM notes
JOIN note_revisions USING (noteId) JOIN note_revisions USING (noteId)
WHERE notes.isErased = 1 WHERE notes.isErased = 1
AND note_revisions.isErased = 0`, AND note_revisions.isErased = 0`,
async ({noteId, noteRevisionId}) => { ({noteId, noteRevisionId}) => {
if (this.autoFix) { if (this.autoFix) {
const noteRevision = await repository.getNoteRevision(noteRevisionId); const noteRevision = repository.getNoteRevision(noteRevisionId);
noteRevision.isErased = true; noteRevision.isErased = true;
await noteRevision.setContent(null); noteRevision.setContent(null);
await noteRevision.save(); noteRevision.save();
logFix(`Note revision ${noteRevisionId} has been erased since its note ${noteId} is also erased.`); logFix(`Note revision ${noteRevisionId} has been erased since its note ${noteId} is also erased.`);
} else { } else {
@ -379,18 +379,18 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT note_revisions.noteRevisionId SELECT note_revisions.noteRevisionId
FROM note_revisions FROM note_revisions
LEFT JOIN note_revision_contents USING (noteRevisionId) LEFT JOIN note_revision_contents USING (noteRevisionId)
WHERE note_revision_contents.noteRevisionId IS NULL WHERE note_revision_contents.noteRevisionId IS NULL
AND note_revisions.isProtected = 0`, AND note_revisions.isProtected = 0`,
async ({noteRevisionId}) => { ({noteRevisionId}) => {
if (this.autoFix) { if (this.autoFix) {
const noteRevision = await repository.getNoteRevision(noteRevisionId); const noteRevision = repository.getNoteRevision(noteRevisionId);
await noteRevision.setContent(null); noteRevision.setContent(null);
noteRevision.isErased = true; noteRevision.isErased = true;
await noteRevision.save(); noteRevision.save();
logFix(`Note revision content ${noteRevisionId} was created and set to erased since it did not exist.`); logFix(`Note revision content ${noteRevisionId} was created and set to erased since it did not exist.`);
} else { } else {
@ -398,17 +398,17 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT noteRevisionId SELECT noteRevisionId
FROM note_revisions FROM note_revisions
JOIN note_revision_contents USING (noteRevisionId) JOIN note_revision_contents USING (noteRevisionId)
WHERE isErased = 0 WHERE isErased = 0
AND content IS NULL`, AND content IS NULL`,
async ({noteRevisionId}) => { ({noteRevisionId}) => {
if (this.autoFix) { if (this.autoFix) {
const noteRevision = await repository.getNoteRevision(noteRevisionId); const noteRevision = repository.getNoteRevision(noteRevisionId);
noteRevision.isErased = true; noteRevision.isErased = true;
await noteRevision.save(); noteRevision.save();
logFix(`Note revision ${noteRevisionId} content was set to empty string since it was null even though it is not erased`); logFix(`Note revision ${noteRevisionId} content was set to empty string since it was null even though it is not erased`);
} else { } else {
@ -416,15 +416,15 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT noteRevisionId SELECT noteRevisionId
FROM note_revisions FROM note_revisions
JOIN note_revision_contents USING (noteRevisionId) JOIN note_revision_contents USING (noteRevisionId)
WHERE isErased = 1 WHERE isErased = 1
AND content IS NOT NULL`, AND content IS NOT NULL`,
async ({noteRevisionId}) => { ({noteRevisionId}) => {
if (this.autoFix) { if (this.autoFix) {
await sql.execute(`UPDATE note_revision_contents SET content = NULL WHERE noteRevisionId = ?`, [noteRevisionId]); sql.execute(`UPDATE note_revision_contents SET content = NULL WHERE noteRevisionId = ?`, [noteRevisionId]);
logFix(`Note revision ${noteRevisionId} content was set to null since the note revision is erased`); logFix(`Note revision ${noteRevisionId} content was set to null since the note revision is erased`);
} }
@ -433,16 +433,16 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT noteId SELECT noteId
FROM notes FROM notes
WHERE isErased = 1 WHERE isErased = 1
AND isDeleted = 0`, AND isDeleted = 0`,
async ({noteId}) => { ({noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
note.isDeleted = true; note.isDeleted = true;
await note.save(); note.save();
logFix(`Note ${noteId} was set to deleted since it is erased`); logFix(`Note ${noteId} was set to deleted since it is erased`);
} }
@ -451,23 +451,23 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT parentNoteId SELECT parentNoteId
FROM branches FROM branches
JOIN notes ON notes.noteId = branches.parentNoteId JOIN notes ON notes.noteId = branches.parentNoteId
WHERE notes.isDeleted = 0 WHERE notes.isDeleted = 0
AND notes.type == 'search' AND notes.type == 'search'
AND branches.isDeleted = 0`, AND branches.isDeleted = 0`,
async ({parentNoteId}) => { ({parentNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
const branches = await repository.getEntities(`SELECT * const branches = repository.getEntities(`SELECT *
FROM branches FROM branches
WHERE isDeleted = 0 WHERE isDeleted = 0
AND parentNoteId = ?`, [parentNoteId]); AND parentNoteId = ?`, [parentNoteId]);
for (const branch of branches) { for (const branch of branches) {
branch.parentNoteId = 'root'; branch.parentNoteId = 'root';
await branch.save(); branch.save();
logFix(`Child branch ${branch.branchId} has been moved to root since it was a child of a search note ${parentNoteId}`) logFix(`Child branch ${branch.branchId} has been moved to root since it was a child of a search note ${parentNoteId}`)
} }
@ -476,17 +476,17 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT attributeId SELECT attributeId
FROM attributes FROM attributes
WHERE isDeleted = 0 WHERE isDeleted = 0
AND type = 'relation' AND type = 'relation'
AND value = ''`, AND value = ''`,
async ({attributeId}) => { ({attributeId}) => {
if (this.autoFix) { if (this.autoFix) {
const relation = await repository.getAttribute(attributeId); const relation = repository.getAttribute(attributeId);
relation.isDeleted = true; relation.isDeleted = true;
await relation.save(); relation.save();
logFix(`Removed relation ${relation.attributeId} of name "${relation.name} with empty target.`); logFix(`Removed relation ${relation.attributeId} of name "${relation.name} with empty target.`);
} else { } else {
@ -494,7 +494,7 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT attributeId, SELECT attributeId,
type type
FROM attributes FROM attributes
@ -503,11 +503,11 @@ class ConsistencyChecks {
AND type != 'label-definition' AND type != 'label-definition'
AND type != 'relation' AND type != 'relation'
AND type != 'relation-definition'`, AND type != 'relation-definition'`,
async ({attributeId, type}) => { ({attributeId, type}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = await repository.getAttribute(attributeId); const attribute = repository.getAttribute(attributeId);
attribute.type = 'label'; attribute.type = 'label';
await attribute.save(); attribute.save();
logFix(`Attribute ${attributeId} type was changed to label since it had invalid type '${type}'`); logFix(`Attribute ${attributeId} type was changed to label since it had invalid type '${type}'`);
} else { } else {
@ -515,18 +515,18 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT attributeId, SELECT attributeId,
attributes.noteId attributes.noteId
FROM attributes FROM attributes
JOIN notes ON attributes.noteId = notes.noteId JOIN notes ON attributes.noteId = notes.noteId
WHERE attributes.isDeleted = 0 WHERE attributes.isDeleted = 0
AND notes.isDeleted = 1`, AND notes.isDeleted = 1`,
async ({attributeId, noteId}) => { ({attributeId, noteId}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = await repository.getAttribute(attributeId); const attribute = repository.getAttribute(attributeId);
attribute.isDeleted = true; attribute.isDeleted = true;
await attribute.save(); attribute.save();
logFix(`Removed attribute ${attributeId} because owning note ${noteId} is also deleted.`); logFix(`Removed attribute ${attributeId} because owning note ${noteId} is also deleted.`);
} else { } else {
@ -534,7 +534,7 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT attributeId, SELECT attributeId,
attributes.value AS targetNoteId attributes.value AS targetNoteId
FROM attributes FROM attributes
@ -542,11 +542,11 @@ class ConsistencyChecks {
WHERE attributes.type = 'relation' WHERE attributes.type = 'relation'
AND attributes.isDeleted = 0 AND attributes.isDeleted = 0
AND notes.isDeleted = 1`, AND notes.isDeleted = 1`,
async ({attributeId, targetNoteId}) => { ({attributeId, targetNoteId}) => {
if (this.autoFix) { if (this.autoFix) {
const attribute = await repository.getAttribute(attributeId); const attribute = repository.getAttribute(attributeId);
attribute.isDeleted = true; attribute.isDeleted = true;
await attribute.save(); attribute.save();
logFix(`Removed attribute ${attributeId} because target note ${targetNoteId} is also deleted.`); logFix(`Removed attribute ${attributeId} because target note ${targetNoteId} is also deleted.`);
} else { } else {
@ -555,8 +555,8 @@ class ConsistencyChecks {
}); });
} }
async runSyncRowChecks(entityName, key) { runSyncRowChecks(entityName, key) {
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT SELECT
${key} as entityId ${key} as entityId
FROM FROM
@ -564,9 +564,9 @@ class ConsistencyChecks {
LEFT JOIN sync ON sync.entityName = '${entityName}' AND entityId = ${key} LEFT JOIN sync ON sync.entityName = '${entityName}' AND entityId = ${key}
WHERE WHERE
sync.id IS NULL AND ` + (entityName === 'options' ? 'options.isSynced = 1' : '1'), sync.id IS NULL AND ` + (entityName === 'options' ? 'options.isSynced = 1' : '1'),
async ({entityId}) => { ({entityId}) => {
if (this.autoFix) { if (this.autoFix) {
await syncTableService.addEntitySync(entityName, entityId); syncTableService.addEntitySync(entityName, entityId);
logFix(`Created missing sync record for entityName=${entityName}, entityId=${entityId}`); logFix(`Created missing sync record for entityName=${entityName}, entityId=${entityId}`);
} else { } else {
@ -574,7 +574,7 @@ class ConsistencyChecks {
} }
}); });
await this.findAndFixIssues(` this.findAndFixIssues(`
SELECT SELECT
id, entityId id, entityId
FROM FROM
@ -583,9 +583,9 @@ class ConsistencyChecks {
WHERE WHERE
sync.entityName = '${entityName}' sync.entityName = '${entityName}'
AND ${key} IS NULL`, AND ${key} IS NULL`,
async ({id, entityId}) => { ({id, entityId}) => {
if (this.autoFix) { if (this.autoFix) {
await sql.execute("DELETE FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]); sql.execute("DELETE FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`); logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`);
} else { } else {
@ -594,43 +594,43 @@ class ConsistencyChecks {
}); });
} }
async findSyncRowsIssues() { findSyncRowsIssues() {
await this.runSyncRowChecks("notes", "noteId"); this.runSyncRowChecks("notes", "noteId");
await this.runSyncRowChecks("note_contents", "noteId"); this.runSyncRowChecks("note_contents", "noteId");
await this.runSyncRowChecks("note_revisions", "noteRevisionId"); this.runSyncRowChecks("note_revisions", "noteRevisionId");
await this.runSyncRowChecks("branches", "branchId"); this.runSyncRowChecks("branches", "branchId");
await this.runSyncRowChecks("recent_notes", "noteId"); this.runSyncRowChecks("recent_notes", "noteId");
await this.runSyncRowChecks("attributes", "attributeId"); this.runSyncRowChecks("attributes", "attributeId");
await this.runSyncRowChecks("api_tokens", "apiTokenId"); this.runSyncRowChecks("api_tokens", "apiTokenId");
await this.runSyncRowChecks("options", "name"); this.runSyncRowChecks("options", "name");
} }
async runAllChecks() { runAllChecks() {
this.unrecoveredConsistencyErrors = false; this.unrecoveredConsistencyErrors = false;
this.fixedIssues = false; this.fixedIssues = false;
await this.findBrokenReferenceIssues(); this.findBrokenReferenceIssues();
await this.findExistencyIssues(); this.findExistencyIssues();
await this.findLogicIssues(); this.findLogicIssues();
await this.findSyncRowsIssues(); this.findSyncRowsIssues();
// root branch should always be expanded // root branch should always be expanded
await sql.execute("UPDATE branches SET isExpanded = 1 WHERE branchId = 'root'"); sql.execute("UPDATE branches SET isExpanded = 1 WHERE branchId = 'root'");
if (this.unrecoveredConsistencyErrors) { if (this.unrecoveredConsistencyErrors) {
// we run this only if basic checks passed since this assumes basic data consistency // we run this only if basic checks passed since this assumes basic data consistency
await this.checkTreeCycles(); this.checkTreeCycles();
} }
return !this.unrecoveredConsistencyErrors; return !this.unrecoveredConsistencyErrors;
} }
async showEntityStat(name, query) { showEntityStat(name, query) {
const map = await sql.getMap(query); const map = sql.getMap(query);
map[0] = map[0] || 0; map[0] = map[0] || 0;
map[1] = map[1] || 0; map[1] = map[1] || 0;
@ -638,20 +638,20 @@ class ConsistencyChecks {
log.info(`${name} deleted: ${map[1]}, not deleted ${map[0]}`); log.info(`${name} deleted: ${map[1]}, not deleted ${map[0]}`);
} }
async runDbDiagnostics() { runDbDiagnostics() {
await this.showEntityStat("Notes", `SELECT isDeleted, count(1) this.showEntityStat("Notes", `SELECT isDeleted, count(1)
FROM notes FROM notes
GROUP BY isDeleted`); GROUP BY isDeleted`);
await this.showEntityStat("Note revisions", `SELECT isErased, count(1) this.showEntityStat("Note revisions", `SELECT isErased, count(1)
FROM note_revisions FROM note_revisions
GROUP BY isErased`); GROUP BY isErased`);
await this.showEntityStat("Branches", `SELECT isDeleted, count(1) this.showEntityStat("Branches", `SELECT isDeleted, count(1)
FROM branches FROM branches
GROUP BY isDeleted`); GROUP BY isDeleted`);
await this.showEntityStat("Attributes", `SELECT isDeleted, count(1) this.showEntityStat("Attributes", `SELECT isDeleted, count(1)
FROM attributes FROM attributes
GROUP BY isDeleted`); GROUP BY isDeleted`);
await this.showEntityStat("API tokens", `SELECT isDeleted, count(1) this.showEntityStat("API tokens", `SELECT isDeleted, count(1)
FROM api_tokens FROM api_tokens
GROUP BY isDeleted`); GROUP BY isDeleted`);
} }
@ -659,12 +659,12 @@ class ConsistencyChecks {
async runChecks() { async runChecks() {
let elapsedTimeMs; let elapsedTimeMs;
await syncMutexService.doExclusively(async () => { await syncMutexService.doExclusively(() => {
const startTime = new Date(); const startTime = new Date();
await this.runDbDiagnostics(); this.runDbDiagnostics();
await this.runAllChecks(); this.runAllChecks();
elapsedTimeMs = Date.now() - startTime.getTime(); elapsedTimeMs = Date.now() - startTime.getTime();
}); });
@ -687,24 +687,22 @@ function logError(message) {
log.info("Consistency error: " + message); log.info("Consistency error: " + message);
} }
async function runPeriodicChecks() { function runPeriodicChecks() {
const autoFix = await optionsService.getOptionBool('autoFixConsistencyIssues'); const autoFix = optionsService.getOptionBool('autoFixConsistencyIssues');
const consistencyChecks = new ConsistencyChecks(autoFix); const consistencyChecks = new ConsistencyChecks(autoFix);
await consistencyChecks.runChecks(); consistencyChecks.runChecks();
} }
async function runOnDemandChecks(autoFix) { function runOnDemandChecks(autoFix) {
const consistencyChecks = new ConsistencyChecks(autoFix); const consistencyChecks = new ConsistencyChecks(autoFix);
await consistencyChecks.runChecks(); consistencyChecks.runChecks();
} }
sqlInit.dbReady.then(() => { setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000);
setInterval(cls.wrap(runPeriodicChecks), 60 * 60 * 1000);
// kickoff checks soon after startup (to not block the initial load) // kickoff checks soon after startup (to not block the initial load)
setTimeout(cls.wrap(runPeriodicChecks), 20 * 1000); setTimeout(cls.wrap(runPeriodicChecks), 20 * 1000);
});
module.exports = { module.exports = {
runOnDemandChecks runOnDemandChecks

View File

@ -11,8 +11,8 @@ const NoteRevision = require('../entities/note_revision');
const RecentNote = require('../entities/recent_note'); const RecentNote = require('../entities/recent_note');
const Option = require('../entities/option'); const Option = require('../entities/option');
async function getSectorHashes(tableName, primaryKeyName, whereBranch) { function getSectorHashes(tableName, primaryKeyName, whereBranch) {
const hashes = await sql.getRows(`SELECT ${primaryKeyName} AS id, hash FROM ${tableName} ` const hashes = sql.getRows(`SELECT ${primaryKeyName} AS id, hash FROM ${tableName} `
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + (whereBranch ? `WHERE ${whereBranch} ` : '')
+ ` ORDER BY ${primaryKeyName}`); + ` ORDER BY ${primaryKeyName}`);
@ -29,19 +29,19 @@ async function getSectorHashes(tableName, primaryKeyName, whereBranch) {
return map; return map;
} }
async function getEntityHashes() { function getEntityHashes() {
const startTime = new Date(); const startTime = new Date();
const hashes = { const hashes = {
notes: await getSectorHashes(Note.entityName, Note.primaryKeyName), notes: getSectorHashes(Note.entityName, Note.primaryKeyName),
note_contents: await getSectorHashes("note_contents", "noteId"), note_contents: getSectorHashes("note_contents", "noteId"),
branches: await getSectorHashes(Branch.entityName, Branch.primaryKeyName), branches: getSectorHashes(Branch.entityName, Branch.primaryKeyName),
note_revisions: await getSectorHashes(NoteRevision.entityName, NoteRevision.primaryKeyName), note_revisions: getSectorHashes(NoteRevision.entityName, NoteRevision.primaryKeyName),
note_revision_contents: await getSectorHashes("note_revision_contents", "noteRevisionId"), note_revision_contents: getSectorHashes("note_revision_contents", "noteRevisionId"),
recent_notes: await getSectorHashes(RecentNote.entityName, RecentNote.primaryKeyName), recent_notes: getSectorHashes(RecentNote.entityName, RecentNote.primaryKeyName),
options: await getSectorHashes(Option.entityName, Option.primaryKeyName, "isSynced = 1"), options: getSectorHashes(Option.entityName, Option.primaryKeyName, "isSynced = 1"),
attributes: await getSectorHashes(Attribute.entityName, Attribute.primaryKeyName), attributes: getSectorHashes(Attribute.entityName, Attribute.primaryKeyName),
api_tokens: await getSectorHashes(ApiToken.entityName, ApiToken.primaryKeyName), api_tokens: getSectorHashes(ApiToken.entityName, ApiToken.primaryKeyName),
}; };
const elapsedTimeMs = Date.now() - startTime.getTime(); const elapsedTimeMs = Date.now() - startTime.getTime();
@ -51,8 +51,8 @@ async function getEntityHashes() {
return hashes; return hashes;
} }
async function checkContentHashes(otherHashes) { function checkContentHashes(otherHashes) {
const entityHashes = await getEntityHashes(); const entityHashes = getEntityHashes();
const failedChecks = []; const failedChecks = [];
for (const entityName in entityHashes) { for (const entityName in entityHashes) {

View File

@ -13,8 +13,8 @@ const DATE_LABEL = 'dateNote';
const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']; const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
async function createNote(parentNoteId, noteTitle) { function createNote(parentNoteId, noteTitle) {
return (await noteService.createNewNote({ return (noteService.createNewNote({
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
title: noteTitle, title: noteTitle,
content: '', content: '',
@ -23,20 +23,20 @@ async function createNote(parentNoteId, noteTitle) {
})).note; })).note;
} }
async function getNoteStartingWith(parentNoteId, startsWith) { function getNoteStartingWith(parentNoteId, startsWith) {
return await repository.getEntity(`SELECT notes.* FROM notes JOIN branches USING(noteId) return repository.getEntity(`SELECT notes.* FROM notes JOIN branches USING(noteId)
WHERE parentNoteId = ? AND title LIKE '${startsWith}%' WHERE parentNoteId = ? AND title LIKE '${startsWith}%'
AND notes.isDeleted = 0 AND isProtected = 0 AND notes.isDeleted = 0 AND isProtected = 0
AND branches.isDeleted = 0`, [parentNoteId]); AND branches.isDeleted = 0`, [parentNoteId]);
} }
/** @return {Promise<Note>} */ /** @return {Promise<Note>} */
async function getRootCalendarNote() { function getRootCalendarNote() {
// some caching here could be useful (e.g. in CLS) // some caching here could be useful (e.g. in CLS)
let rootNote = await attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL); let rootNote = attributeService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
if (!rootNote) { if (!rootNote) {
rootNote = (await noteService.createNewNote({ rootNote = (noteService.createNewNote({
parentNoteId: 'root', parentNoteId: 'root',
title: 'Calendar', title: 'Calendar',
target: 'into', target: 'into',
@ -45,36 +45,36 @@ async function getRootCalendarNote() {
content: '' content: ''
})).note; })).note;
await attributeService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL); attributeService.createLabel(rootNote.noteId, CALENDAR_ROOT_LABEL);
await attributeService.createLabel(rootNote.noteId, 'sorted'); attributeService.createLabel(rootNote.noteId, 'sorted');
} }
return rootNote; return rootNote;
} }
/** @return {Promise<Note>} */ /** @return {Promise<Note>} */
async function getYearNote(dateStr, rootNote) { function getYearNote(dateStr, rootNote) {
if (!rootNote) { if (!rootNote) {
rootNote = await getRootCalendarNote(); rootNote = getRootCalendarNote();
} }
const yearStr = dateStr.substr(0, 4); const yearStr = dateStr.substr(0, 4);
let yearNote = await attributeService.getNoteWithLabel(YEAR_LABEL, yearStr); let yearNote = attributeService.getNoteWithLabel(YEAR_LABEL, yearStr);
if (!yearNote) { if (!yearNote) {
yearNote = await getNoteStartingWith(rootNote.noteId, yearStr); yearNote = getNoteStartingWith(rootNote.noteId, yearStr);
if (!yearNote) { if (!yearNote) {
yearNote = await createNote(rootNote.noteId, yearStr); yearNote = createNote(rootNote.noteId, yearStr);
await attributeService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr); attributeService.createLabel(yearNote.noteId, YEAR_LABEL, yearStr);
await attributeService.createLabel(yearNote.noteId, 'sorted'); attributeService.createLabel(yearNote.noteId, 'sorted');
const yearTemplateAttr = await rootNote.getOwnedAttribute('relation', 'yearTemplate'); const yearTemplateAttr = rootNote.getOwnedAttribute('relation', 'yearTemplate');
if (yearTemplateAttr) { if (yearTemplateAttr) {
await attributeService.createRelation(yearNote.noteId, 'template', yearTemplateAttr.value); attributeService.createRelation(yearNote.noteId, 'template', yearTemplateAttr.value);
} }
} }
} }
@ -82,8 +82,8 @@ async function getYearNote(dateStr, rootNote) {
return yearNote; return yearNote;
} }
async function getMonthNoteTitle(rootNote, monthNumber, dateObj) { function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
const pattern = await rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}"; const pattern = rootNote.getOwnedLabelValue("monthPattern") || "{monthNumberPadded} - {month}";
const monthName = MONTHS[dateObj.getMonth()]; const monthName = MONTHS[dateObj.getMonth()];
return pattern return pattern
@ -92,35 +92,35 @@ async function getMonthNoteTitle(rootNote, monthNumber, dateObj) {
} }
/** @return {Promise<Note>} */ /** @return {Promise<Note>} */
async function getMonthNote(dateStr, rootNote) { function getMonthNote(dateStr, rootNote) {
if (!rootNote) { if (!rootNote) {
rootNote = await getRootCalendarNote(); rootNote = getRootCalendarNote();
} }
const monthStr = dateStr.substr(0, 7); const monthStr = dateStr.substr(0, 7);
const monthNumber = dateStr.substr(5, 2); const monthNumber = dateStr.substr(5, 2);
let monthNote = await attributeService.getNoteWithLabel(MONTH_LABEL, monthStr); let monthNote = attributeService.getNoteWithLabel(MONTH_LABEL, monthStr);
if (!monthNote) { if (!monthNote) {
const yearNote = await getYearNote(dateStr, rootNote); const yearNote = getYearNote(dateStr, rootNote);
monthNote = await getNoteStartingWith(yearNote.noteId, monthNumber); monthNote = getNoteStartingWith(yearNote.noteId, monthNumber);
if (!monthNote) { if (!monthNote) {
const dateObj = dateUtils.parseLocalDate(dateStr); const dateObj = dateUtils.parseLocalDate(dateStr);
const noteTitle = await getMonthNoteTitle(rootNote, monthNumber, dateObj); const noteTitle = getMonthNoteTitle(rootNote, monthNumber, dateObj);
monthNote = await createNote(yearNote.noteId, noteTitle); monthNote = createNote(yearNote.noteId, noteTitle);
await attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr); attributeService.createLabel(monthNote.noteId, MONTH_LABEL, monthStr);
await attributeService.createLabel(monthNote.noteId, 'sorted'); attributeService.createLabel(monthNote.noteId, 'sorted');
const monthTemplateAttr = await rootNote.getOwnedAttribute('relation', 'monthTemplate'); const monthTemplateAttr = rootNote.getOwnedAttribute('relation', 'monthTemplate');
if (monthTemplateAttr) { if (monthTemplateAttr) {
await attributeService.createRelation(monthNote.noteId, 'template', monthTemplateAttr.value); attributeService.createRelation(monthNote.noteId, 'template', monthTemplateAttr.value);
} }
} }
} }
@ -128,8 +128,8 @@ async function getMonthNote(dateStr, rootNote) {
return monthNote; return monthNote;
} }
async function getDateNoteTitle(rootNote, dayNumber, dateObj) { function getDateNoteTitle(rootNote, dayNumber, dateObj) {
const pattern = await rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}"; const pattern = rootNote.getOwnedLabelValue("datePattern") || "{dayInMonthPadded} - {weekDay}";
const weekDay = DAYS[dateObj.getDay()]; const weekDay = DAYS[dateObj.getDay()];
return pattern return pattern
@ -140,31 +140,31 @@ async function getDateNoteTitle(rootNote, dayNumber, dateObj) {
} }
/** @return {Promise<Note>} */ /** @return {Promise<Note>} */
async function getDateNote(dateStr) { function getDateNote(dateStr) {
const rootNote = await getRootCalendarNote(); const rootNote = getRootCalendarNote();
const dayNumber = dateStr.substr(8, 2); const dayNumber = dateStr.substr(8, 2);
let dateNote = await attributeService.getNoteWithLabel(DATE_LABEL, dateStr); let dateNote = attributeService.getNoteWithLabel(DATE_LABEL, dateStr);
if (!dateNote) { if (!dateNote) {
const monthNote = await getMonthNote(dateStr, rootNote); const monthNote = getMonthNote(dateStr, rootNote);
dateNote = await getNoteStartingWith(monthNote.noteId, dayNumber); dateNote = getNoteStartingWith(monthNote.noteId, dayNumber);
if (!dateNote) { if (!dateNote) {
const dateObj = dateUtils.parseLocalDate(dateStr); const dateObj = dateUtils.parseLocalDate(dateStr);
const noteTitle = await getDateNoteTitle(rootNote, dayNumber, dateObj); const noteTitle = getDateNoteTitle(rootNote, dayNumber, dateObj);
dateNote = await createNote(monthNote.noteId, noteTitle); dateNote = createNote(monthNote.noteId, noteTitle);
await attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substr(0, 10)); attributeService.createLabel(dateNote.noteId, DATE_LABEL, dateStr.substr(0, 10));
const dateTemplateAttr = await rootNote.getOwnedAttribute('relation', 'dateTemplate'); const dateTemplateAttr = rootNote.getOwnedAttribute('relation', 'dateTemplate');
if (dateTemplateAttr) { if (dateTemplateAttr) {
await attributeService.createRelation(dateNote.noteId, 'template', dateTemplateAttr.value); attributeService.createRelation(dateNote.noteId, 'template', dateTemplateAttr.value);
} }
} }
} }
@ -172,8 +172,8 @@ async function getDateNote(dateStr) {
return dateNote; return dateNote;
} }
async function getTodayNote() { function getTodayNote() {
return await getDateNote(dateUtils.localNowDate()); return getDateNote(dateUtils.localNowDate());
} }
function getStartOfTheWeek(date, startOfTheWeek) { function getStartOfTheWeek(date, startOfTheWeek) {
@ -193,7 +193,7 @@ function getStartOfTheWeek(date, startOfTheWeek) {
return new Date(date.setDate(diff)); return new Date(date.setDate(diff));
} }
async function getWeekNote(dateStr, options = {}) { function getWeekNote(dateStr, options = {}) {
const startOfTheWeek = options.startOfTheWeek || "monday"; const startOfTheWeek = options.startOfTheWeek || "monday";
const dateObj = getStartOfTheWeek(dateUtils.parseLocalDate(dateStr), startOfTheWeek); const dateObj = getStartOfTheWeek(dateUtils.parseLocalDate(dateStr), startOfTheWeek);

View File

@ -25,13 +25,13 @@ function subscribe(eventTypes, listener) {
} }
} }
async function emit(eventType, data) { function emit(eventType, data) {
const listeners = eventListeners[eventType]; const listeners = eventListeners[eventType];
if (listeners) { if (listeners) {
for (const listener of listeners) { for (const listener of listeners) {
try { try {
await listener(data); listener(data);
} }
catch (e) { catch (e) {
log.error("Listener threw error: " + e.stack); log.error("Listener threw error: " + e.stack);

View File

@ -3,20 +3,20 @@
const repository = require("../repository"); const repository = require("../repository");
const utils = require('../utils'); const utils = require('../utils');
async function exportToOpml(taskContext, branch, version, res) { function exportToOpml(taskContext, branch, version, res) {
if (!['1.0', '2.0'].includes(version)) { if (!['1.0', '2.0'].includes(version)) {
throw new Error("Unrecognized OPML version " + version); throw new Error("Unrecognized OPML version " + version);
} }
const opmlVersion = parseInt(version); const opmlVersion = parseInt(version);
const note = await branch.getNote(); const note = branch.getNote();
async function exportNoteInner(branchId) { function exportNoteInner(branchId) {
const branch = await repository.getBranch(branchId); const branch = repository.getBranch(branchId);
const note = await branch.getNote(); const note = branch.getNote();
if (await note.hasOwnedLabel('excludeFromExport')) { if (note.hasOwnedLabel('excludeFromExport')) {
return; return;
} }
@ -24,13 +24,13 @@ async function exportToOpml(taskContext, branch, version, res) {
if (opmlVersion === 1) { if (opmlVersion === 1) {
const preparedTitle = escapeXmlAttribute(title); const preparedTitle = escapeXmlAttribute(title);
const preparedContent = note.isStringNote() ? prepareText(await note.getContent()) : ''; const preparedContent = note.isStringNote() ? prepareText(note.getContent()) : '';
res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`); res.write(`<outline title="${preparedTitle}" text="${preparedContent}">\n`);
} }
else if (opmlVersion === 2) { else if (opmlVersion === 2) {
const preparedTitle = escapeXmlAttribute(title); const preparedTitle = escapeXmlAttribute(title);
const preparedContent = note.isStringNote() ? escapeXmlAttribute(await note.getContent()) : ''; const preparedContent = note.isStringNote() ? escapeXmlAttribute(note.getContent()) : '';
res.write(`<outline text="${preparedTitle}" _note="${preparedContent}">\n`); res.write(`<outline text="${preparedTitle}" _note="${preparedContent}">\n`);
} }
@ -40,8 +40,8 @@ async function exportToOpml(taskContext, branch, version, res) {
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
for (const child of await note.getChildBranches()) { for (const child of note.getChildBranches()) {
await exportNoteInner(child.branchId); exportNoteInner(child.branchId);
} }
res.write('</outline>'); res.write('</outline>');
@ -60,7 +60,7 @@ async function exportToOpml(taskContext, branch, version, res) {
</head> </head>
<body>`); <body>`);
await exportNoteInner(branch.branchId); exportNoteInner(branch.branchId);
res.write(`</body> res.write(`</body>
</opml>`); </opml>`);

View File

@ -5,8 +5,8 @@ const html = require('html');
const utils = require('../utils'); const utils = require('../utils');
const mdService = require('./md'); const mdService = require('./md');
async function exportSingleNote(taskContext, branch, format, res) { function exportSingleNote(taskContext, branch, format, res) {
const note = await branch.getNote(); const note = branch.getNote();
if (note.type === 'image' || note.type === 'file') { if (note.type === 'image' || note.type === 'file') {
return [400, `Note type ${note.type} cannot be exported as single file.`]; return [400, `Note type ${note.type} cannot be exported as single file.`];
@ -18,7 +18,7 @@ async function exportSingleNote(taskContext, branch, format, res) {
let payload, extension, mime; let payload, extension, mime;
let content = await note.getContent(); let content = note.getContent();
if (note.type === 'text') { if (note.type === 'text') {
if (format === 'html') { if (format === 'html') {

View File

@ -20,7 +20,7 @@ const yazl = require("yazl");
* @param {Branch} branch * @param {Branch} branch
* @param {string} format - 'html' or 'markdown' * @param {string} format - 'html' or 'markdown'
*/ */
async function exportToZip(taskContext, branch, format, res) { function exportToZip(taskContext, branch, format, res) {
const zipFile = new yazl.ZipFile(); const zipFile = new yazl.ZipFile();
const noteIdToMeta = {}; const noteIdToMeta = {};
@ -80,10 +80,10 @@ async function exportToZip(taskContext, branch, format, res) {
return getUniqueFilename(existingFileNames, fileName); return getUniqueFilename(existingFileNames, fileName);
} }
async function getNoteMeta(branch, parentMeta, existingFileNames) { function getNoteMeta(branch, parentMeta, existingFileNames) {
const note = await branch.getNote(); const note = branch.getNote();
if (await note.hasOwnedLabel('excludeFromExport')) { if (note.hasOwnedLabel('excludeFromExport')) {
return; return;
} }
@ -122,7 +122,7 @@ async function exportToZip(taskContext, branch, format, res) {
type: note.type, type: note.type,
mime: note.mime, mime: note.mime,
// we don't export utcDateCreated and utcDateModified of any entity since that would be a bit misleading // we don't export utcDateCreated and utcDateModified of any entity since that would be a bit misleading
attributes: (await note.getOwnedAttributes()).map(attribute => ({ attributes: (note.getOwnedAttributes()).map(attribute => ({
type: attribute.type, type: attribute.type,
name: attribute.name, name: attribute.name,
value: attribute.value, value: attribute.value,
@ -139,12 +139,12 @@ async function exportToZip(taskContext, branch, format, res) {
noteIdToMeta[note.noteId] = meta; noteIdToMeta[note.noteId] = meta;
const childBranches = await note.getChildBranches(); const childBranches = note.getChildBranches();
const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable(); const available = !note.isProtected || protectedSessionService.isProtectedSessionAvailable();
// if it's a leaf then we'll export it even if it's empty // if it's a leaf then we'll export it even if it's empty
if (available && ((await note.getContent()).length > 0 || childBranches.length === 0)) { if (available && ((note.getContent()).length > 0 || childBranches.length === 0)) {
meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames); meta.dataFileName = getDataFileName(note, baseFileName, existingFileNames);
} }
@ -156,7 +156,7 @@ async function exportToZip(taskContext, branch, format, res) {
const childExistingNames = {}; const childExistingNames = {};
for (const childBranch of childBranches) { for (const childBranch of childBranches) {
const note = await getNoteMeta(childBranch, meta, childExistingNames); const note = getNoteMeta(childBranch, meta, childExistingNames);
// can be undefined if export is disabled for this note // can be undefined if export is disabled for this note
if (note) { if (note) {
@ -258,7 +258,7 @@ ${content}
// noteId => file path // noteId => file path
const notePaths = {}; const notePaths = {};
async function saveNote(noteMeta, filePathPrefix) { function saveNote(noteMeta, filePathPrefix) {
if (noteMeta.isClone) { if (noteMeta.isClone) {
const targetUrl = getTargetUrl(noteMeta.noteId, noteMeta); const targetUrl = getTargetUrl(noteMeta.noteId, noteMeta);
@ -271,12 +271,12 @@ ${content}
return; return;
} }
const note = await repository.getNote(noteMeta.noteId); const note = repository.getNote(noteMeta.noteId);
notePaths[note.noteId] = filePathPrefix + (noteMeta.dataFileName || noteMeta.dirFileName); notePaths[note.noteId] = filePathPrefix + (noteMeta.dataFileName || noteMeta.dirFileName);
if (noteMeta.dataFileName) { if (noteMeta.dataFileName) {
const content = prepareContent(noteMeta.title, await note.getContent(), noteMeta); const content = prepareContent(noteMeta.title, note.getContent(), noteMeta);
zipFile.addBuffer(content, filePathPrefix + noteMeta.dataFileName, {mtime: dateUtils.parseDateTime(note.utcDateModified)}); zipFile.addBuffer(content, filePathPrefix + noteMeta.dataFileName, {mtime: dateUtils.parseDateTime(note.utcDateModified)});
} }
@ -289,12 +289,12 @@ ${content}
zipFile.addEmptyDirectory(directoryPath, {mtime: dateUtils.parseDateTime(note.utcDateModified)}); zipFile.addEmptyDirectory(directoryPath, {mtime: dateUtils.parseDateTime(note.utcDateModified)});
for (const childMeta of noteMeta.children) { for (const childMeta of noteMeta.children) {
await saveNote(childMeta, directoryPath + '/'); saveNote(childMeta, directoryPath + '/');
} }
} }
} }
async function saveNavigation(rootMeta, navigationMeta) { function saveNavigation(rootMeta, navigationMeta) {
function saveNavigationInner(meta) { function saveNavigationInner(meta) {
let html = '<li>'; let html = '<li>';
@ -336,7 +336,7 @@ ${content}
zipFile.addBuffer(prettyHtml, navigationMeta.dataFileName); zipFile.addBuffer(prettyHtml, navigationMeta.dataFileName);
} }
async function saveIndex(rootMeta, indexMeta) { function saveIndex(rootMeta, indexMeta) {
let firstNonEmptyNote; let firstNonEmptyNote;
let curMeta = rootMeta; let curMeta = rootMeta;
@ -367,14 +367,14 @@ ${content}
zipFile.addBuffer(fullHtml, indexMeta.dataFileName); zipFile.addBuffer(fullHtml, indexMeta.dataFileName);
} }
async function saveCss(rootMeta, cssMeta) { function saveCss(rootMeta, cssMeta) {
const cssContent = fs.readFileSync(RESOURCE_DIR + '/libraries/ckeditor/ckeditor-content.css'); const cssContent = fs.readFileSync(RESOURCE_DIR + '/libraries/ckeditor/ckeditor-content.css');
zipFile.addBuffer(cssContent, cssMeta.dataFileName); zipFile.addBuffer(cssContent, cssMeta.dataFileName);
} }
const existingFileNames = format === 'html' ? ['navigation', 'index'] : []; const existingFileNames = format === 'html' ? ['navigation', 'index'] : [];
const rootMeta = await getNoteMeta(branch, { notePath: [] }, existingFileNames); const rootMeta = getNoteMeta(branch, { notePath: [] }, existingFileNames);
const metaFile = { const metaFile = {
formatVersion: 1, formatVersion: 1,
@ -421,15 +421,15 @@ ${content}
zipFile.addBuffer(metaFileJson, "!!!meta.json"); zipFile.addBuffer(metaFileJson, "!!!meta.json");
await saveNote(rootMeta, ''); saveNote(rootMeta, '');
if (format === 'html') { if (format === 'html') {
await saveNavigation(rootMeta, navigationMeta); saveNavigation(rootMeta, navigationMeta);
await saveIndex(rootMeta, indexMeta); saveIndex(rootMeta, indexMeta);
await saveCss(rootMeta, cssMeta); saveCss(rootMeta, cssMeta);
} }
const note = await branch.getNote(); const note = branch.getNote();
const zipFileName = (branch.prefix ? (branch.prefix + " - ") : "") + note.title + ".zip"; const zipFileName = (branch.prefix ? (branch.prefix + " - ") : "") + note.title + ".zip";
res.setHeader('Content-Disposition', utils.getContentDisposition(zipFileName)); res.setHeader('Content-Disposition', utils.getContentDisposition(zipFileName));

View File

@ -5,14 +5,14 @@ const log = require('./log');
const repository = require('./repository'); const repository = require('./repository');
const Attribute = require('../entities/attribute'); const Attribute = require('../entities/attribute');
async function runAttachedRelations(note, relationName, originEntity) { function runAttachedRelations(note, relationName, originEntity) {
const runRelations = await note.getRelations(relationName); const runRelations = note.getRelations(relationName);
for (const relation of runRelations) { for (const relation of runRelations) {
const scriptNote = await relation.getTargetNote(); const scriptNote = relation.getTargetNote();
if (scriptNote) { if (scriptNote) {
await scriptService.executeNoteNoException(scriptNote, { originEntity }); scriptService.executeNoteNoException(scriptNote, { originEntity });
} }
else { else {
log.error(`Target note ${relation.value} of atttribute ${relation.attributeId} has not been found.`); log.error(`Target note ${relation.value} of atttribute ${relation.attributeId} has not been found.`);
@ -20,90 +20,90 @@ async function runAttachedRelations(note, relationName, originEntity) {
} }
} }
eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => { eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => {
await runAttachedRelations(note, 'runOnNoteTitleChange', note); runAttachedRelations(note, 'runOnNoteTitleChange', note);
if (!note.isRoot()) { if (!note.isRoot()) {
const parents = await note.getParentNotes(); const parents = note.getParentNotes();
for (const parent of parents) { for (const parent of parents) {
if (await parent.hasOwnedLabel("sorted")) { if (parent.hasOwnedLabel("sorted")) {
await treeService.sortNotesAlphabetically(parent.noteId); treeService.sortNotesAlphabetically(parent.noteId);
} }
} }
} }
}); });
eventService.subscribe([ eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED ], async ({ entityName, entity }) => { eventService.subscribe([ eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED ], ({ entityName, entity }) => {
if (entityName === 'attributes') { if (entityName === 'attributes') {
await runAttachedRelations(await entity.getNote(), 'runOnAttributeChange', entity); runAttachedRelations(entity.getNote(), 'runOnAttributeChange', entity);
} }
else if (entityName === 'notes') { else if (entityName === 'notes') {
await runAttachedRelations(entity, 'runOnNoteChange', entity); runAttachedRelations(entity, 'runOnNoteChange', entity);
} }
}); });
eventService.subscribe(eventService.ENTITY_CREATED, async ({ entityName, entity }) => { eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => {
if (entityName === 'attributes') { if (entityName === 'attributes') {
await runAttachedRelations(await entity.getNote(), 'runOnAttributeCreation', entity); runAttachedRelations(entity.getNote(), 'runOnAttributeCreation', entity);
if (entity.type === 'relation' && entity.name === 'template') { if (entity.type === 'relation' && entity.name === 'template') {
const note = await repository.getNote(entity.noteId); const note = repository.getNote(entity.noteId);
if (!note.isStringNote()) { if (!note.isStringNote()) {
return; return;
} }
const content = await note.getContent(); const content = note.getContent();
if (content && content.trim().length > 0) { if (content && content.trim().length > 0) {
return; return;
} }
const targetNote = await repository.getNote(entity.value); const targetNote = repository.getNote(entity.value);
if (!targetNote || !targetNote.isStringNote()) { if (!targetNote || !targetNote.isStringNote()) {
return; return;
} }
await note.setContent(await targetNote.getContent()); note.setContent(targetNote.getContent());
} }
} }
else if (entityName === 'notes') { else if (entityName === 'notes') {
await runAttachedRelations(entity, 'runOnNoteCreation', entity); runAttachedRelations(entity, 'runOnNoteCreation', entity);
} }
}); });
eventService.subscribe(eventService.CHILD_NOTE_CREATED, async ({ parentNote, childNote }) => { eventService.subscribe(eventService.CHILD_NOTE_CREATED, ({ parentNote, childNote }) => {
await runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote); runAttachedRelations(parentNote, 'runOnChildNoteCreation', childNote);
}); });
async function processInverseRelations(entityName, entity, handler) { function processInverseRelations(entityName, entity, handler) {
if (entityName === 'attributes' && entity.type === 'relation') { if (entityName === 'attributes' && entity.type === 'relation') {
const note = await entity.getNote(); const note = entity.getNote();
const attributes = (await note.getOwnedAttributes(entity.name)).filter(relation => relation.type === 'relation-definition'); const attributes = (note.getOwnedAttributes(entity.name)).filter(relation => relation.type === 'relation-definition');
for (const attribute of attributes) { for (const attribute of attributes) {
const definition = attribute.value; const definition = attribute.value;
if (definition.inverseRelation && definition.inverseRelation.trim()) { if (definition.inverseRelation && definition.inverseRelation.trim()) {
const targetNote = await entity.getTargetNote(); const targetNote = entity.getTargetNote();
await handler(definition, note, targetNote); handler(definition, note, targetNote);
} }
} }
} }
} }
eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity }) => { eventService.subscribe(eventService.ENTITY_CHANGED, ({ entityName, entity }) => {
await processInverseRelations(entityName, entity, async (definition, note, targetNote) => { processInverseRelations(entityName, entity, (definition, note, targetNote) => {
// we need to make sure that also target's inverse attribute exists and if note, then create it // we need to make sure that also target's inverse attribute exists and if note, then create it
// inverse attribute has to target our note as well // inverse attribute has to target our note as well
const hasInverseAttribute = (await targetNote.getRelations(definition.inverseRelation)) const hasInverseAttribute = (targetNote.getRelations(definition.inverseRelation))
.some(attr => attr.value === note.noteId); .some(attr => attr.value === note.noteId);
if (!hasInverseAttribute) { if (!hasInverseAttribute) {
await new Attribute({ new Attribute({
noteId: targetNote.noteId, noteId: targetNote.noteId,
type: 'relation', type: 'relation',
name: definition.inverseRelation, name: definition.inverseRelation,
@ -116,16 +116,16 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({ entityName, entity
}); });
}); });
eventService.subscribe(eventService.ENTITY_DELETED, async ({ entityName, entity }) => { eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) => {
await processInverseRelations(entityName, entity, async (definition, note, targetNote) => { processInverseRelations(entityName, entity, (definition, note, targetNote) => {
// if one inverse attribute is deleted then the other should be deleted as well // if one inverse attribute is deleted then the other should be deleted as well
const relations = await targetNote.getOwnedRelations(definition.inverseRelation); const relations = targetNote.getOwnedRelations(definition.inverseRelation);
let deletedSomething = false; let deletedSomething = false;
for (const relation of relations) { for (const relation of relations) {
if (relation.value === note.noteId) { if (relation.value === note.noteId) {
relation.isDeleted = true; relation.isDeleted = true;
await relation.save(); relation.save();
deletedSomething = true; deletedSomething = true;
} }

View File

@ -3,12 +3,10 @@ const sqlInit = require('./sql_init');
const eventService = require('./events'); const eventService = require('./events');
const hoistedNote = require('./hoisted_note'); const hoistedNote = require('./hoisted_note');
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entity}) => { eventService.subscribe(eventService.ENTITY_CHANGED, ({entityName, entity}) => {
if (entityName === 'options' && entity.name === 'hoistedNoteId') { if (entityName === 'options' && entity.name === 'hoistedNoteId') {
hoistedNote.setHoistedNoteId(entity.value); hoistedNote.setHoistedNoteId(entity.value);
} }
}); });
sqlInit.dbReady.then(async () => { hoistedNote.setHoistedNoteId(optionService.getOption('hoistedNoteId'));
hoistedNote.setHoistedNoteId(await optionService.getOption('hoistedNoteId'));
});

View File

@ -15,7 +15,7 @@ const sanitizeFilename = require('sanitize-filename');
const noteRevisionService = require('./note_revisions.js'); const noteRevisionService = require('./note_revisions.js');
const isSvg = require('is-svg'); const isSvg = require('is-svg');
async function processImage(uploadBuffer, originalName, shrinkImageSwitch) { function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
const origImageFormat = getImageType(uploadBuffer); const origImageFormat = getImageType(uploadBuffer);
if (origImageFormat && ["webp", "svg"].includes(origImageFormat.ext)) { if (origImageFormat && ["webp", "svg"].includes(origImageFormat.ext)) {
@ -23,7 +23,7 @@ async function processImage(uploadBuffer, originalName, shrinkImageSwitch) {
shrinkImageSwitch = false; shrinkImageSwitch = false;
} }
const finalImageBuffer = shrinkImageSwitch ? await shrinkImage(uploadBuffer, originalName) : uploadBuffer; const finalImageBuffer = shrinkImageSwitch ? shrinkImage(uploadBuffer, originalName) : uploadBuffer;
const imageFormat = getImageType(finalImageBuffer); const imageFormat = getImageType(finalImageBuffer);
@ -50,34 +50,34 @@ function getImageMimeFromExtension(ext) {
return 'image/' + (ext === 'svg' ? 'svg+xml' : ext); return 'image/' + (ext === 'svg' ? 'svg+xml' : ext);
} }
async function updateImage(noteId, uploadBuffer, originalName) { function updateImage(noteId, uploadBuffer, originalName) {
log.info(`Updating image ${noteId}: ${originalName}`); log.info(`Updating image ${noteId}: ${originalName}`);
const {buffer, imageFormat} = await processImage(uploadBuffer, originalName, true); const {buffer, imageFormat} = processImage(uploadBuffer, originalName, true);
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
await noteRevisionService.createNoteRevision(note); noteRevisionService.createNoteRevision(note);
note.mime = getImageMimeFromExtension(imageFormat.ext); note.mime = getImageMimeFromExtension(imageFormat.ext);
await note.setContent(buffer); note.setContent(buffer);
await note.setLabel('originalFileName', originalName); note.setLabel('originalFileName', originalName);
await noteRevisionService.protectNoteRevisions(note); noteRevisionService.protectNoteRevisions(note);
} }
async function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch) { function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSwitch) {
log.info(`Saving image ${originalName}`); log.info(`Saving image ${originalName}`);
const {buffer, imageFormat} = await processImage(uploadBuffer, originalName, shrinkImageSwitch); const {buffer, imageFormat} = processImage(uploadBuffer, originalName, shrinkImageSwitch);
const fileName = sanitizeFilename(originalName); const fileName = sanitizeFilename(originalName);
const parentNote = await repository.getNote(parentNoteId); const parentNote = repository.getNote(parentNoteId);
const {note} = await noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId, parentNoteId,
title: fileName, title: fileName,
content: buffer, content: buffer,
@ -86,7 +86,7 @@ async function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSw
isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable() isProtected: parentNote.isProtected && protectedSessionService.isProtectedSessionAvailable()
}); });
await note.addLabel('originalFileName', originalName); note.addLabel('originalFileName', originalName);
return { return {
fileName, fileName,
@ -96,18 +96,18 @@ async function saveImage(parentNoteId, uploadBuffer, originalName, shrinkImageSw
}; };
} }
async function shrinkImage(buffer, originalName) { function shrinkImage(buffer, originalName) {
// we do resizing with max (100) quality which will be trimmed during optimization step next // we do resizing with max (100) quality which will be trimmed during optimization step next
const resizedImage = await resize(buffer, 100); const resizedImage = resize(buffer, 100);
let finalImageBuffer; let finalImageBuffer;
const jpegQuality = await optionService.getOptionInt('imageJpegQuality'); const jpegQuality = optionService.getOptionInt('imageJpegQuality');
try { try {
finalImageBuffer = await optimize(resizedImage, jpegQuality); finalImageBuffer = optimize(resizedImage, jpegQuality);
} catch (e) { } catch (e) {
log.error("Failed to optimize image '" + originalName + "'\nStack: " + e.stack); log.error("Failed to optimize image '" + originalName + "'\nStack: " + e.stack);
finalImageBuffer = await resize(buffer, jpegQuality); finalImageBuffer = resize(buffer, jpegQuality);
} }
// if resizing & shrinking did not help with size then save the original // if resizing & shrinking did not help with size then save the original
@ -119,10 +119,10 @@ async function shrinkImage(buffer, originalName) {
return finalImageBuffer; return finalImageBuffer;
} }
async function resize(buffer, quality) { function resize(buffer, quality) {
const imageMaxWidthHeight = await optionService.getOptionInt('imageMaxWidthHeight'); const imageMaxWidthHeight = optionService.getOptionInt('imageMaxWidthHeight');
const image = await jimp.read(buffer); const image = jimp.read(buffer);
if (image.bitmap.width > image.bitmap.height && image.bitmap.width > imageMaxWidthHeight) { if (image.bitmap.width > image.bitmap.height && image.bitmap.width > imageMaxWidthHeight) {
image.resize(imageMaxWidthHeight, jimp.AUTO); image.resize(imageMaxWidthHeight, jimp.AUTO);
@ -139,8 +139,8 @@ async function resize(buffer, quality) {
return image.getBufferAsync(jimp.MIME_JPEG); return image.getBufferAsync(jimp.MIME_JPEG);
} }
async function optimize(buffer, jpegQuality) { function optimize(buffer, jpegQuality) {
return await imagemin.buffer(buffer, { return imagemin.buffer(buffer, {
plugins: [ plugins: [
imageminMozJpeg({ imageminMozJpeg({
quality: jpegQuality quality: jpegQuality

View File

@ -20,7 +20,7 @@ function parseDate(text) {
let note = {}; let note = {};
let resource; let resource;
async function importEnex(taskContext, file, parentNote) { function importEnex(taskContext, file, parentNote) {
const saxStream = sax.createStream(true); const saxStream = sax.createStream(true);
const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex") const rootNoteTitle = file.originalname.toLowerCase().endsWith(".enex")
@ -28,7 +28,7 @@ async function importEnex(taskContext, file, parentNote) {
: file.originalname; : file.originalname;
// root note is new note into all ENEX/notebook's notes will be imported // root note is new note into all ENEX/notebook's notes will be imported
const rootNote = (await noteService.createNewNote({ const rootNote = (noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title: rootNoteTitle, title: rootNoteTitle,
content: "", content: "",
@ -191,9 +191,9 @@ async function importEnex(taskContext, file, parentNote) {
} }
}); });
async function updateDates(noteId, utcDateCreated, utcDateModified) { function updateDates(noteId, utcDateCreated, utcDateModified) {
// it's difficult to force custom dateCreated and dateModified to Note entity so we do it post-creation with SQL // it's difficult to force custom dateCreated and dateModified to Note entity so we do it post-creation with SQL
await sql.execute(` sql.execute(`
UPDATE notes UPDATE notes
SET dateCreated = ?, SET dateCreated = ?,
utcDateCreated = ?, utcDateCreated = ?,
@ -202,20 +202,20 @@ async function importEnex(taskContext, file, parentNote) {
WHERE noteId = ?`, WHERE noteId = ?`,
[utcDateCreated, utcDateCreated, utcDateModified, utcDateModified, noteId]); [utcDateCreated, utcDateCreated, utcDateModified, utcDateModified, noteId]);
await sql.execute(` sql.execute(`
UPDATE note_contents UPDATE note_contents
SET utcDateModified = ? SET utcDateModified = ?
WHERE noteId = ?`, WHERE noteId = ?`,
[utcDateModified, noteId]); [utcDateModified, noteId]);
} }
async function saveNote() { function saveNote() {
// make a copy because stream continues with the next async call and note gets overwritten // make a copy because stream continues with the next call and note gets overwritten
let {title, content, attributes, resources, utcDateCreated, utcDateModified} = note; let {title, content, attributes, resources, utcDateCreated, utcDateModified} = note;
content = extractContent(content); content = extractContent(content);
const noteEntity = (await noteService.createNewNote({ const noteEntity = (noteService.createNewNote({
parentNoteId: rootNote.noteId, parentNoteId: rootNote.noteId,
title, title,
content, content,
@ -226,7 +226,7 @@ async function importEnex(taskContext, file, parentNote) {
})).note; })).note;
for (const attr of attributes) { for (const attr of attributes) {
await noteEntity.addAttribute(attr.type, attr.name, attr.value); noteEntity.addAttribute(attr.type, attr.name, attr.value);
} }
utcDateCreated = utcDateCreated || noteEntity.utcDateCreated; utcDateCreated = utcDateCreated || noteEntity.utcDateCreated;
@ -240,14 +240,14 @@ async function importEnex(taskContext, file, parentNote) {
const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g'); const mediaRegex = new RegExp(`<en-media hash="${hash}"[^>]*>`, 'g');
const fileTypeFromBuffer = await FileType.fromBuffer(resource.content); const fileTypeFromBuffer = FileType.fromBuffer(resource.content);
if (fileTypeFromBuffer) { if (fileTypeFromBuffer) {
// If fileType returns something for buffer, then set the mime given // If fileType returns something for buffer, then set the mime given
resource.mime = fileTypeFromBuffer.mime; resource.mime = fileTypeFromBuffer.mime;
} }
const createFileNote = async () => { const createFileNote = () => {
const resourceNote = (await noteService.createNewNote({ const resourceNote = (noteService.createNewNote({
parentNoteId: noteEntity.noteId, parentNoteId: noteEntity.noteId,
title: resource.title, title: resource.title,
content: resource.content, content: resource.content,
@ -257,10 +257,10 @@ async function importEnex(taskContext, file, parentNote) {
})).note; })).note;
for (const attr of resource.attributes) { for (const attr of resource.attributes) {
await noteEntity.addAttribute(attr.type, attr.name, attr.value); noteEntity.addAttribute(attr.type, attr.name, attr.value);
} }
await updateDates(resourceNote.noteId, utcDateCreated, utcDateModified); updateDates(resourceNote.noteId, utcDateCreated, utcDateModified);
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
@ -273,9 +273,9 @@ async function importEnex(taskContext, file, parentNote) {
try { try {
const originalName = "image." + resource.mime.substr(6); const originalName = "image." + resource.mime.substr(6);
const {url, note: imageNote} = await imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages); const {url, note: imageNote} = imageService.saveImage(noteEntity.noteId, resource.content, originalName, taskContext.data.shrinkImages);
await updateDates(imageNote.noteId, utcDateCreated, utcDateModified); updateDates(imageNote.noteId, utcDateCreated, utcDateModified);
const imageLink = `<img src="${url}">`; const imageLink = `<img src="${url}">`;
@ -288,22 +288,22 @@ async function importEnex(taskContext, file, parentNote) {
} }
} catch (e) { } catch (e) {
log.error("error when saving image from ENEX file: " + e); log.error("error when saving image from ENEX file: " + e);
await createFileNote(); createFileNote();
} }
} else { } else {
await createFileNote(); createFileNote();
} }
} }
// save updated content with links to files/images // save updated content with links to files/images
await noteEntity.setContent(content); noteEntity.setContent(content);
await noteService.scanForLinks(noteEntity); noteService.scanForLinks(noteEntity);
await updateDates(noteEntity.noteId, utcDateCreated, utcDateModified); updateDates(noteEntity.noteId, utcDateCreated, utcDateModified);
} }
saxStream.on("closetag", async tag => { saxStream.on("closetag", tag => {
path.pop(); path.pop();
if (tag === 'note') { if (tag === 'note') {

View File

@ -10,8 +10,8 @@ const protectedSessionService = require('../protected_session');
* @param {Note} parentNote * @param {Note} parentNote
* @return {Promise<*[]|*>} * @return {Promise<*[]|*>}
*/ */
async function importOpml(taskContext, fileBuffer, parentNote) { function importOpml(taskContext, fileBuffer, parentNote) {
const xml = await new Promise(function(resolve, reject) const xml = new Promise(function(resolve, reject)
{ {
parseString(fileBuffer, function (err, result) { parseString(fileBuffer, function (err, result) {
if (err) { if (err) {
@ -29,7 +29,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
const opmlVersion = parseInt(xml.opml.$.version); const opmlVersion = parseInt(xml.opml.$.version);
async function importOutline(outline, parentNoteId) { function importOutline(outline, parentNoteId) {
let title, content; let title, content;
if (opmlVersion === 1) { if (opmlVersion === 1) {
@ -44,7 +44,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
throw new Error("Unrecognized OPML version " + opmlVersion); throw new Error("Unrecognized OPML version " + opmlVersion);
} }
const {note} = await noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId, parentNoteId,
title, title,
content, content,
@ -55,7 +55,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
for (const childOutline of (outline.outline || [])) { for (const childOutline of (outline.outline || [])) {
await importOutline(childOutline, note.noteId); importOutline(childOutline, note.noteId);
} }
return note; return note;
@ -65,7 +65,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
let returnNote = null; let returnNote = null;
for (const outline of outlines) { for (const outline of outlines) {
const note = await importOutline(outline, parentNote.noteId); const note = importOutline(outline, parentNote.noteId);
// first created note will be activated after import // first created note will be activated after import
returnNote = returnNote || note; returnNote = returnNote || note;

View File

@ -7,42 +7,42 @@ const commonmark = require('commonmark');
const mimeService = require('./mime'); const mimeService = require('./mime');
const utils = require('../../services/utils'); const utils = require('../../services/utils');
async function importSingleFile(taskContext, file, parentNote) { function importSingleFile(taskContext, file, parentNote) {
const mime = mimeService.getMime(file.originalname) || file.mimetype; const mime = mimeService.getMime(file.originalname) || file.mimetype;
if (taskContext.data.textImportedAsText) { if (taskContext.data.textImportedAsText) {
if (mime === 'text/html') { if (mime === 'text/html') {
return await importHtml(taskContext, file, parentNote); return importHtml(taskContext, file, parentNote);
} else if (['text/markdown', 'text/x-markdown'].includes(mime)) { } else if (['text/markdown', 'text/x-markdown'].includes(mime)) {
return await importMarkdown(taskContext, file, parentNote); return importMarkdown(taskContext, file, parentNote);
} else if (mime === 'text/plain') { } else if (mime === 'text/plain') {
return await importPlainText(taskContext, file, parentNote); return importPlainText(taskContext, file, parentNote);
} }
} }
if (taskContext.data.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === 'code') { if (taskContext.data.codeImportedAsCode && mimeService.getType(taskContext.data, mime) === 'code') {
return await importCodeNote(taskContext, file, parentNote); return importCodeNote(taskContext, file, parentNote);
} }
if (["image/jpeg", "image/gif", "image/png", "image/webp"].includes(mime)) { if (["image/jpeg", "image/gif", "image/png", "image/webp"].includes(mime)) {
return await importImage(file, parentNote, taskContext); return importImage(file, parentNote, taskContext);
} }
return await importFile(taskContext, file, parentNote); return importFile(taskContext, file, parentNote);
} }
async function importImage(file, parentNote, taskContext) { function importImage(file, parentNote, taskContext) {
const {note} = await imageService.saveImage(parentNote.noteId, file.buffer, file.originalname, taskContext.data.shrinkImages); const {note} = imageService.saveImage(parentNote.noteId, file.buffer, file.originalname, taskContext.data.shrinkImages);
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
return note; return note;
} }
async function importFile(taskContext, file, parentNote) { function importFile(taskContext, file, parentNote) {
const originalName = file.originalname; const originalName = file.originalname;
const {note} = await noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title: originalName, title: originalName,
content: file.buffer, content: file.buffer,
@ -51,20 +51,20 @@ async function importFile(taskContext, file, parentNote) {
mime: mimeService.getMime(originalName) || file.mimetype mime: mimeService.getMime(originalName) || file.mimetype
}); });
await note.addLabel("originalFileName", originalName); note.addLabel("originalFileName", originalName);
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
return note; return note;
} }
async function importCodeNote(taskContext, file, parentNote) { function importCodeNote(taskContext, file, parentNote) {
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces); const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
const content = file.buffer.toString("UTF-8"); const content = file.buffer.toString("UTF-8");
const detectedMime = mimeService.getMime(file.originalname) || file.mimetype; const detectedMime = mimeService.getMime(file.originalname) || file.mimetype;
const mime = mimeService.normalizeMimeType(detectedMime); const mime = mimeService.normalizeMimeType(detectedMime);
const {note} = await noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title, title,
content, content,
@ -78,12 +78,12 @@ async function importCodeNote(taskContext, file, parentNote) {
return note; return note;
} }
async function importPlainText(taskContext, file, parentNote) { function importPlainText(taskContext, file, parentNote) {
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces); const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
const plainTextContent = file.buffer.toString("UTF-8"); const plainTextContent = file.buffer.toString("UTF-8");
const htmlContent = convertTextToHtml(plainTextContent); const htmlContent = convertTextToHtml(plainTextContent);
const {note} = await noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title, title,
content: htmlContent, content: htmlContent,
@ -115,7 +115,7 @@ function convertTextToHtml(text) {
return text; return text;
} }
async function importMarkdown(taskContext, file, parentNote) { function importMarkdown(taskContext, file, parentNote) {
const markdownContent = file.buffer.toString("UTF-8"); const markdownContent = file.buffer.toString("UTF-8");
const reader = new commonmark.Parser(); const reader = new commonmark.Parser();
@ -126,7 +126,7 @@ async function importMarkdown(taskContext, file, parentNote) {
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces); const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
const {note} = await noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title, title,
content: htmlContent, content: htmlContent,
@ -140,11 +140,11 @@ async function importMarkdown(taskContext, file, parentNote) {
return note; return note;
} }
async function importHtml(taskContext, file, parentNote) { function importHtml(taskContext, file, parentNote) {
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces); const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
const content = file.buffer.toString("UTF-8"); const content = file.buffer.toString("UTF-8");
const {note} = await noteService.createNewNote({ const {note} = noteService.createNewNote({
parentNoteId: parentNote.noteId, parentNoteId: parentNote.noteId,
title, title,
content, content,

View File

@ -22,7 +22,7 @@ const treeService = require("../tree");
* @param {Note} importRootNote * @param {Note} importRootNote
* @return {Promise<*>} * @return {Promise<*>}
*/ */
async function importTar(taskContext, fileBuffer, importRootNote) { function importTar(taskContext, fileBuffer, importRootNote) {
// maps from original noteId (in tar file) to newly generated noteId // maps from original noteId (in tar file) to newly generated noteId
const noteIdMap = {}; const noteIdMap = {};
const attributes = []; const attributes = [];
@ -77,7 +77,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
}; };
} }
async function getParentNoteId(filePath, parentNoteMeta) { function getParentNoteId(filePath, parentNoteMeta) {
let parentNoteId; let parentNoteId;
if (parentNoteMeta) { if (parentNoteMeta) {
@ -95,7 +95,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
else { else {
// tar allows creating out of order records - i.e. file in a directory can appear in the tar stream before actual directory // tar allows creating out of order records - i.e. file in a directory can appear in the tar stream before actual directory
// (out-of-order-directory-records.tar in test set) // (out-of-order-directory-records.tar in test set)
parentNoteId = await saveDirectory(parentPath); parentNoteId = saveDirectory(parentPath);
} }
} }
@ -123,7 +123,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return { mime, type }; return { mime, type };
} }
async function saveAttributes(note, noteMeta) { function saveAttributes(note, noteMeta) {
if (!noteMeta) { if (!noteMeta) {
return; return;
} }
@ -153,20 +153,20 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
} }
} }
async function saveDirectory(filePath) { function saveDirectory(filePath) {
const { parentNoteMeta, noteMeta } = getMeta(filePath); const { parentNoteMeta, noteMeta } = getMeta(filePath);
const noteId = getNoteId(noteMeta, filePath); const noteId = getNoteId(noteMeta, filePath);
const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta); const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta);
const parentNoteId = await getParentNoteId(filePath, parentNoteMeta); const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
let note = await repository.getNote(noteId); let note = repository.getNote(noteId);
if (note) { if (note) {
return; return;
} }
({note} = await noteService.createNewNote({ ({note} = noteService.createNewNote({
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
title: noteTitle, title: noteTitle,
content: '', content: '',
@ -178,7 +178,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
})); }));
await saveAttributes(note, noteMeta); saveAttributes(note, noteMeta);
if (!firstNote) { if (!firstNote) {
firstNote = note; firstNote = note;
@ -211,7 +211,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
return targetNoteId; return targetNoteId;
} }
async function saveNote(filePath, content) { function saveNote(filePath, content) {
const {parentNoteMeta, noteMeta} = getMeta(filePath); const {parentNoteMeta, noteMeta} = getMeta(filePath);
if (noteMeta && noteMeta.noImport) { if (noteMeta && noteMeta.noImport) {
@ -219,10 +219,10 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
} }
const noteId = getNoteId(noteMeta, filePath); const noteId = getNoteId(noteMeta, filePath);
const parentNoteId = await getParentNoteId(filePath, parentNoteMeta); const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
if (noteMeta && noteMeta.isClone) { if (noteMeta && noteMeta.isClone) {
await new Branch({ new Branch({
noteId, noteId,
parentNoteId, parentNoteId,
isExpanded: noteMeta.isExpanded, isExpanded: noteMeta.isExpanded,
@ -300,13 +300,13 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
} }
} }
let note = await repository.getNote(noteId); let note = repository.getNote(noteId);
if (note) { if (note) {
await note.setContent(content); note.setContent(content);
} }
else { else {
({note} = await noteService.createNewNote({ ({note} = noteService.createNewNote({
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
title: noteTitle, title: noteTitle,
content: content, content: content,
@ -319,7 +319,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(), isProtected: importRootNote.isProtected && protectedSessionService.isProtectedSessionAvailable(),
})); }));
await saveAttributes(note, noteMeta); saveAttributes(note, noteMeta);
if (!firstNote) { if (!firstNote) {
firstNote = note; firstNote = note;
@ -366,7 +366,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
// stream is the content body (might be an empty stream) // stream is the content body (might be an empty stream)
// call next when you are done with this entry // call next when you are done with this entry
stream.on('end', async function() { stream.on('end', function() {
const filePath = normalizeFilePath(header.name); const filePath = normalizeFilePath(header.name);
const content = Buffer.concat(chunks); const content = Buffer.concat(chunks);
@ -375,10 +375,10 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
metaFile = JSON.parse(content.toString("UTF-8")); metaFile = JSON.parse(content.toString("UTF-8"));
} }
else if (header.type === 'directory') { else if (header.type === 'directory') {
await saveDirectory(filePath); saveDirectory(filePath);
} }
else if (header.type === 'file') { else if (header.type === 'file') {
await saveNote(filePath, content); saveNote(filePath, content);
} }
else { else {
log.info("Ignoring tar import entry with type " + header.type); log.info("Ignoring tar import entry with type " + header.type);
@ -393,7 +393,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
}); });
return new Promise(resolve => { return new Promise(resolve => {
extract.on('finish', async function() { extract.on('finish', function() {
const createdNoteIds = {}; const createdNoteIds = {};
for (const path in createdPaths) { for (const path in createdPaths) {
@ -403,12 +403,12 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
} }
for (const noteId in createdNoteIds) { // now the noteIds are unique for (const noteId in createdNoteIds) { // now the noteIds are unique
await noteService.scanForLinks(await repository.getNote(noteId)); noteService.scanForLinks(repository.getNote(noteId));
if (!metaFile) { if (!metaFile) {
// if there's no meta file then the notes are created based on the order in that tar file but that // if there's no meta file then the notes are created based on the order in that tar file but that
// is usually quite random so we sort the notes in the way they would appear in the file manager // is usually quite random so we sort the notes in the way they would appear in the file manager
await treeService.sortNotesAlphabetically(noteId, true); treeService.sortNotesAlphabetically(noteId, true);
} }
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
@ -418,7 +418,7 @@ async function importTar(taskContext, fileBuffer, importRootNote) {
// are already in the database (we don't want to have "broken" relations, not even transitionally) // are already in the database (we don't want to have "broken" relations, not even transitionally)
for (const attr of attributes) { for (const attr of attributes) {
if (attr.type !== 'relation' || attr.value in createdNoteIds) { if (attr.type !== 'relation' || attr.value in createdNoteIds) {
await new Attribute(attr).save(); new Attribute(attr).save();
} }
else { else {
log.info("Relation not imported since target note doesn't exist: " + JSON.stringify(attr)); log.info("Relation not imported since target note doesn't exist: " + JSON.stringify(attr));

View File

@ -21,7 +21,7 @@ const yauzl = require("yauzl");
* @param {Note} importRootNote * @param {Note} importRootNote
* @return {Promise<*>} * @return {Promise<*>}
*/ */
async function importZip(taskContext, fileBuffer, importRootNote) { function importZip(taskContext, fileBuffer, importRootNote) {
// maps from original noteId (in tar file) to newly generated noteId // maps from original noteId (in tar file) to newly generated noteId
const noteIdMap = {}; const noteIdMap = {};
const attributes = []; const attributes = [];
@ -75,7 +75,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}; };
} }
async function getParentNoteId(filePath, parentNoteMeta) { function getParentNoteId(filePath, parentNoteMeta) {
let parentNoteId; let parentNoteId;
if (parentNoteMeta) { if (parentNoteMeta) {
@ -93,7 +93,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
else { else {
// tar allows creating out of order records - i.e. file in a directory can appear in the tar stream before actual directory // tar allows creating out of order records - i.e. file in a directory can appear in the tar stream before actual directory
// (out-of-order-directory-records.tar in test set) // (out-of-order-directory-records.tar in test set)
parentNoteId = await saveDirectory(parentPath); parentNoteId = saveDirectory(parentPath);
} }
} }
@ -125,7 +125,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
return { mime, type }; return { mime, type };
} }
async function saveAttributes(note, noteMeta) { function saveAttributes(note, noteMeta) {
if (!noteMeta) { if (!noteMeta) {
return; return;
} }
@ -155,20 +155,20 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
} }
} }
async function saveDirectory(filePath) { function saveDirectory(filePath) {
const { parentNoteMeta, noteMeta } = getMeta(filePath); const { parentNoteMeta, noteMeta } = getMeta(filePath);
const noteId = getNoteId(noteMeta, filePath); const noteId = getNoteId(noteMeta, filePath);
const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta); const noteTitle = utils.getNoteTitle(filePath, taskContext.data.replaceUnderscoresWithSpaces, noteMeta);
const parentNoteId = await getParentNoteId(filePath, parentNoteMeta); const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
let note = await repository.getNote(noteId); let note = repository.getNote(noteId);
if (note) { if (note) {
return; return;
} }
({note} = await noteService.createNewNote({ ({note} = noteService.createNewNote({
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
title: noteTitle, title: noteTitle,
content: '', content: '',
@ -182,7 +182,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
createdNoteIds[note.noteId] = true; createdNoteIds[note.noteId] = true;
await saveAttributes(note, noteMeta); saveAttributes(note, noteMeta);
if (!firstNote) { if (!firstNote) {
firstNote = note; firstNote = note;
@ -215,7 +215,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
return targetNoteId; return targetNoteId;
} }
async function saveNote(filePath, content) { function saveNote(filePath, content) {
const {parentNoteMeta, noteMeta} = getMeta(filePath); const {parentNoteMeta, noteMeta} = getMeta(filePath);
if (noteMeta && noteMeta.noImport) { if (noteMeta && noteMeta.noImport) {
@ -223,14 +223,14 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
} }
const noteId = getNoteId(noteMeta, filePath); const noteId = getNoteId(noteMeta, filePath);
const parentNoteId = await getParentNoteId(filePath, parentNoteMeta); const parentNoteId = getParentNoteId(filePath, parentNoteMeta);
if (!parentNoteId) { if (!parentNoteId) {
throw new Error(`Cannot find parentNoteId for ${filePath}`); throw new Error(`Cannot find parentNoteId for ${filePath}`);
} }
if (noteMeta && noteMeta.isClone) { if (noteMeta && noteMeta.isClone) {
await new Branch({ new Branch({
noteId, noteId,
parentNoteId, parentNoteId,
isExpanded: noteMeta.isExpanded, isExpanded: noteMeta.isExpanded,
@ -318,13 +318,13 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
} }
} }
let note = await repository.getNote(noteId); let note = repository.getNote(noteId);
if (note) { if (note) {
await note.setContent(content); note.setContent(content);
} }
else { else {
({note} = await noteService.createNewNote({ ({note} = noteService.createNewNote({
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
title: noteTitle, title: noteTitle,
content: content, content: content,
@ -339,7 +339,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
createdNoteIds[note.noteId] = true; createdNoteIds[note.noteId] = true;
await saveAttributes(note, noteMeta); saveAttributes(note, noteMeta);
if (!firstNote) { if (!firstNote) {
firstNote = note; firstNote = note;
@ -405,11 +405,11 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
// we're running two passes to make sure that the meta file is loaded before the rest of the files is processed. // we're running two passes to make sure that the meta file is loaded before the rest of the files is processed.
await readZipFile(fileBuffer, async (zipfile, entry) => { readZipFile(fileBuffer, (zipfile, entry) => {
const filePath = normalizeFilePath(entry.fileName); const filePath = normalizeFilePath(entry.fileName);
if (filePath === '!!!meta.json') { if (filePath === '!!!meta.json') {
const content = await readContent(zipfile, entry); const content = readContent(zipfile, entry);
metaFile = JSON.parse(content.toString("UTF-8")); metaFile = JSON.parse(content.toString("UTF-8"));
} }
@ -417,16 +417,16 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
zipfile.readEntry(); zipfile.readEntry();
}); });
await readZipFile(fileBuffer, async (zipfile, entry) => { readZipFile(fileBuffer, (zipfile, entry) => {
const filePath = normalizeFilePath(entry.fileName); const filePath = normalizeFilePath(entry.fileName);
if (/\/$/.test(entry.fileName)) { if (/\/$/.test(entry.fileName)) {
await saveDirectory(filePath); saveDirectory(filePath);
} }
else if (filePath !== '!!!meta.json') { else if (filePath !== '!!!meta.json') {
const content = await readContent(zipfile, entry); const content = readContent(zipfile, entry);
await saveNote(filePath, content); saveNote(filePath, content);
} }
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
@ -434,12 +434,12 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
}); });
for (const noteId in createdNoteIds) { // now the noteIds are unique for (const noteId in createdNoteIds) { // now the noteIds are unique
await noteService.scanForLinks(await repository.getNote(noteId)); noteService.scanForLinks(repository.getNote(noteId));
if (!metaFile) { if (!metaFile) {
// if there's no meta file then the notes are created based on the order in that tar file but that // if there's no meta file then the notes are created based on the order in that tar file but that
// is usually quite random so we sort the notes in the way they would appear in the file manager // is usually quite random so we sort the notes in the way they would appear in the file manager
await treeService.sortNotesAlphabetically(noteId, true); treeService.sortNotesAlphabetically(noteId, true);
} }
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
@ -449,7 +449,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
// are already in the database (we don't want to have "broken" relations, not even transitionally) // are already in the database (we don't want to have "broken" relations, not even transitionally)
for (const attr of attributes) { for (const attr of attributes) {
if (attr.type !== 'relation' || attr.value in createdNoteIds) { if (attr.type !== 'relation' || attr.value in createdNoteIds) {
await new Attribute(attr).save(); new Attribute(attr).save();
} }
else { else {
log.info("Relation not imported since target note doesn't exist: " + JSON.stringify(attr)); log.info("Relation not imported since target note doesn't exist: " + JSON.stringify(attr));

View File

@ -391,14 +391,14 @@ for (const action of DEFAULT_KEYBOARD_ACTIONS) {
} }
} }
async function getKeyboardActions() { function getKeyboardActions() {
const actions = JSON.parse(JSON.stringify(DEFAULT_KEYBOARD_ACTIONS)); const actions = JSON.parse(JSON.stringify(DEFAULT_KEYBOARD_ACTIONS));
for (const action of actions) { for (const action of actions) {
action.effectiveShortcuts = action.effectiveShortcuts ? action.defaultShortcuts.slice() : []; action.effectiveShortcuts = action.effectiveShortcuts ? action.defaultShortcuts.slice() : [];
} }
for (const option of await optionService.getOptions()) { for (const option of optionService.getOptions()) {
if (option.name.startsWith('keyboardShortcuts')) { if (option.name.startsWith('keyboardShortcuts')) {
let actionName = option.name.substr(17); let actionName = option.name.substr(17);
actionName = actionName.charAt(0).toLowerCase() + actionName.slice(1); actionName = actionName.charAt(0).toLowerCase() + actionName.slice(1);

View File

@ -7,13 +7,13 @@ const log = require('./log');
const utils = require('./utils'); const utils = require('./utils');
const resourceDir = require('./resource_dir'); const resourceDir = require('./resource_dir');
async function migrate() { function migrate() {
const migrations = []; const migrations = [];
// backup before attempting migration // backup before attempting migration
await backupService.backupNow("before-migration"); backupService.backupNow("before-migration");
const currentDbVersion = parseInt(await optionService.getOption('dbVersion')); const currentDbVersion = parseInt(optionService.getOption('dbVersion'));
fs.readdirSync(resourceDir.MIGRATIONS_DIR).forEach(file => { fs.readdirSync(resourceDir.MIGRATIONS_DIR).forEach(file => {
const match = file.match(/([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)/); const match = file.match(/([0-9]{4})__([a-zA-Z0-9_ ]+)\.(sql|js)/);
@ -43,26 +43,26 @@ async function migrate() {
try { try {
log.info("Attempting migration to version " + mig.dbVersion); log.info("Attempting migration to version " + mig.dbVersion);
await sql.transactional(async () => { sql.transactional(() => {
if (mig.type === 'sql') { if (mig.type === 'sql') {
const migrationSql = fs.readFileSync(resourceDir.MIGRATIONS_DIR + "/" + mig.file).toString('utf8'); const migrationSql = fs.readFileSync(resourceDir.MIGRATIONS_DIR + "/" + mig.file).toString('utf8');
console.log("Migration with SQL script: " + migrationSql); console.log("Migration with SQL script: " + migrationSql);
await sql.executeScript(migrationSql); sql.executeScript(migrationSql);
} }
else if (mig.type === 'js') { else if (mig.type === 'js') {
console.log("Migration with JS module"); console.log("Migration with JS module");
const migrationModule = require(resourceDir.MIGRATIONS_DIR + "/" + mig.file); const migrationModule = require(resourceDir.MIGRATIONS_DIR + "/" + mig.file);
await migrationModule(); migrationModule();
} }
else { else {
throw new Error("Unknown migration type " + mig.type); throw new Error("Unknown migration type " + mig.type);
} }
// not using repository because of changed utcDateModified column in migration 129 // not using repository because of changed utcDateModified column in migration 129
await sql.execute(`UPDATE options SET value = ? WHERE name = ?`, [mig.dbVersion, "dbVersion"]); sql.execute(`UPDATE options SET value = ? WHERE name = ?`, [mig.dbVersion, "dbVersion"]);
}); });
log.info("Migration to version " + mig.dbVersion + " has been successful."); log.info("Migration to version " + mig.dbVersion + " has been successful.");
@ -75,8 +75,8 @@ async function migrate() {
} }
} }
if (await sqlInit.isDbUpToDate()) { if (sqlInit.isDbUpToDate()) {
await sqlInit.initDbConnection(); sqlInit.initDbConnection();
} }
} }

View File

@ -3,19 +3,19 @@
const optionService = require('./options'); const optionService = require('./options');
const crypto = require('crypto'); const crypto = require('crypto');
async function getVerificationHash(password) { function getVerificationHash(password) {
const salt = await optionService.getOption('passwordVerificationSalt'); const salt = optionService.getOption('passwordVerificationSalt');
return getScryptHash(password, salt); return getScryptHash(password, salt);
} }
async function getPasswordDerivedKey(password) { function getPasswordDerivedKey(password) {
const salt = await optionService.getOption('passwordDerivedKeySalt'); const salt = optionService.getOption('passwordDerivedKeySalt');
return getScryptHash(password, salt); return getScryptHash(password, salt);
} }
async function getScryptHash(password, salt) { function getScryptHash(password, salt) {
const hashed = crypto.scryptSync(password, salt, 32, const hashed = crypto.scryptSync(password, salt, 32,
{N: 16384, r:8, p:1}); {N: 16384, r:8, p:1});

View File

@ -1,31 +1,28 @@
"use strict"; "use strict";
const sql = require('../sql.js'); const sql = require('../sql.js');
const sqlInit = require('../sql_init.js');
const eventService = require('../events.js'); const eventService = require('../events.js');
const noteCache = require('./note_cache'); const noteCache = require('./note_cache');
const Note = require('./entities/note'); const Note = require('./entities/note');
const Branch = require('./entities/branch'); const Branch = require('./entities/branch');
const Attribute = require('./entities/attribute'); const Attribute = require('./entities/attribute');
async function load() { function load() {
await sqlInit.dbReady;
noteCache.reset(); noteCache.reset();
(await sql.getRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified, contentLength FROM notes WHERE isDeleted = 0`, [])) sql.getRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified, contentLength FROM notes WHERE isDeleted = 0`, [])
.map(row => new Note(noteCache, row)); .map(row => new Note(noteCache, row));
(await sql.getRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`, [])) sql.getRows(`SELECT branchId, noteId, parentNoteId, prefix FROM branches WHERE isDeleted = 0`, [])
.map(row => new Branch(noteCache, row)); .map(row => new Branch(noteCache, row));
(await sql.getRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`, [])).map(row => new Attribute(noteCache, row)); sql.getRows(`SELECT attributeId, noteId, type, name, value, isInheritable FROM attributes WHERE isDeleted = 0`, []).map(row => new Attribute(noteCache, row));
noteCache.loaded = true; noteCache.loaded = true;
noteCache.loadedResolve(); noteCache.loadedResolve();
} }
eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED, eventService.ENTITY_SYNCED], async ({entityName, entity}) => { eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED, eventService.ENTITY_SYNCED], ({entityName, entity}) => {
// note that entity can also be just POJO without methods if coming from sync // note that entity can also be just POJO without methods if coming from sync
if (!noteCache.loaded) { if (!noteCache.loaded) {
@ -150,4 +147,5 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
noteCache.loadedPromise.then(() => noteCache.decryptProtectedNotes()); noteCache.loadedPromise.then(() => noteCache.decryptProtectedNotes());
}); });
load(); // FIXME
// load();

View File

@ -191,7 +191,7 @@ function setImmediatePromise() {
}); });
} }
async function findSimilarNotes(noteId) { function findSimilarNotes(noteId) {
const results = []; const results = [];
let i = 0; let i = 0;
@ -211,7 +211,7 @@ async function findSimilarNotes(noteId) {
i++; i++;
if (i % 200 === 0) { if (i % 200 === 0) {
await setImmediatePromise(); setImmediatePromise();
} }
} }

View File

@ -6,17 +6,17 @@ const dateUtils = require('../services/date_utils');
/** /**
* @param {Note} note * @param {Note} note
*/ */
async function protectNoteRevisions(note) { function protectNoteRevisions(note) {
for (const revision of await note.getRevisions()) { for (const revision of note.getRevisions()) {
if (note.isProtected !== revision.isProtected) { if (note.isProtected !== revision.isProtected) {
const content = await revision.getContent(); const content = revision.getContent();
revision.isProtected = note.isProtected; revision.isProtected = note.isProtected;
// this will force de/encryption // this will force de/encryption
await revision.setContent(content); revision.setContent(content);
await revision.save(); revision.save();
} }
} }
} }
@ -25,12 +25,12 @@ async function protectNoteRevisions(note) {
* @param {Note} note * @param {Note} note
* @return {NoteRevision} * @return {NoteRevision}
*/ */
async function createNoteRevision(note) { function createNoteRevision(note) {
if (await note.hasLabel("disableVersioning")) { if (note.hasLabel("disableVersioning")) {
return; return;
} }
const noteRevision = await new NoteRevision({ const noteRevision = new NoteRevision({
noteId: note.noteId, noteId: note.noteId,
// title and text should be decrypted now // title and text should be decrypted now
title: note.title, title: note.title,
@ -45,7 +45,7 @@ async function createNoteRevision(note) {
dateCreated: dateUtils.localNowDateTime() dateCreated: dateUtils.localNowDateTime()
}).save(); }).save();
await noteRevision.setContent(await note.getContent()); noteRevision.setContent(note.getContent());
return noteRevision; return noteRevision;
} }

View File

@ -20,8 +20,8 @@ const request = require('./request');
const path = require('path'); const path = require('path');
const url = require('url'); const url = require('url');
async function getNewNotePosition(parentNoteId) { function getNewNotePosition(parentNoteId) {
const maxNotePos = await sql.getValue(` const maxNotePos = sql.getValue(`
SELECT MAX(notePosition) SELECT MAX(notePosition)
FROM branches FROM branches
WHERE parentNoteId = ? WHERE parentNoteId = ?
@ -30,12 +30,12 @@ async function getNewNotePosition(parentNoteId) {
return maxNotePos === null ? 0 : maxNotePos + 10; return maxNotePos === null ? 0 : maxNotePos + 10;
} }
async function triggerChildNoteCreated(childNote, parentNote) { function triggerChildNoteCreated(childNote, parentNote) {
await eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote, parentNote }); eventService.emit(eventService.CHILD_NOTE_CREATED, { childNote, parentNote });
} }
async function triggerNoteTitleChanged(note) { function triggerNoteTitleChanged(note) {
await eventService.emit(eventService.NOTE_TITLE_CHANGED, note); eventService.emit(eventService.NOTE_TITLE_CHANGED, note);
} }
function deriveMime(type, mime) { function deriveMime(type, mime) {
@ -60,10 +60,10 @@ function deriveMime(type, mime) {
return mime; return mime;
} }
async function copyChildAttributes(parentNote, childNote) { function copyChildAttributes(parentNote, childNote) {
for (const attr of await parentNote.getOwnedAttributes()) { for (const attr of parentNote.getOwnedAttributes()) {
if (attr.name.startsWith("child:")) { if (attr.name.startsWith("child:")) {
await new Attribute({ new Attribute({
noteId: childNote.noteId, noteId: childNote.noteId,
type: attr.type, type: attr.type,
name: attr.name.substr(6), name: attr.name.substr(6),
@ -94,8 +94,8 @@ async function copyChildAttributes(parentNote, childNote) {
* @param params * @param params
* @return {Promise<{note: Note, branch: Branch}>} * @return {Promise<{note: Note, branch: Branch}>}
*/ */
async function createNewNote(params) { function createNewNote(params) {
const parentNote = await repository.getNote(params.parentNoteId); const parentNote = repository.getNote(params.parentNoteId);
if (!parentNote) { if (!parentNote) {
throw new Error(`Parent note "${params.parentNoteId}" not found.`); throw new Error(`Parent note "${params.parentNoteId}" not found.`);
@ -105,7 +105,7 @@ async function createNewNote(params) {
throw new Error(`Note title must not be empty`); throw new Error(`Note title must not be empty`);
} }
const note = await new Note({ const note = new Note({
noteId: params.noteId, // optionally can force specific noteId noteId: params.noteId, // optionally can force specific noteId
title: params.title, title: params.title,
isProtected: !!params.isProtected, isProtected: !!params.isProtected,
@ -113,22 +113,22 @@ async function createNewNote(params) {
mime: deriveMime(params.type, params.mime) mime: deriveMime(params.type, params.mime)
}).save(); }).save();
await note.setContent(params.content); note.setContent(params.content);
const branch = await new Branch({ const branch = new Branch({
noteId: note.noteId, noteId: note.noteId,
parentNoteId: params.parentNoteId, parentNoteId: params.parentNoteId,
notePosition: params.notePosition !== undefined ? params.notePosition : await getNewNotePosition(params.parentNoteId), notePosition: params.notePosition !== undefined ? params.notePosition : getNewNotePosition(params.parentNoteId),
prefix: params.prefix, prefix: params.prefix,
isExpanded: !!params.isExpanded isExpanded: !!params.isExpanded
}).save(); }).save();
await scanForLinks(note); scanForLinks(note);
await copyChildAttributes(parentNote, note); copyChildAttributes(parentNote, note);
await triggerNoteTitleChanged(note); triggerNoteTitleChanged(note);
await triggerChildNoteCreated(note, parentNote); triggerChildNoteCreated(note, parentNote);
return { return {
note, note,
@ -136,9 +136,9 @@ async function createNewNote(params) {
}; };
} }
async function createNewNoteWithTarget(target, targetBranchId, params) { function createNewNoteWithTarget(target, targetBranchId, params) {
if (!params.type) { if (!params.type) {
const parentNote = await repository.getNote(params.parentNoteId); const parentNote = repository.getNote(params.parentNoteId);
// code note type can be inherited, otherwise text is default // code note type can be inherited, otherwise text is default
params.type = parentNote.type === 'code' ? 'code' : 'text'; params.type = parentNote.type === 'code' ? 'code' : 'text';
@ -146,20 +146,20 @@ async function createNewNoteWithTarget(target, targetBranchId, params) {
} }
if (target === 'into') { if (target === 'into') {
return await createNewNote(params); return createNewNote(params);
} }
else if (target === 'after') { else if (target === 'after') {
const afterNote = await sql.getRow('SELECT notePosition FROM branches WHERE branchId = ?', [targetBranchId]); const afterNote = sql.getRow('SELECT notePosition FROM branches WHERE branchId = ?', [targetBranchId]);
// not updating utcDateModified to avoig having to sync whole rows // not updating utcDateModified to avoig having to sync whole rows
await sql.execute('UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0', sql.execute('UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0',
[params.parentNoteId, afterNote.notePosition]); [params.parentNoteId, afterNote.notePosition]);
params.notePosition = afterNote.notePosition + 10; params.notePosition = afterNote.notePosition + 10;
const retObject = await createNewNote(params); const retObject = createNewNote(params);
await syncTableService.addNoteReorderingSync(params.parentNoteId); syncTableService.addNoteReorderingSync(params.parentNoteId);
return retObject; return retObject;
} }
@ -168,31 +168,31 @@ async function createNewNoteWithTarget(target, targetBranchId, params) {
} }
} }
async function protectNoteRecursively(note, protect, includingSubTree, taskContext) { function protectNoteRecursively(note, protect, includingSubTree, taskContext) {
await protectNote(note, protect); protectNote(note, protect);
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
if (includingSubTree) { if (includingSubTree) {
for (const child of await note.getChildNotes()) { for (const child of note.getChildNotes()) {
await protectNoteRecursively(child, protect, includingSubTree, taskContext); protectNoteRecursively(child, protect, includingSubTree, taskContext);
} }
} }
} }
async function protectNote(note, protect) { function protectNote(note, protect) {
if (protect !== note.isProtected) { if (protect !== note.isProtected) {
const content = await note.getContent(); const content = note.getContent();
note.isProtected = protect; note.isProtected = protect;
// this will force de/encryption // this will force de/encryption
await note.setContent(content); note.setContent(content);
await note.save(); note.save();
} }
await noteRevisionService.protectNoteRevisions(note); noteRevisionService.protectNoteRevisions(note);
} }
function findImageLinks(content, foundLinks) { function findImageLinks(content, foundLinks) {
@ -260,9 +260,9 @@ async function downloadImage(noteId, imageUrl) {
const title = path.basename(parsedUrl.pathname); const title = path.basename(parsedUrl.pathname);
const imageService = require('../services/image'); const imageService = require('../services/image');
const {note} = await imageService.saveImage(noteId, imageBuffer, title, true); const {note} = imageService.saveImage(noteId, imageBuffer, title, true);
await note.addLabel('imageUrl', imageUrl); note.addLabel('imageUrl', imageUrl);
imageUrlToNoteIdMapping[imageUrl] = note.noteId; imageUrlToNoteIdMapping[imageUrl] = note.noteId;
@ -282,7 +282,7 @@ function replaceUrl(content, url, imageNote) {
return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`); return content.replace(new RegExp(`\\s+src=[\"']${quotedUrl}[\"']`, "g"), ` src="api/images/${imageNote.noteId}/${imageNote.title}"`);
} }
async function downloadImages(noteId, content) { function downloadImages(noteId, content) {
const re = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/ig; const re = /<img[^>]*?\ssrc=['"]([^'">]+)['"]/ig;
let match; let match;
@ -296,7 +296,7 @@ async function downloadImages(noteId, content) {
&& (url.length !== 20 || url.toLowerCase().startsWith('http'))) { && (url.length !== 20 || url.toLowerCase().startsWith('http'))) {
if (url in imageUrlToNoteIdMapping) { if (url in imageUrlToNoteIdMapping) {
const imageNote = await repository.getNote(imageUrlToNoteIdMapping[url]); const imageNote = repository.getNote(imageUrlToNoteIdMapping[url]);
if (!imageNote || imageNote.isDeleted) { if (!imageNote || imageNote.isDeleted) {
delete imageUrlToNoteIdMapping[url]; delete imageUrlToNoteIdMapping[url];
@ -308,7 +308,7 @@ async function downloadImages(noteId, content) {
} }
} }
const existingImage = (await attributeService.getNotesWithLabel('imageUrl', url)) const existingImage = (attributeService.getNotesWithLabel('imageUrl', url))
.find(note => note.type === 'image'); .find(note => note.type === 'image');
if (existingImage) { if (existingImage) {
@ -331,7 +331,7 @@ async function downloadImages(noteId, content) {
} }
Promise.all(Object.values(downloadImagePromises)).then(() => { Promise.all(Object.values(downloadImagePromises)).then(() => {
setTimeout(async () => { setTimeout(() => {
// the normal expected flow of the offline image saving is that users will paste the image(s) // the normal expected flow of the offline image saving is that users will paste the image(s)
// which will get asynchronously downloaded, during that time they keep editing the note // which will get asynchronously downloaded, during that time they keep editing the note
// once the download is finished, the image note representing downloaded image will be used // once the download is finished, the image note representing downloaded image will be used
@ -340,11 +340,11 @@ async function downloadImages(noteId, content) {
// are downloaded and the IMG references are not updated. For this occassion we have this code // are downloaded and the IMG references are not updated. For this occassion we have this code
// which upon the download of all the images will update the note if the links have not been fixed before // which upon the download of all the images will update the note if the links have not been fixed before
await sql.transactional(async () => { sql.transactional(() => {
const imageNotes = await repository.getNotes(Object.values(imageUrlToNoteIdMapping)); const imageNotes = repository.getNotes(Object.values(imageUrlToNoteIdMapping));
const origNote = await repository.getNote(noteId); const origNote = repository.getNote(noteId);
const origContent = await origNote.getContent(); const origContent = origNote.getContent();
let updatedContent = origContent; let updatedContent = origContent;
for (const url in imageUrlToNoteIdMapping) { for (const url in imageUrlToNoteIdMapping) {
@ -357,9 +357,9 @@ async function downloadImages(noteId, content) {
// update only if the links have not been already fixed. // update only if the links have not been already fixed.
if (updatedContent !== origContent) { if (updatedContent !== origContent) {
await origNote.setContent(updatedContent); origNote.setContent(updatedContent);
await scanForLinks(origNote); scanForLinks(origNote);
console.log(`Fixed the image links for note ${noteId} to the offline saved.`); console.log(`Fixed the image links for note ${noteId} to the offline saved.`);
} }
@ -370,7 +370,7 @@ async function downloadImages(noteId, content) {
return content; return content;
} }
async function saveLinks(note, content) { function saveLinks(note, content) {
if (note.type !== 'text' && note.type !== 'relation-map') { if (note.type !== 'text' && note.type !== 'relation-map') {
return content; return content;
} }
@ -382,7 +382,7 @@ async function saveLinks(note, content) {
const foundLinks = []; const foundLinks = [];
if (note.type === 'text') { if (note.type === 'text') {
content = await downloadImages(note.noteId, content); content = downloadImages(note.noteId, content);
content = findImageLinks(content, foundLinks); content = findImageLinks(content, foundLinks);
content = findInternalLinks(content, foundLinks); content = findInternalLinks(content, foundLinks);
@ -395,10 +395,10 @@ async function saveLinks(note, content) {
throw new Error("Unrecognized type " + note.type); throw new Error("Unrecognized type " + note.type);
} }
const existingLinks = await note.getLinks(); const existingLinks = note.getLinks();
for (const foundLink of foundLinks) { for (const foundLink of foundLinks) {
const targetNote = await repository.getNote(foundLink.value); const targetNote = repository.getNote(foundLink.value);
if (!targetNote || targetNote.isDeleted) { if (!targetNote || targetNote.isDeleted) {
continue; continue;
} }
@ -408,7 +408,7 @@ async function saveLinks(note, content) {
&& existingLink.name === foundLink.name); && existingLink.name === foundLink.name);
if (!existingLink) { if (!existingLink) {
const newLink = await new Attribute({ const newLink = new Attribute({
noteId: note.noteId, noteId: note.noteId,
type: 'relation', type: 'relation',
name: foundLink.name, name: foundLink.name,
@ -419,7 +419,7 @@ async function saveLinks(note, content) {
} }
else if (existingLink.isDeleted) { else if (existingLink.isDeleted) {
existingLink.isDeleted = false; existingLink.isDeleted = false;
await existingLink.save(); existingLink.save();
} }
// else the link exists so we don't need to do anything // else the link exists so we don't need to do anything
} }
@ -431,30 +431,30 @@ async function saveLinks(note, content) {
for (const unusedLink of unusedLinks) { for (const unusedLink of unusedLinks) {
unusedLink.isDeleted = true; unusedLink.isDeleted = true;
await unusedLink.save(); unusedLink.save();
} }
return content; return content;
} }
async function saveNoteRevision(note) { function saveNoteRevision(note) {
// files and images are versioned separately // files and images are versioned separately
if (note.type === 'file' || note.type === 'image' || await note.hasLabel('disableVersioning')) { if (note.type === 'file' || note.type === 'image' || note.hasLabel('disableVersioning')) {
return; return;
} }
const now = new Date(); const now = new Date();
const noteRevisionSnapshotTimeInterval = parseInt(await optionService.getOption('noteRevisionSnapshotTimeInterval')); const noteRevisionSnapshotTimeInterval = parseInt(optionService.getOption('noteRevisionSnapshotTimeInterval'));
const revisionCutoff = dateUtils.utcDateStr(new Date(now.getTime() - noteRevisionSnapshotTimeInterval * 1000)); const revisionCutoff = dateUtils.utcDateStr(new Date(now.getTime() - noteRevisionSnapshotTimeInterval * 1000));
const existingNoteRevisionId = await sql.getValue( const existingNoteRevisionId = sql.getValue(
"SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND utcDateCreated >= ?", [note.noteId, revisionCutoff]); "SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND utcDateCreated >= ?", [note.noteId, revisionCutoff]);
const msSinceDateCreated = now.getTime() - dateUtils.parseDateTime(note.utcDateCreated).getTime(); const msSinceDateCreated = now.getTime() - dateUtils.parseDateTime(note.utcDateCreated).getTime();
if (!existingNoteRevisionId && msSinceDateCreated >= noteRevisionSnapshotTimeInterval * 1000) { if (!existingNoteRevisionId && msSinceDateCreated >= noteRevisionSnapshotTimeInterval * 1000) {
const noteRevision = await new NoteRevision({ const noteRevision = new NoteRevision({
noteId: note.noteId, noteId: note.noteId,
// title and text should be decrypted now // title and text should be decrypted now
title: note.title, title: note.title,
@ -469,41 +469,41 @@ async function saveNoteRevision(note) {
dateCreated: dateUtils.localNowDateTime() dateCreated: dateUtils.localNowDateTime()
}).save(); }).save();
await noteRevision.setContent(await note.getContent()); noteRevision.setContent(note.getContent());
} }
} }
async function updateNote(noteId, noteUpdates) { function updateNote(noteId, noteUpdates) {
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (!note.isContentAvailable) { if (!note.isContentAvailable) {
throw new Error(`Note ${noteId} is not available for change!`); throw new Error(`Note ${noteId} is not available for change!`);
} }
await saveNoteRevision(note); saveNoteRevision(note);
// if protected status changed, then we need to encrypt/decrypt the content anyway // if protected status changed, then we need to encrypt/decrypt the content anyway
if (['file', 'image'].includes(note.type) && note.isProtected !== noteUpdates.isProtected) { if (['file', 'image'].includes(note.type) && note.isProtected !== noteUpdates.isProtected) {
noteUpdates.content = await note.getContent(); noteUpdates.content = note.getContent();
} }
const noteTitleChanged = note.title !== noteUpdates.title; const noteTitleChanged = note.title !== noteUpdates.title;
note.title = noteUpdates.title; note.title = noteUpdates.title;
note.isProtected = noteUpdates.isProtected; note.isProtected = noteUpdates.isProtected;
await note.save(); note.save();
if (noteUpdates.content !== undefined && noteUpdates.content !== null) { if (noteUpdates.content !== undefined && noteUpdates.content !== null) {
noteUpdates.content = await saveLinks(note, noteUpdates.content); noteUpdates.content = saveLinks(note, noteUpdates.content);
await note.setContent(noteUpdates.content); note.setContent(noteUpdates.content);
} }
if (noteTitleChanged) { if (noteTitleChanged) {
await triggerNoteTitleChanged(note); triggerNoteTitleChanged(note);
} }
await noteRevisionService.protectNoteRevisions(note); noteRevisionService.protectNoteRevisions(note);
return { return {
dateModified: note.dateModified, dateModified: note.dateModified,
@ -518,7 +518,7 @@ async function updateNote(noteId, noteUpdates) {
* *
* @return {boolean} - true if note has been deleted, false otherwise * @return {boolean} - true if note has been deleted, false otherwise
*/ */
async function deleteBranch(branch, deleteId, taskContext) { function deleteBranch(branch, deleteId, taskContext) {
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
if (!branch || branch.isDeleted) { if (!branch || branch.isDeleted) {
@ -527,41 +527,41 @@ async function deleteBranch(branch, deleteId, taskContext) {
if (branch.branchId === 'root' if (branch.branchId === 'root'
|| branch.noteId === 'root' || branch.noteId === 'root'
|| branch.noteId === await hoistedNoteService.getHoistedNoteId()) { || branch.noteId === hoistedNoteService.getHoistedNoteId()) {
throw new Error("Can't delete root branch/note"); throw new Error("Can't delete root branch/note");
} }
branch.isDeleted = true; branch.isDeleted = true;
branch.deleteId = deleteId; branch.deleteId = deleteId;
await branch.save(); branch.save();
const note = await branch.getNote(); const note = branch.getNote();
const notDeletedBranches = await note.getBranches(); const notDeletedBranches = note.getBranches();
if (notDeletedBranches.length === 0) { if (notDeletedBranches.length === 0) {
for (const childBranch of await note.getChildBranches()) { for (const childBranch of note.getChildBranches()) {
await deleteBranch(childBranch, deleteId, taskContext); deleteBranch(childBranch, deleteId, taskContext);
} }
// first delete children and then parent - this will show up better in recent changes // first delete children and then parent - this will show up better in recent changes
note.isDeleted = true; note.isDeleted = true;
note.deleteId = deleteId; note.deleteId = deleteId;
await note.save(); note.save();
log.info("Deleting note " + note.noteId); log.info("Deleting note " + note.noteId);
for (const attribute of await note.getOwnedAttributes()) { for (const attribute of note.getOwnedAttributes()) {
attribute.isDeleted = true; attribute.isDeleted = true;
attribute.deleteId = deleteId; attribute.deleteId = deleteId;
await attribute.save(); attribute.save();
} }
for (const relation of await note.getTargetRelations()) { for (const relation of note.getTargetRelations()) {
relation.isDeleted = true; relation.isDeleted = true;
relation.deleteId = deleteId; relation.deleteId = deleteId;
await relation.save(); relation.save();
} }
return true; return true;
@ -576,8 +576,8 @@ async function deleteBranch(branch, deleteId, taskContext) {
* @param {string} deleteId * @param {string} deleteId
* @param {TaskContext} taskContext * @param {TaskContext} taskContext
*/ */
async function undeleteNote(note, deleteId, taskContext) { function undeleteNote(note, deleteId, taskContext) {
const undeletedParentBranches = await getUndeletedParentBranches(note.noteId, deleteId); const undeletedParentBranches = getUndeletedParentBranches(note.noteId, deleteId);
if (undeletedParentBranches.length === 0) { if (undeletedParentBranches.length === 0) {
// cannot undelete if there's no undeleted parent // cannot undelete if there's no undeleted parent
@ -585,7 +585,7 @@ async function undeleteNote(note, deleteId, taskContext) {
} }
for (const parentBranch of undeletedParentBranches) { for (const parentBranch of undeletedParentBranches) {
await undeleteBranch(parentBranch, deleteId, taskContext); undeleteBranch(parentBranch, deleteId, taskContext);
} }
} }
@ -594,27 +594,27 @@ async function undeleteNote(note, deleteId, taskContext) {
* @param {string} deleteId * @param {string} deleteId
* @param {TaskContext} taskContext * @param {TaskContext} taskContext
*/ */
async function undeleteBranch(branch, deleteId, taskContext) { function undeleteBranch(branch, deleteId, taskContext) {
if (!branch.isDeleted) { if (!branch.isDeleted) {
return; return;
} }
const note = await branch.getNote(); const note = branch.getNote();
if (note.isDeleted && note.deleteId !== deleteId) { if (note.isDeleted && note.deleteId !== deleteId) {
return; return;
} }
branch.isDeleted = false; branch.isDeleted = false;
await branch.save(); branch.save();
taskContext.increaseProgressCount(); taskContext.increaseProgressCount();
if (note.isDeleted && note.deleteId === deleteId) { if (note.isDeleted && note.deleteId === deleteId) {
note.isDeleted = false; note.isDeleted = false;
await note.save(); note.save();
const attrs = await repository.getEntities(` const attrs = repository.getEntities(`
SELECT * FROM attributes SELECT * FROM attributes
WHERE isDeleted = 1 WHERE isDeleted = 1
AND deleteId = ? AND deleteId = ?
@ -623,10 +623,10 @@ async function undeleteBranch(branch, deleteId, taskContext) {
for (const attr of attrs) { for (const attr of attrs) {
attr.isDeleted = false; attr.isDeleted = false;
await attr.save(); attr.save();
} }
const childBranches = await repository.getEntities(` const childBranches = repository.getEntities(`
SELECT branches.* SELECT branches.*
FROM branches FROM branches
WHERE branches.isDeleted = 1 WHERE branches.isDeleted = 1
@ -634,7 +634,7 @@ async function undeleteBranch(branch, deleteId, taskContext) {
AND branches.parentNoteId = ?`, [deleteId, note.noteId]); AND branches.parentNoteId = ?`, [deleteId, note.noteId]);
for (const childBranch of childBranches) { for (const childBranch of childBranches) {
await undeleteBranch(childBranch, deleteId, taskContext); undeleteBranch(childBranch, deleteId, taskContext);
} }
} }
} }
@ -642,8 +642,8 @@ async function undeleteBranch(branch, deleteId, taskContext) {
/** /**
* @return return deleted branches of an undeleted parent note * @return return deleted branches of an undeleted parent note
*/ */
async function getUndeletedParentBranches(noteId, deleteId) { function getUndeletedParentBranches(noteId, deleteId) {
return await repository.getEntities(` return repository.getEntities(`
SELECT branches.* SELECT branches.*
FROM branches FROM branches
JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId
@ -653,17 +653,17 @@ async function getUndeletedParentBranches(noteId, deleteId) {
AND parentNote.isDeleted = 0`, [noteId, deleteId]); AND parentNote.isDeleted = 0`, [noteId, deleteId]);
} }
async function scanForLinks(note) { function scanForLinks(note) {
if (!note || !['text', 'relation-map'].includes(note.type)) { if (!note || !['text', 'relation-map'].includes(note.type)) {
return; return;
} }
try { try {
const content = await note.getContent(); const content = note.getContent();
const newContent = await saveLinks(note, content); const newContent = saveLinks(note, content);
if (content !== newContent) { if (content !== newContent) {
await note.setContent(newContent); note.setContent(newContent);
} }
} }
catch (e) { catch (e) {
@ -671,12 +671,12 @@ async function scanForLinks(note) {
} }
} }
async function eraseDeletedNotes() { function eraseDeletedNotes() {
const eraseNotesAfterTimeInSeconds = await optionService.getOptionInt('eraseNotesAfterTimeInSeconds'); const eraseNotesAfterTimeInSeconds = optionService.getOptionInt('eraseNotesAfterTimeInSeconds');
const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000); const cutoffDate = new Date(Date.now() - eraseNotesAfterTimeInSeconds * 1000);
const noteIdsToErase = await sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1 AND isErased = 0 AND notes.utcDateModified <= ?", [dateUtils.utcDateStr(cutoffDate)]); const noteIdsToErase = sql.getColumn("SELECT noteId FROM notes WHERE isDeleted = 1 AND isErased = 0 AND notes.utcDateModified <= ?", [dateUtils.utcDateStr(cutoffDate)]);
if (noteIdsToErase.length === 0) { if (noteIdsToErase.length === 0) {
return; return;
@ -688,7 +688,7 @@ async function eraseDeletedNotes() {
// - we don't want change the hash since this erasing happens on each instance separately // - we don't want change the hash since this erasing happens on each instance separately
// and changing the hash would fire up the sync errors temporarily // and changing the hash would fire up the sync errors temporarily
await sql.executeMany(` sql.executeMany(`
UPDATE notes UPDATE notes
SET title = '[deleted]', SET title = '[deleted]',
contentLength = 0, contentLength = 0,
@ -696,26 +696,26 @@ async function eraseDeletedNotes() {
isErased = 1 isErased = 1
WHERE noteId IN (???)`, noteIdsToErase); WHERE noteId IN (???)`, noteIdsToErase);
await sql.executeMany(` sql.executeMany(`
UPDATE note_contents UPDATE note_contents
SET content = NULL SET content = NULL
WHERE noteId IN (???)`, noteIdsToErase); WHERE noteId IN (???)`, noteIdsToErase);
// deleting first contents since the WHERE relies on isErased = 0 // deleting first contents since the WHERE relies on isErased = 0
await sql.executeMany(` sql.executeMany(`
UPDATE note_revision_contents UPDATE note_revision_contents
SET content = NULL SET content = NULL
WHERE noteRevisionId IN WHERE noteRevisionId IN
(SELECT noteRevisionId FROM note_revisions WHERE isErased = 0 AND noteId IN (???))`, noteIdsToErase); (SELECT noteRevisionId FROM note_revisions WHERE isErased = 0 AND noteId IN (???))`, noteIdsToErase);
await sql.executeMany(` sql.executeMany(`
UPDATE note_revisions UPDATE note_revisions
SET isErased = 1, SET isErased = 1,
title = NULL, title = NULL,
contentLength = 0 contentLength = 0
WHERE isErased = 0 AND noteId IN (???)`, noteIdsToErase); WHERE isErased = 0 AND noteId IN (???)`, noteIdsToErase);
await sql.executeMany(` sql.executeMany(`
UPDATE attributes UPDATE attributes
SET name = 'deleted', SET name = 'deleted',
value = '' value = ''
@ -724,36 +724,36 @@ async function eraseDeletedNotes() {
log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`); log.info(`Erased notes: ${JSON.stringify(noteIdsToErase)}`);
} }
async function duplicateNote(noteId, parentNoteId) { function duplicateNote(noteId, parentNoteId) {
const origNote = await repository.getNote(noteId); const origNote = repository.getNote(noteId);
if (origNote.isProtected && !protectedSessionService.isProtectedSessionAvailable()) { if (origNote.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
throw new Error(`Cannot duplicate note=${origNote.noteId} because it is protected and protected session is not available`); throw new Error(`Cannot duplicate note=${origNote.noteId} because it is protected and protected session is not available`);
} }
// might be null if orig note is not in the target parentNoteId // might be null if orig note is not in the target parentNoteId
const origBranch = (await origNote.getBranches()).find(branch => branch.parentNoteId === parentNoteId); const origBranch = (origNote.getBranches()).find(branch => branch.parentNoteId === parentNoteId);
const newNote = new Note(origNote); const newNote = new Note(origNote);
newNote.noteId = undefined; // force creation of new note newNote.noteId = undefined; // force creation of new note
newNote.title += " (dup)"; newNote.title += " (dup)";
await newNote.save(); newNote.save();
await newNote.setContent(await origNote.getContent()); newNote.setContent(origNote.getContent());
const newBranch = await new Branch({ const newBranch = new Branch({
noteId: newNote.noteId, noteId: newNote.noteId,
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
// here increasing just by 1 to make sure it's directly after original // here increasing just by 1 to make sure it's directly after original
notePosition: origBranch ? origBranch.notePosition + 1 : null notePosition: origBranch ? origBranch.notePosition + 1 : null
}).save(); }).save();
for (const attribute of await origNote.getOwnedAttributes()) { for (const attribute of origNote.getOwnedAttributes()) {
const attr = new Attribute(attribute); const attr = new Attribute(attribute);
attr.attributeId = undefined; // force creation of new attribute attr.attributeId = undefined; // force creation of new attribute
attr.noteId = newNote.noteId; attr.noteId = newNote.noteId;
await attr.save(); attr.save();
} }
return { return {
@ -762,12 +762,10 @@ async function duplicateNote(noteId, parentNoteId) {
}; };
} }
sqlInit.dbReady.then(() => { // first cleanup kickoff 5 minutes after startup
// first cleanup kickoff 5 minutes after startup setTimeout(cls.wrap(eraseDeletedNotes), 5 * 60 * 1000);
setTimeout(cls.wrap(eraseDeletedNotes), 5 * 60 * 1000);
setInterval(cls.wrap(eraseDeletedNotes), 4 * 3600 * 1000); setInterval(cls.wrap(eraseDeletedNotes), 4 * 3600 * 1000);
});
module.exports = { module.exports = {
createNewNote, createNewNote,

View File

@ -1,7 +1,7 @@
const utils = require('./utils'); const utils = require('./utils');
async function getOption(name) { function getOption(name) {
const option = await require('./repository').getOption(name); const option = require('./repository').getOption(name);
if (!option) { if (!option) {
throw new Error(`Option ${name} doesn't exist`); throw new Error(`Option ${name} doesn't exist`);
@ -13,8 +13,8 @@ async function getOption(name) {
/** /**
* @return {Promise<number>} * @return {Promise<number>}
*/ */
async function getOptionInt(name) { function getOptionInt(name) {
const val = await getOption(name); const val = getOption(name);
const intVal = parseInt(val); const intVal = parseInt(val);
@ -28,8 +28,8 @@ async function getOptionInt(name) {
/** /**
* @return {Promise<boolean>} * @return {Promise<boolean>}
*/ */
async function getOptionBool(name) { function getOptionBool(name) {
const val = await getOption(name); const val = getOption(name);
if (!['true', 'false'].includes(val)) { if (!['true', 'false'].includes(val)) {
throw new Error(`Could not parse "${val}" into boolean for option "${name}"`); throw new Error(`Could not parse "${val}" into boolean for option "${name}"`);
@ -38,36 +38,36 @@ async function getOptionBool(name) {
return val === 'true'; return val === 'true';
} }
async function setOption(name, value) { function setOption(name, value) {
const option = await require('./repository').getOption(name); const option = require('./repository').getOption(name);
if (option) { if (option) {
option.value = value; option.value = value;
await option.save(); option.save();
} }
else { else {
await createOption(name, value, false); createOption(name, value, false);
} }
} }
async function createOption(name, value, isSynced) { function createOption(name, value, isSynced) {
// to avoid circular dependency, need to find better solution // to avoid circular dependency, need to find better solution
const Option = require('../entities/option'); const Option = require('../entities/option');
await new Option({ new Option({
name: name, name: name,
value: value, value: value,
isSynced: isSynced isSynced: isSynced
}).save(); }).save();
} }
async function getOptions() { function getOptions() {
return await require('./repository').getEntities("SELECT * FROM options ORDER BY name"); return require('./repository').getEntities("SELECT * FROM options ORDER BY name");
} }
async function getOptionsMap() { function getOptionsMap() {
const options = await getOptions(); const options = getOptions();
return utils.toObject(options, opt => [opt.name, opt.value]); return utils.toObject(options, opt => [opt.name, opt.value]);
} }

View File

@ -7,28 +7,28 @@ const log = require('./log');
const dateUtils = require('./date_utils'); const dateUtils = require('./date_utils');
const keyboardActions = require('./keyboard_actions'); const keyboardActions = require('./keyboard_actions');
async function initDocumentOptions() { function initDocumentOptions() {
await optionService.createOption('documentId', utils.randomSecureToken(16), false); optionService.createOption('documentId', utils.randomSecureToken(16), false);
await optionService.createOption('documentSecret', utils.randomSecureToken(16), false); optionService.createOption('documentSecret', utils.randomSecureToken(16), false);
} }
async function initSyncedOptions(username, password) { function initSyncedOptions(username, password) {
await optionService.createOption('username', username, true); optionService.createOption('username', username, true);
await optionService.createOption('passwordVerificationSalt', utils.randomSecureToken(32), true); optionService.createOption('passwordVerificationSalt', utils.randomSecureToken(32), true);
await optionService.createOption('passwordDerivedKeySalt', utils.randomSecureToken(32), true); optionService.createOption('passwordDerivedKeySalt', utils.randomSecureToken(32), true);
const passwordVerificationKey = utils.toBase64(await myScryptService.getVerificationHash(password), true); const passwordVerificationKey = utils.toBase64(myScryptService.getVerificationHash(password), true);
await optionService.createOption('passwordVerificationHash', passwordVerificationKey, true); optionService.createOption('passwordVerificationHash', passwordVerificationKey, true);
// passwordEncryptionService expects these options to already exist // passwordEncryptionService expects these options to already exist
await optionService.createOption('encryptedDataKey', '', true); optionService.createOption('encryptedDataKey', '', true);
await passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16), true); passwordEncryptionService.setDataKey(password, utils.randomSecureToken(16), true);
} }
async function initNotSyncedOptions(initialized, startNotePath = 'root', opts = {}) { function initNotSyncedOptions(initialized, startNotePath = 'root', opts = {}) {
await optionService.createOption('openTabs', JSON.stringify([ optionService.createOption('openTabs', JSON.stringify([
{ {
notePath: startNotePath, notePath: startNotePath,
active: true, active: true,
@ -38,21 +38,21 @@ async function initNotSyncedOptions(initialized, startNotePath = 'root', opts =
} }
]), false); ]), false);
await optionService.createOption('lastDailyBackupDate', dateUtils.utcNowDateTime(), false); optionService.createOption('lastDailyBackupDate', dateUtils.utcNowDateTime(), false);
await optionService.createOption('lastWeeklyBackupDate', dateUtils.utcNowDateTime(), false); optionService.createOption('lastWeeklyBackupDate', dateUtils.utcNowDateTime(), false);
await optionService.createOption('lastMonthlyBackupDate', dateUtils.utcNowDateTime(), false); optionService.createOption('lastMonthlyBackupDate', dateUtils.utcNowDateTime(), false);
await optionService.createOption('dbVersion', appInfo.dbVersion, false); optionService.createOption('dbVersion', appInfo.dbVersion, false);
await optionService.createOption('initialized', initialized ? 'true' : 'false', false); optionService.createOption('initialized', initialized ? 'true' : 'false', false);
await optionService.createOption('lastSyncedPull', '0', false); optionService.createOption('lastSyncedPull', '0', false);
await optionService.createOption('lastSyncedPush', '0', false); optionService.createOption('lastSyncedPush', '0', false);
await optionService.createOption('theme', opts.theme || 'white', false); optionService.createOption('theme', opts.theme || 'white', false);
await optionService.createOption('syncServerHost', opts.syncServerHost || '', false); optionService.createOption('syncServerHost', opts.syncServerHost || '', false);
await optionService.createOption('syncServerTimeout', '5000', false); optionService.createOption('syncServerTimeout', '5000', false);
await optionService.createOption('syncProxy', opts.syncProxy || '', false); optionService.createOption('syncProxy', opts.syncProxy || '', false);
} }
const defaultOptions = [ const defaultOptions = [
@ -87,14 +87,14 @@ const defaultOptions = [
{ name: 'hideIncludedImages_main', value: 'true', isSynced: false } { name: 'hideIncludedImages_main', value: 'true', isSynced: false }
]; ];
async function initStartupOptions() { function initStartupOptions() {
const optionsMap = await optionService.getOptionsMap(); const optionsMap = optionService.getOptionsMap();
const allDefaultOptions = defaultOptions.concat(getKeyboardDefaultOptions()); const allDefaultOptions = defaultOptions.concat(getKeyboardDefaultOptions());
for (const {name, value, isSynced} of allDefaultOptions) { for (const {name, value, isSynced} of allDefaultOptions) {
if (!(name in optionsMap)) { if (!(name in optionsMap)) {
await optionService.createOption(name, value, isSynced); optionService.createOption(name, value, isSynced);
log.info(`Created missing option "${name}" with default value "${value}"`); log.info(`Created missing option "${name}" with default value "${value}"`);
} }

View File

@ -3,26 +3,26 @@ const myScryptService = require('./my_scrypt');
const utils = require('./utils'); const utils = require('./utils');
const dataEncryptionService = require('./data_encryption'); const dataEncryptionService = require('./data_encryption');
async function verifyPassword(password) { function verifyPassword(password) {
const givenPasswordHash = utils.toBase64(await myScryptService.getVerificationHash(password)); const givenPasswordHash = utils.toBase64(myScryptService.getVerificationHash(password));
const dbPasswordHash = await optionService.getOption('passwordVerificationHash'); const dbPasswordHash = optionService.getOption('passwordVerificationHash');
return givenPasswordHash === dbPasswordHash; return givenPasswordHash === dbPasswordHash;
} }
async function setDataKey(password, plainTextDataKey) { function setDataKey(password, plainTextDataKey) {
const passwordDerivedKey = await myScryptService.getPasswordDerivedKey(password); const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey, 16); const newEncryptedDataKey = dataEncryptionService.encrypt(passwordDerivedKey, plainTextDataKey, 16);
await optionService.setOption('encryptedDataKey', newEncryptedDataKey); optionService.setOption('encryptedDataKey', newEncryptedDataKey);
} }
async function getDataKey(password) { function getDataKey(password) {
const passwordDerivedKey = await myScryptService.getPasswordDerivedKey(password); const passwordDerivedKey = myScryptService.getPasswordDerivedKey(password);
const encryptedDataKey = await optionService.getOption('encryptedDataKey'); const encryptedDataKey = optionService.getOption('encryptedDataKey');
const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey, 16); const decryptedDataKey = dataEncryptionService.decrypt(passwordDerivedKey, encryptedDataKey, 16);

View File

@ -4,31 +4,26 @@ const sql = require('./sql');
const syncTableService = require('../services/sync_table'); const syncTableService = require('../services/sync_table');
const eventService = require('./events'); const eventService = require('./events');
const cls = require('./cls'); const cls = require('./cls');
const entityConstructor = require('../entities/entity_constructor');
let entityConstructor; function getEntityFromName(entityName, entityId) {
async function setEntityConstructor(constructor) {
entityConstructor = constructor;
}
async function getEntityFromName(entityName, entityId) {
if (!entityName || !entityId) { if (!entityName || !entityId) {
return null; return null;
} }
const constructor = entityConstructor.getEntityFromEntityName(entityName); const constructor = entityConstructor.getEntityFromEntityName(entityName);
return await getEntity(`SELECT * FROM ${constructor.entityName} WHERE ${constructor.primaryKeyName} = ?`, [entityId]); return getEntity(`SELECT * FROM ${constructor.entityName} WHERE ${constructor.primaryKeyName} = ?`, [entityId]);
} }
async function getEntities(query, params = []) { function getEntities(query, params = []) {
const rows = await sql.getRows(query, params); const rows = sql.getRows(query, params);
return rows.map(entityConstructor.createEntityFromRow); return rows.map(entityConstructor.createEntityFromRow);
} }
async function getEntity(query, params = []) { function getEntity(query, params = []) {
const row = await sql.getRowOrNull(query, params); const row = sql.getRowOrNull(query, params);
if (!row) { if (!row) {
return null; return null;
@ -37,11 +32,11 @@ async function getEntity(query, params = []) {
return entityConstructor.createEntityFromRow(row); return entityConstructor.createEntityFromRow(row);
} }
async function getCachedEntity(entityName, entityId, query) { function getCachedEntity(entityName, entityId, query) {
let entity = cls.getEntityFromCache(entityName, entityId); let entity = cls.getEntityFromCache(entityName, entityId);
if (!entity) { if (!entity) {
entity = await getEntity(query, [entityId]); entity = getEntity(query, [entityId]);
cls.setEntityToCache(entityName, entityId, entity); cls.setEntityToCache(entityName, entityId, entity);
} }
@ -50,18 +45,18 @@ async function getCachedEntity(entityName, entityId, query) {
} }
/** @returns {Promise<Note|null>} */ /** @returns {Promise<Note|null>} */
async function getNote(noteId) { function getNote(noteId) {
return await getCachedEntity('notes', noteId, "SELECT * FROM notes WHERE noteId = ?"); return getCachedEntity('notes', noteId, "SELECT * FROM notes WHERE noteId = ?");
} }
/** @returns {Promise<Note[]>} */ /** @returns {Promise<Note[]>} */
async function getNotes(noteIds) { function getNotes(noteIds) {
// this note might be optimised, but remember that it must keep the existing order of noteIds // this note might be optimised, but remember that it must keep the existing order of noteIds
// (important e.g. for @orderBy in search) // (important e.g. for @orderBy in search)
const notes = []; const notes = [];
for (const noteId of noteIds) { for (const noteId of noteIds) {
const note = await getNote(noteId); const note = getNote(noteId);
notes.push(note); notes.push(note);
} }
@ -70,40 +65,40 @@ async function getNotes(noteIds) {
} }
/** @returns {Promise<NoteRevision|null>} */ /** @returns {Promise<NoteRevision|null>} */
async function getNoteRevision(noteRevisionId) { function getNoteRevision(noteRevisionId) {
return await getCachedEntity('note_revisions', noteRevisionId, "SELECT * FROM note_revisions WHERE noteRevisionId = ?"); return getCachedEntity('note_revisions', noteRevisionId, "SELECT * FROM note_revisions WHERE noteRevisionId = ?");
} }
/** @returns {Promise<Branch|null>} */ /** @returns {Promise<Branch|null>} */
async function getBranch(branchId) { function getBranch(branchId) {
return await getCachedEntity('branches', branchId, "SELECT * FROM branches WHERE branchId = ?", [branchId]); return getCachedEntity('branches', branchId, "SELECT * FROM branches WHERE branchId = ?", [branchId]);
} }
/** @returns {Promise<Attribute|null>} */ /** @returns {Promise<Attribute|null>} */
async function getAttribute(attributeId) { function getAttribute(attributeId) {
return await getCachedEntity('attributes', attributeId, "SELECT * FROM attributes WHERE attributeId = ?"); return getCachedEntity('attributes', attributeId, "SELECT * FROM attributes WHERE attributeId = ?");
} }
/** @returns {Promise<Option|null>} */ /** @returns {Promise<Option|null>} */
async function getOption(name) { function getOption(name) {
return await getEntity("SELECT * FROM options WHERE name = ?", [name]); return getEntity("SELECT * FROM options WHERE name = ?", [name]);
} }
async function updateEntity(entity) { function updateEntity(entity) {
const entityName = entity.constructor.entityName; const entityName = entity.constructor.entityName;
const primaryKeyName = entity.constructor.primaryKeyName; const primaryKeyName = entity.constructor.primaryKeyName;
const isNewEntity = !entity[primaryKeyName]; const isNewEntity = !entity[primaryKeyName];
if (entity.beforeSaving) { if (entity.beforeSaving) {
await entity.beforeSaving(); entity.beforeSaving();
} }
const clone = Object.assign({}, entity); const clone = Object.assign({}, entity);
// this check requires that updatePojo is not static // this check requires that updatePojo is not static
if (entity.updatePojo) { if (entity.updatePojo) {
await entity.updatePojo(clone); entity.updatePojo(clone);
} }
// indicates whether entity actually changed // indicates whether entity actually changed
@ -116,15 +111,15 @@ async function updateEntity(entity) {
} }
} }
await sql.transactional(async () => { sql.transactional(() => {
await sql.upsert(entityName, primaryKeyName, clone); sql.upsert(entityName, primaryKeyName, clone);
const primaryKey = entity[primaryKeyName]; const primaryKey = entity[primaryKeyName];
if (entity.isChanged) { if (entity.isChanged) {
const isSynced = entityName !== 'options' || entity.isSynced; const isSynced = entityName !== 'options' || entity.isSynced;
await syncTableService.addEntitySync(entityName, primaryKey, null, isSynced); syncTableService.addEntitySync(entityName, primaryKey, null, isSynced);
if (!cls.isEntityEventsDisabled()) { if (!cls.isEntityEventsDisabled()) {
const eventPayload = { const eventPayload = {
@ -133,11 +128,11 @@ async function updateEntity(entity) {
}; };
if (isNewEntity && !entity.isDeleted) { if (isNewEntity && !entity.isDeleted) {
await eventService.emit(eventService.ENTITY_CREATED, eventPayload); eventService.emit(eventService.ENTITY_CREATED, eventPayload);
} }
// it seems to be better to handle deletion and update separately // it seems to be better to handle deletion and update separately
await eventService.emit(entity.isDeleted ? eventService.ENTITY_DELETED : eventService.ENTITY_CHANGED, eventPayload); eventService.emit(entity.isDeleted ? eventService.ENTITY_DELETED : eventService.ENTITY_CHANGED, eventPayload);
} }
} }
}); });
@ -153,6 +148,5 @@ module.exports = {
getBranch, getBranch,
getAttribute, getAttribute,
getOption, getOption,
updateEntity, updateEntity
setEntityConstructor
}; };

View File

@ -19,7 +19,7 @@ function exec(opts) {
const proxyAgent = getProxyAgent(opts); const proxyAgent = getProxyAgent(opts);
const parsedTargetUrl = url.parse(opts.url); const parsedTargetUrl = url.parse(opts.url);
return new Promise(async (resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const headers = { const headers = {
Cookie: (opts.cookieJar && opts.cookieJar.header) || "", Cookie: (opts.cookieJar && opts.cookieJar.header) || "",
@ -83,18 +83,18 @@ function exec(opts) {
}); });
} }
async function getImage(imageUrl) { function getImage(imageUrl) {
const opts = { const opts = {
method: 'GET', method: 'GET',
url: imageUrl, url: imageUrl,
proxy: await syncOptions.getSyncProxy() proxy: syncOptions.getSyncProxy()
}; };
const client = getClient(opts); const client = getClient(opts);
const proxyAgent = getProxyAgent(opts); const proxyAgent = getProxyAgent(opts);
const parsedTargetUrl = url.parse(opts.url); const parsedTargetUrl = url.parse(opts.url);
return await new Promise(async (resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const request = client.request({ const request = client.request({
method: opts.method, method: opts.method,

View File

@ -3,8 +3,8 @@ const repository = require('./repository');
const cls = require('./cls'); const cls = require('./cls');
const sqlInit = require('./sql_init'); const sqlInit = require('./sql_init');
async function runNotesWithLabel(runAttrValue) { function runNotesWithLabel(runAttrValue) {
const notes = await repository.getEntities(` const notes = repository.getEntities(`
SELECT notes.* SELECT notes.*
FROM notes FROM notes
JOIN attributes ON attributes.noteId = notes.noteId JOIN attributes ON attributes.noteId = notes.noteId
@ -21,10 +21,8 @@ async function runNotesWithLabel(runAttrValue) {
} }
} }
sqlInit.dbReady.then(() => { setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
setTimeout(cls.wrap(() => runNotesWithLabel('backendStartup')), 10 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000); setInterval(cls.wrap(() => runNotesWithLabel('hourly')), 3600 * 1000);
setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000); setInterval(cls.wrap(() => runNotesWithLabel('daily')), 24 * 3600 * 1000);
});

View File

@ -4,28 +4,28 @@ const repository = require('./repository');
const cls = require('./cls'); const cls = require('./cls');
const log = require('./log'); const log = require('./log');
async function executeNote(note, apiParams) { function executeNote(note, apiParams) {
if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable) { if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable) {
log.info(`Cannot execute note ${note.noteId}`); log.info(`Cannot execute note ${note.noteId}`);
return; return;
} }
const bundle = await getScriptBundle(note); const bundle = getScriptBundle(note);
return await executeBundle(bundle, apiParams); return executeBundle(bundle, apiParams);
} }
async function executeNoteNoException(note, apiParams) { function executeNoteNoException(note, apiParams) {
try { try {
await executeNote(note, apiParams); executeNote(note, apiParams);
} }
catch (e) { catch (e) {
// just swallow, exception is logged already in executeNote // just swallow, exception is logged already in executeNote
} }
} }
async function executeBundle(bundle, apiParams = {}) { function executeBundle(bundle, apiParams = {}) {
if (!apiParams.startNote) { if (!apiParams.startNote) {
// this is the default case, the only exception is when we want to preserve frontend startNote // this is the default case, the only exception is when we want to preserve frontend startNote
apiParams.startNote = bundle.note; apiParams.startNote = bundle.note;
@ -34,16 +34,16 @@ async function executeBundle(bundle, apiParams = {}) {
cls.set('sourceId', 'script'); cls.set('sourceId', 'script');
// last \r\n is necessary if script contains line comment on its last line // last \r\n is necessary if script contains line comment on its last line
const script = "async function() {\r\n" + bundle.script + "\r\n}"; const script = "function() {\r\n" + bundle.script + "\r\n}";
const ctx = new ScriptContext(bundle.allNotes, apiParams); const ctx = new ScriptContext(bundle.allNotes, apiParams);
try { try {
if (await bundle.note.hasOwnedLabel('manualTransactionHandling')) { if (bundle.note.hasOwnedLabel('manualTransactionHandling')) {
return await execute(ctx, script); return execute(ctx, script);
} }
else { else {
return await sql.transactional(async () => await execute(ctx, script)); return sql.transactional(() => execute(ctx, script));
} }
} }
catch (e) { catch (e) {
@ -57,22 +57,22 @@ async function executeBundle(bundle, apiParams = {}) {
* This method preserves frontend startNode - that's why we start execution from currentNote and override * This method preserves frontend startNode - that's why we start execution from currentNote and override
* bundle's startNote. * bundle's startNote.
*/ */
async function executeScript(script, params, startNoteId, currentNoteId, originEntityName, originEntityId) { function executeScript(script, params, startNoteId, currentNoteId, originEntityName, originEntityId) {
const startNote = await repository.getNote(startNoteId); const startNote = repository.getNote(startNoteId);
const currentNote = await repository.getNote(currentNoteId); const currentNote = repository.getNote(currentNoteId);
const originEntity = await repository.getEntityFromName(originEntityName, originEntityId); const originEntity = repository.getEntityFromName(originEntityName, originEntityId);
currentNote.content = `return await (${script}\r\n)(${getParams(params)})`; currentNote.content = `return (${script}\r\n)(${getParams(params)})`;
currentNote.type = 'code'; currentNote.type = 'code';
currentNote.mime = 'application/javascript;env=backend'; currentNote.mime = 'application/javascript;env=backend';
const bundle = await getScriptBundle(currentNote); const bundle = getScriptBundle(currentNote);
return await executeBundle(bundle, { startNote, originEntity }); return executeBundle(bundle, { startNote, originEntity });
} }
async function execute(ctx, script) { function execute(ctx, script) {
return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx)); return (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx));
} }
function getParams(params) { function getParams(params) {
@ -90,8 +90,8 @@ function getParams(params) {
}).join(","); }).join(",");
} }
async function getScriptBundleForFrontend(note) { function getScriptBundleForFrontend(note) {
const bundle = await getScriptBundle(note); const bundle = getScriptBundle(note);
if (!bundle) { if (!bundle) {
return; return;
@ -107,7 +107,7 @@ async function getScriptBundleForFrontend(note) {
return bundle; return bundle;
} }
async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) { function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
if (!note.isContentAvailable) { if (!note.isContentAvailable) {
return; return;
} }
@ -116,7 +116,7 @@ async function getScriptBundle(note, root = true, scriptEnv = null, includedNote
return; return;
} }
if (!root && await note.hasOwnedLabel('disableInclusion')) { if (!root && note.hasOwnedLabel('disableInclusion')) {
return; return;
} }
@ -143,8 +143,8 @@ async function getScriptBundle(note, root = true, scriptEnv = null, includedNote
const modules = []; const modules = [];
for (const child of await note.getChildNotes()) { for (const child of note.getChildNotes()) {
const childBundle = await getScriptBundle(child, false, scriptEnv, includedNoteIds); const childBundle = getScriptBundle(child, false, scriptEnv, includedNoteIds);
if (childBundle) { if (childBundle) {
modules.push(childBundle.note); modules.push(childBundle.note);
@ -159,10 +159,10 @@ async function getScriptBundle(note, root = true, scriptEnv = null, includedNote
if (note.isJavaScript()) { if (note.isJavaScript()) {
bundle.script += ` bundle.script += `
apiContext.modules['${note.noteId}'] = {}; apiContext.modules['${note.noteId}'] = {};
${root ? 'return ' : ''}await ((async function(exports, module, require, api` + (modules.length > 0 ? ', ' : '') + ${root ? 'return ' : ''}((function(exports, module, require, api` + (modules.length > 0 ? ', ' : '') +
modules.map(child => sanitizeVariableName(child.title)).join(', ') + `) { modules.map(child => sanitizeVariableName(child.title)).join(', ') + `) {
try { try {
${await note.getContent()}; ${note.getContent()};
} catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); } } catch (e) { throw new Error("Load of script note \\"${note.title}\\" (${note.noteId}) failed with: " + e.message); }
if (!module.exports) module.exports = {}; if (!module.exports) module.exports = {};
for (const exportKey in exports) module.exports[exportKey] = exports[exportKey]; for (const exportKey in exports) module.exports[exportKey] = exports[exportKey];
@ -172,7 +172,7 @@ return module.exports;
`; `;
} }
else if (note.isHtml()) { else if (note.isHtml()) {
bundle.html += await note.getContent(); bundle.html += note.getContent();
} }
return bundle; return bundle;

View File

@ -20,19 +20,19 @@ const parseFilters = require('./search/parse_filters.js');
const buildSearchQuery = require('./build_search_query'); const buildSearchQuery = require('./build_search_query');
const noteCacheService = require('./note_cache/note_cache.js'); const noteCacheService = require('./note_cache/note_cache.js');
async function searchForNotes(searchString) { function searchForNotes(searchString) {
const noteIds = await searchForNoteIds(searchString); const noteIds = searchForNoteIds(searchString);
return await repository.getNotes(noteIds); return repository.getNotes(noteIds);
} }
async function searchForNoteIds(searchString) { function searchForNoteIds(searchString) {
const filters = parseFilters(searchString); const filters = parseFilters(searchString);
const {query, params} = buildSearchQuery(filters, 'notes.noteId'); const {query, params} = buildSearchQuery(filters, 'notes.noteId');
try { try {
let noteIds = await sql.getColumn(query, params); let noteIds = sql.getColumn(query, params);
noteIds = noteIds.filter(noteCacheService.isAvailable); noteIds = noteIds.filter(noteCacheService.isAvailable);

View File

@ -18,9 +18,9 @@ class AndExp extends Expression {
this.subExpressions = subExpressions; this.subExpressions = subExpressions;
} }
async execute(inputNoteSet, searchContext) { execute(inputNoteSet, searchContext) {
for (const subExpression of this.subExpressions) { for (const subExpression of this.subExpressions) {
inputNoteSet = await subExpression.execute(inputNoteSet, searchContext); inputNoteSet = subExpression.execute(inputNoteSet, searchContext);
} }
return inputNoteSet; return inputNoteSet;

View File

@ -15,13 +15,13 @@ class NoteContentFulltextExp extends Expression {
this.tokens = tokens; this.tokens = tokens;
} }
async execute(inputNoteSet) { execute(inputNoteSet) {
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
const wheres = this.tokens.map(token => "note_contents.content LIKE " + utils.prepareSqlForLike(this.likePrefix, token, this.likeSuffix)); const wheres = this.tokens.map(token => "note_contents.content LIKE " + utils.prepareSqlForLike(this.likePrefix, token, this.likeSuffix));
const sql = require('../../sql'); const sql = require('../../sql');
const noteIds = await sql.getColumn(` const noteIds = sql.getColumn(`
SELECT notes.noteId SELECT notes.noteId
FROM notes FROM notes
JOIN note_contents ON notes.noteId = note_contents.noteId JOIN note_contents ON notes.noteId = note_contents.noteId

View File

@ -21,11 +21,11 @@ class OrExp extends Expression {
this.subExpressions = subExpressions; this.subExpressions = subExpressions;
} }
async execute(inputNoteSet, searchContext) { execute(inputNoteSet, searchContext) {
const resultNoteSet = new NoteSet(); const resultNoteSet = new NoteSet();
for (const subExpression of this.subExpressions) { for (const subExpression of this.subExpressions) {
resultNoteSet.mergeIn(await subExpression.execute(inputNoteSet, searchContext)); resultNoteSet.mergeIn(subExpression.execute(inputNoteSet, searchContext));
} }
return resultNoteSet; return resultNoteSet;

View File

@ -15,7 +15,7 @@ const utils = require('../utils');
* @param {Expression} expression * @param {Expression} expression
* @return {Promise<SearchResult[]>} * @return {Promise<SearchResult[]>}
*/ */
async function findNotesWithExpression(expression) { function findNotesWithExpression(expression) {
const hoistedNote = noteCache.notes[hoistedNoteService.getHoistedNoteId()]; const hoistedNote = noteCache.notes[hoistedNoteService.getHoistedNoteId()];
const allNotes = (hoistedNote && hoistedNote.noteId !== 'root') const allNotes = (hoistedNote && hoistedNote.noteId !== 'root')
? hoistedNote.subtreeNotes ? hoistedNote.subtreeNotes
@ -27,7 +27,7 @@ async function findNotesWithExpression(expression) {
noteIdToNotePath: {} noteIdToNotePath: {}
}; };
const noteSet = await expression.execute(allNoteSet, searchContext); const noteSet = expression.execute(allNoteSet, searchContext);
let searchResults = noteSet.notes let searchResults = noteSet.notes
.map(note => searchContext.noteIdToNotePath[note.noteId] || noteCacheService.getSomePath(note)) .map(note => searchContext.noteIdToNotePath[note.noteId] || noteCacheService.getSomePath(note))
@ -67,17 +67,17 @@ function parseQueryToExpression(query, parsingContext) {
* @param {ParsingContext} parsingContext * @param {ParsingContext} parsingContext
* @return {Promise<SearchResult[]>} * @return {Promise<SearchResult[]>}
*/ */
async function findNotesWithQuery(query, parsingContext) { function findNotesWithQuery(query, parsingContext) {
const expression = parseQueryToExpression(query, parsingContext); const expression = parseQueryToExpression(query, parsingContext);
if (!expression) { if (!expression) {
return []; return [];
} }
return await findNotesWithExpression(expression); return findNotesWithExpression(expression);
} }
async function searchNotes(query) { function searchNotes(query) {
if (!query.trim().length) { if (!query.trim().length) {
return []; return [];
} }
@ -87,7 +87,7 @@ async function searchNotes(query) {
fuzzyAttributeSearch: false fuzzyAttributeSearch: false
}); });
const allSearchResults = await findNotesWithQuery(query, parsingContext); const allSearchResults = findNotesWithQuery(query, parsingContext);
const trimmedSearchResults = allSearchResults.slice(0, 200); const trimmedSearchResults = allSearchResults.slice(0, 200);
return { return {
@ -96,7 +96,7 @@ async function searchNotes(query) {
}; };
} }
async function searchNotesForAutocomplete(query) { function searchNotesForAutocomplete(query) {
if (!query.trim().length) { if (!query.trim().length) {
return []; return [];
} }
@ -106,7 +106,7 @@ async function searchNotesForAutocomplete(query) {
fuzzyAttributeSearch: true fuzzyAttributeSearch: true
}); });
let searchResults = await findNotesWithQuery(query, parsingContext); let searchResults = findNotesWithQuery(query, parsingContext);
searchResults = searchResults.slice(0, 200); searchResults = searchResults.slice(0, 200);

View File

@ -22,9 +22,9 @@ function triggerSync() {
log.info("Triggering sync."); log.info("Triggering sync.");
// it's ok to not wait for it here // it's ok to not wait for it here
syncService.sync().then(async res => { syncService.sync().then(res => {
if (res.success) { if (res.success) {
await sqlInit.dbInitialized(); sqlInit.dbInitialized();
} }
}); });
} }
@ -33,30 +33,30 @@ async function sendSeedToSyncServer() {
log.info("Initiating sync to server"); log.info("Initiating sync to server");
await requestToSyncServer('POST', '/api/setup/sync-seed', { await requestToSyncServer('POST', '/api/setup/sync-seed', {
options: await getSyncSeedOptions(), options: getSyncSeedOptions(),
syncVersion: appInfo.syncVersion syncVersion: appInfo.syncVersion
}); });
// this is completely new sync, need to reset counters. If this would not be new sync, // this is completely new sync, need to reset counters. If this would not be new sync,
// the previous request would have failed. // the previous request would have failed.
await optionService.setOption('lastSyncedPush', 0); optionService.setOption('lastSyncedPush', 0);
await optionService.setOption('lastSyncedPull', 0); optionService.setOption('lastSyncedPull', 0);
} }
async function requestToSyncServer(method, path, body = null) { async function requestToSyncServer(method, path, body = null) {
const timeout = await syncOptions.getSyncTimeout(); const timeout = syncOptions.getSyncTimeout();
return utils.timeLimit(request.exec({ return await utils.timeLimit(request.exec({
method, method,
url: await syncOptions.getSyncServerHost() + path, url: syncOptions.getSyncServerHost() + path,
body, body,
proxy: await syncOptions.getSyncProxy(), proxy: syncOptions.getSyncProxy(),
timeout: timeout timeout: timeout
}), timeout); }), timeout);
} }
async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) { async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, password) {
if (await sqlInit.isDbInitialized()) { if (sqlInit.isDbInitialized()) {
return { return {
result: 'failure', result: 'failure',
error: 'DB is already initialized.' error: 'DB is already initialized.'
@ -89,7 +89,7 @@ async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, pass
} }
} }
await sqlInit.createDatabaseForSync(resp.options, syncServerHost, syncProxy); sqlInit.createDatabaseForSync(resp.options, syncServerHost, syncProxy);
triggerSync(); triggerSync();
@ -105,10 +105,10 @@ async function setupSyncFromSyncServer(syncServerHost, syncProxy, username, pass
} }
} }
async function getSyncSeedOptions() { function getSyncSeedOptions() {
return [ return [
await repository.getOption('documentId'), repository.getOption('documentId'),
await repository.getOption('documentSecret') repository.getOption('documentSecret')
]; ];
} }

View File

@ -5,13 +5,13 @@ const sql = require('./sql');
const sqlInit = require('./sql_init'); const sqlInit = require('./sql_init');
const cls = require('./cls'); const cls = require('./cls');
async function saveSourceId(sourceId) { function saveSourceId(sourceId) {
await sql.insert("source_ids", { sql.insert("source_ids", {
sourceId: sourceId, sourceId: sourceId,
utcDateCreated: dateUtils.utcNowDateTime() utcDateCreated: dateUtils.utcNowDateTime()
}); });
await refreshSourceIds(); refreshSourceIds();
} }
function createSourceId() { function createSourceId() {
@ -21,16 +21,16 @@ function createSourceId() {
return sourceId; return sourceId;
} }
async function generateSourceId() { function generateSourceId() {
const sourceId = createSourceId(); const sourceId = createSourceId();
await saveSourceId(sourceId); saveSourceId(sourceId);
return sourceId; return sourceId;
} }
async function refreshSourceIds() { function refreshSourceIds() {
const sourceIdsArr = await sql.getColumn("SELECT sourceId FROM source_ids ORDER BY utcDateCreated DESC"); const sourceIdsArr = sql.getColumn("SELECT sourceId FROM source_ids ORDER BY utcDateCreated DESC");
allSourceIds = {}; allSourceIds = {};
@ -48,7 +48,7 @@ function isLocalSourceId(srcId) {
const currentSourceId = createSourceId(); const currentSourceId = createSourceId();
// this will also refresh source IDs // this will also refresh source IDs
sqlInit.dbReady.then(cls.wrap(() => saveSourceId(currentSourceId))); cls.wrap(() => saveSourceId(currentSourceId));
function getCurrentSourceId() { function getCurrentSourceId() {
return currentSourceId; return currentSourceId;

View File

@ -19,7 +19,7 @@ function setDbConnection(connection) {
}); });
}); });
async function insert(tableName, rec, replace = false) { function insert(tableName, rec, replace = false) {
const keys = Object.keys(rec); const keys = Object.keys(rec);
if (keys.length === 0) { if (keys.length === 0) {
log.error("Can't insert empty object into table " + tableName); log.error("Can't insert empty object into table " + tableName);
@ -31,16 +31,16 @@ async function insert(tableName, rec, replace = false) {
const query = "INSERT " + (replace ? "OR REPLACE" : "") + " INTO " + tableName + "(" + columns + ") VALUES (" + questionMarks + ")"; const query = "INSERT " + (replace ? "OR REPLACE" : "") + " INTO " + tableName + "(" + columns + ") VALUES (" + questionMarks + ")";
const res = await execute(query, Object.values(rec)); const res = execute(query, Object.values(rec));
return res.lastInsertRowid; return res.lastInsertRowid;
} }
async function replace(tableName, rec) { function replace(tableName, rec) {
return await insert(tableName, rec, true); return insert(tableName, rec, true);
} }
async function upsert(tableName, primaryKey, rec) { function upsert(tableName, primaryKey, rec) {
const keys = Object.keys(rec); const keys = Object.keys(rec);
if (keys.length === 0) { if (keys.length === 0) {
log.error("Can't upsert empty object into table " + tableName); log.error("Can't upsert empty object into table " + tableName);
@ -62,7 +62,7 @@ async function upsert(tableName, primaryKey, rec) {
} }
} }
await execute(query, rec); execute(query, rec);
} }
const statementCache = {}; const statementCache = {};
@ -87,18 +87,18 @@ function rollback() {
return stmt("ROLLBACK").run(); return stmt("ROLLBACK").run();
} }
async function getRow(query, params = []) { function getRow(query, params = []) {
return wrap(() => stmt(query).get(params), query); return wrap(() => stmt(query).get(params), query);
} }
async function getRowOrNull(query, params = []) { function getRowOrNull(query, params = []) {
const all = await getRows(query, params); const all = getRows(query, params);
return all.length > 0 ? all[0] : null; return all.length > 0 ? all[0] : null;
} }
async function getValue(query, params = []) { function getValue(query, params = []) {
const row = await getRowOrNull(query, params); const row = getRowOrNull(query, params);
if (!row) { if (!row) {
return null; return null;
@ -110,7 +110,7 @@ async function getValue(query, params = []) {
const PARAM_LIMIT = 900; // actual limit is 999 const PARAM_LIMIT = 900; // actual limit is 999
// this is to overcome 999 limit of number of query parameters // this is to overcome 999 limit of number of query parameters
async function getManyRows(query, params) { function getManyRows(query, params) {
let results = []; let results = [];
while (params.length > 0) { while (params.length > 0) {
@ -128,19 +128,19 @@ async function getManyRows(query, params) {
const questionMarks = curParams.map(() => ":param" + i++).join(","); const questionMarks = curParams.map(() => ":param" + i++).join(",");
const curQuery = query.replace(/\?\?\?/g, questionMarks); const curQuery = query.replace(/\?\?\?/g, questionMarks);
results = results.concat(await getRows(curQuery, curParamsObj)); results = results.concat(getRows(curQuery, curParamsObj));
} }
return results; return results;
} }
async function getRows(query, params = []) { function getRows(query, params = []) {
return wrap(() => stmt(query).all(params), query); return wrap(() => stmt(query).all(params), query);
} }
async function getMap(query, params = []) { function getMap(query, params = []) {
const map = {}; const map = {};
const results = await getRows(query, params); const results = getRows(query, params);
for (const row of results) { for (const row of results) {
const keys = Object.keys(row); const keys = Object.keys(row);
@ -151,9 +151,9 @@ async function getMap(query, params = []) {
return map; return map;
} }
async function getColumn(query, params = []) { function getColumn(query, params = []) {
const list = []; const list = [];
const result = await getRows(query, params); const result = getRows(query, params);
if (result.length === 0) { if (result.length === 0) {
return list; return list;
@ -168,25 +168,25 @@ async function getColumn(query, params = []) {
return list; return list;
} }
async function execute(query, params = []) { function execute(query, params = []) {
await startTransactionIfNecessary(); startTransactionIfNecessary();
return wrap(() => stmt(query).run(params), query); return wrap(() => stmt(query).run(params), query);
} }
async function executeWithoutTransaction(query, params = []) { function executeWithoutTransaction(query, params = []) {
await dbConnection.run(query, params); dbConnection.run(query, params);
} }
async function executeMany(query, params) { function executeMany(query, params) {
await startTransactionIfNecessary(); startTransactionIfNecessary();
// essentially just alias // essentially just alias
await getManyRows(query, params); getManyRows(query, params);
} }
async function executeScript(query) { function executeScript(query) {
await startTransactionIfNecessary(); startTransactionIfNecessary();
return wrap(() => stmt.run(query), query); return wrap(() => stmt.run(query), query);
} }
@ -231,37 +231,33 @@ let transactionActive = false;
let transactionPromise = null; let transactionPromise = null;
let transactionPromiseResolve = null; let transactionPromiseResolve = null;
async function startTransactionIfNecessary() { function startTransactionIfNecessary() {
if (!cls.get('isTransactional') if (!cls.get('isTransactional')
|| cls.get('isInTransaction')) { || cls.get('isInTransaction')) {
return; return;
} }
while (transactionActive) {
await transactionPromise;
}
// first set semaphore (atomic operation and only then start transaction // first set semaphore (atomic operation and only then start transaction
transactionActive = true; transactionActive = true;
transactionPromise = new Promise(res => transactionPromiseResolve = res); transactionPromise = new Promise(res => transactionPromiseResolve = res);
cls.set('isInTransaction', true); cls.set('isInTransaction', true);
await beginTransaction(); beginTransaction();
} }
async function transactional(func) { function transactional(func) {
// if the CLS is already transactional then the whole transaction is handled by higher level transactional() call // if the CLS is already transactional then the whole transaction is handled by higher level transactional() call
if (cls.get('isTransactional')) { if (cls.get('isTransactional')) {
return await func(); return func();
} }
cls.set('isTransactional', true); // this signals that transaction will be needed if there's a write operation cls.set('isTransactional', true); // this signals that transaction will be needed if there's a write operation
try { try {
const ret = await func(); const ret = func();
if (cls.get('isInTransaction')) { if (cls.get('isInTransaction')) {
await commit(); commit();
// note that sync rows sent from this action will be sent again by scheduled periodic ping // note that sync rows sent from this action will be sent again by scheduled periodic ping
require('./ws.js').sendPingToAllClients(); require('./ws.js').sendPingToAllClients();
@ -271,7 +267,7 @@ async function transactional(func) {
} }
catch (e) { catch (e) {
if (cls.get('isInTransaction')) { if (cls.get('isInTransaction')) {
await rollback(); rollback();
} }
throw e; throw e;

View File

@ -19,32 +19,32 @@ sql.setDbConnection(dbConnection);
const dbReady = initDbConnection(); const dbReady = initDbConnection();
async function schemaExists() { function schemaExists() {
const tableResults = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='options'"); const tableResults = sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='options'");
return tableResults.length === 1; return tableResults.length === 1;
} }
async function isDbInitialized() { function isDbInitialized() {
if (!await schemaExists()) { if (!schemaExists()) {
return false; return false;
} }
const initialized = await sql.getValue("SELECT value FROM options WHERE name = 'initialized'"); const initialized = sql.getValue("SELECT value FROM options WHERE name = 'initialized'");
// !initialized may be removed in the future, required only for migration // !initialized may be removed in the future, required only for migration
return !initialized || initialized === 'true'; return !initialized || initialized === 'true';
} }
async function initDbConnection() { function initDbConnection() {
await cls.init(async () => { cls.init(() => {
if (!await isDbInitialized()) { if (!isDbInitialized()) {
log.info(`DB not initialized, please visit setup page` + (utils.isElectron() ? '' : ` - http://[your-server-host]:${await port} to see instructions on how to initialize Trilium.`)); log.info(`DB not initialized, please visit setup page` + (utils.isElectron() ? '' : ` - http://[your-server-host]:${port} to see instructions on how to initialize Trilium.`));
return; return;
} }
const currentDbVersion = await getDbVersion(); const currentDbVersion = getDbVersion();
if (currentDbVersion > appInfo.dbVersion) { if (currentDbVersion > appInfo.dbVersion) {
log.error(`Current DB version ${currentDbVersion} is newer than app db version ${appInfo.dbVersion} which means that it was created by newer and incompatible version of Trilium. Upgrade to latest version of Trilium to resolve this issue.`); log.error(`Current DB version ${currentDbVersion} is newer than app db version ${appInfo.dbVersion} which means that it was created by newer and incompatible version of Trilium. Upgrade to latest version of Trilium to resolve this issue.`);
@ -52,45 +52,43 @@ async function initDbConnection() {
utils.crash(); utils.crash();
} }
if (!await isDbUpToDate()) { if (!isDbUpToDate()) {
// avoiding circular dependency // avoiding circular dependency
const migrationService = require('./migration'); const migrationService = require('./migration');
await migrationService.migrate(); migrationService.migrate();
} }
await require('./options_init').initStartupOptions(); require('./options_init').initStartupOptions();
log.info("DB ready.");
}); });
} }
async function createInitialDatabase(username, password, theme) { function createInitialDatabase(username, password, theme) {
log.info("Creating initial database ..."); log.info("Creating initial database ...");
if (await isDbInitialized()) { if (isDbInitialized()) {
throw new Error("DB is already initialized"); throw new Error("DB is already initialized");
} }
const schema = fs.readFileSync(resourceDir.DB_INIT_DIR + '/schema.sql', 'UTF-8'); const schema = fs.readFileSync(resourceDir.DB_INIT_DIR + '/schema.sql', 'UTF-8');
const demoFile = fs.readFileSync(resourceDir.DB_INIT_DIR + '/demo.zip'); const demoFile = fs.readFileSync(resourceDir.DB_INIT_DIR + '/demo.zip');
await sql.transactional(async () => { sql.transactional(() => {
await sql.executeScript(schema); sql.executeScript(schema);
const Note = require("../entities/note"); const Note = require("../entities/note");
const Branch = require("../entities/branch"); const Branch = require("../entities/branch");
const rootNote = await new Note({ const rootNote = new Note({
noteId: 'root', noteId: 'root',
title: 'root', title: 'root',
type: 'text', type: 'text',
mime: 'text/html' mime: 'text/html'
}).save(); }).save();
await rootNote.setContent(''); rootNote.setContent('');
await new Branch({ new Branch({
branchId: 'root', branchId: 'root',
noteId: 'root', noteId: 'root',
parentNoteId: 'none', parentNoteId: 'none',
@ -101,53 +99,53 @@ async function createInitialDatabase(username, password, theme) {
const dummyTaskContext = new TaskContext("1", 'import', false); const dummyTaskContext = new TaskContext("1", 'import', false);
const zipImportService = require("./import/zip"); const zipImportService = require("./import/zip");
await zipImportService.importZip(dummyTaskContext, demoFile, rootNote); zipImportService.importZip(dummyTaskContext, demoFile, rootNote);
const startNoteId = await sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition"); const startNoteId = sql.getValue("SELECT noteId FROM branches WHERE parentNoteId = 'root' AND isDeleted = 0 ORDER BY notePosition");
const optionsInitService = require('./options_init'); const optionsInitService = require('./options_init');
await optionsInitService.initDocumentOptions(); optionsInitService.initDocumentOptions();
await optionsInitService.initSyncedOptions(username, password); optionsInitService.initSyncedOptions(username, password);
await optionsInitService.initNotSyncedOptions(true, startNoteId, { theme }); optionsInitService.initNotSyncedOptions(true, startNoteId, { theme });
await require('./sync_table').fillAllSyncRows(); require('./sync_table').fillAllSyncRows();
}); });
log.info("Schema and initial content generated."); log.info("Schema and initial content generated.");
await initDbConnection(); initDbConnection();
} }
async function createDatabaseForSync(options, syncServerHost = '', syncProxy = '') { function createDatabaseForSync(options, syncServerHost = '', syncProxy = '') {
log.info("Creating database for sync"); log.info("Creating database for sync");
if (await isDbInitialized()) { if (isDbInitialized()) {
throw new Error("DB is already initialized"); throw new Error("DB is already initialized");
} }
const schema = fs.readFileSync(resourceDir.DB_INIT_DIR + '/schema.sql', 'UTF-8'); const schema = fs.readFileSync(resourceDir.DB_INIT_DIR + '/schema.sql', 'UTF-8');
await sql.transactional(async () => { sql.transactional(() => {
await sql.executeScript(schema); sql.executeScript(schema);
await require('./options_init').initNotSyncedOptions(false, 'root', { syncServerHost, syncProxy }); require('./options_init').initNotSyncedOptions(false, 'root', { syncServerHost, syncProxy });
// document options required for sync to kick off // document options required for sync to kick off
for (const opt of options) { for (const opt of options) {
await new Option(opt).save(); new Option(opt).save();
} }
}); });
log.info("Schema and not synced options generated."); log.info("Schema and not synced options generated.");
} }
async function getDbVersion() { function getDbVersion() {
return parseInt(await sql.getValue("SELECT value FROM options WHERE name = 'dbVersion'")); return parseInt(sql.getValue("SELECT value FROM options WHERE name = 'dbVersion'"));
} }
async function isDbUpToDate() { function isDbUpToDate() {
const dbVersion = await getDbVersion(); const dbVersion = getDbVersion();
const upToDate = dbVersion >= appInfo.dbVersion; const upToDate = dbVersion >= appInfo.dbVersion;
@ -158,17 +156,15 @@ async function isDbUpToDate() {
return upToDate; return upToDate;
} }
async function dbInitialized() { function dbInitialized() {
if (!await isDbInitialized()) { if (!isDbInitialized()) {
await optionService.setOption('initialized', 'true'); optionService.setOption('initialized', 'true');
await initDbConnection(); initDbConnection();
} }
} }
dbReady.then(async () => { log.info("DB size: " + sql.getValue("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()") + " KB");
log.info("DB size: " + await sql.getValue("SELECT page_count * page_size / 1000 as size FROM pragma_page_count(), pragma_page_size()") + " KB");
});
module.exports = { module.exports = {
dbReady, dbReady,

View File

@ -28,7 +28,7 @@ const stats = {
async function sync() { async function sync() {
try { try {
return await syncMutexService.doExclusively(async () => { return await syncMutexService.doExclusively(async () => {
if (!await syncOptions.isSyncSetup()) { if (!syncOptions.isSyncSetup()) {
return { success: false, message: 'Sync not configured' }; return { success: false, message: 'Sync not configured' };
} }
@ -87,13 +87,13 @@ async function login() {
await setupService.sendSeedToSyncServer(); await setupService.sendSeedToSyncServer();
} }
return await doLogin(); return doLogin();
} }
async function doLogin() { async function doLogin() {
const timestamp = dateUtils.utcNowDateTime(); const timestamp = dateUtils.utcNowDateTime();
const documentSecret = await optionService.getOption('documentSecret'); const documentSecret = optionService.getOption('documentSecret');
const hash = utils.hmac(documentSecret, timestamp); const hash = utils.hmac(documentSecret, timestamp);
const syncContext = { cookieJar: {} }; const syncContext = { cookieJar: {} };
@ -109,14 +109,14 @@ async function doLogin() {
syncContext.sourceId = resp.sourceId; syncContext.sourceId = resp.sourceId;
const lastSyncedPull = await getLastSyncedPull(); const lastSyncedPull = getLastSyncedPull();
// this is important in a scenario where we setup the sync by manually copying the document // this is important in a scenario where we setup the sync by manually copying the document
// lastSyncedPull then could be pretty off for the newly cloned client // lastSyncedPull then could be pretty off for the newly cloned client
if (lastSyncedPull > resp.maxSyncId) { if (lastSyncedPull > resp.maxSyncId) {
log.info(`Lowering last synced pull from ${lastSyncedPull} to ${resp.maxSyncId}`); log.info(`Lowering last synced pull from ${lastSyncedPull} to ${resp.maxSyncId}`);
await setLastSyncedPull(resp.maxSyncId); setLastSyncedPull(resp.maxSyncId);
} }
return syncContext; return syncContext;
@ -126,7 +126,7 @@ async function pullSync(syncContext) {
let appliedPulls = 0; let appliedPulls = 0;
while (true) { while (true) {
const lastSyncedPull = await getLastSyncedPull(); const lastSyncedPull = getLastSyncedPull();
const changesUri = '/api/sync/changed?lastSyncId=' + lastSyncedPull; const changesUri = '/api/sync/changed?lastSyncId=' + lastSyncedPull;
const startDate = Date.now(); const startDate = Date.now();
@ -144,7 +144,7 @@ async function pullSync(syncContext) {
break; break;
} }
await sql.transactional(async () => { sql.transactional(() => {
for (const {sync, entity} of rows) { for (const {sync, entity} of rows) {
if (!sourceIdService.isLocalSourceId(sync.sourceId)) { if (!sourceIdService.isLocalSourceId(sync.sourceId)) {
if (appliedPulls === 0 && sync.entity !== 'recent_notes') { // send only for first if (appliedPulls === 0 && sync.entity !== 'recent_notes') { // send only for first
@ -153,13 +153,13 @@ async function pullSync(syncContext) {
appliedPulls++; appliedPulls++;
} }
await syncUpdateService.updateEntity(sync, entity, syncContext.sourceId); syncUpdateService.updateEntity(sync, entity, syncContext.sourceId);
} }
stats.outstandingPulls = resp.maxSyncId - sync.id; stats.outstandingPulls = resp.maxSyncId - sync.id;
} }
await setLastSyncedPull(rows[rows.length - 1].sync.id); setLastSyncedPull(rows[rows.length - 1].sync.id);
}); });
log.info(`Pulled and updated ${rows.length} changes from ${changesUri} in ${Date.now() - startDate}ms`); log.info(`Pulled and updated ${rows.length} changes from ${changesUri} in ${Date.now() - startDate}ms`);
@ -173,10 +173,10 @@ async function pullSync(syncContext) {
} }
async function pushSync(syncContext) { async function pushSync(syncContext) {
let lastSyncedPush = await getLastSyncedPush(); let lastSyncedPush = getLastSyncedPush();
while (true) { while (true) {
const syncs = await sql.getRows('SELECT * FROM sync WHERE isSynced = 1 AND id > ? LIMIT 1000', [lastSyncedPush]); const syncs = sql.getRows('SELECT * FROM sync WHERE isSynced = 1 AND id > ? LIMIT 1000', [lastSyncedPush]);
if (syncs.length === 0) { if (syncs.length === 0) {
log.info("Nothing to push"); log.info("Nothing to push");
@ -201,12 +201,12 @@ async function pushSync(syncContext) {
if (filteredSyncs.length === 0) { if (filteredSyncs.length === 0) {
// there still might be more syncs (because of batch limit), just all from current batch // there still might be more syncs (because of batch limit), just all from current batch
// has been filtered out // has been filtered out
await setLastSyncedPush(lastSyncedPush); setLastSyncedPush(lastSyncedPush);
continue; continue;
} }
const syncRecords = await getSyncRecords(filteredSyncs); const syncRecords = getSyncRecords(filteredSyncs);
const startDate = new Date(); const startDate = new Date();
await syncRequest(syncContext, 'PUT', '/api/sync/update', { await syncRequest(syncContext, 'PUT', '/api/sync/update', {
@ -218,7 +218,7 @@ async function pushSync(syncContext) {
lastSyncedPush = syncRecords[syncRecords.length - 1].sync.id; lastSyncedPush = syncRecords[syncRecords.length - 1].sync.id;
await setLastSyncedPush(lastSyncedPush); setLastSyncedPush(lastSyncedPush);
} }
} }
@ -228,7 +228,7 @@ async function syncFinished(syncContext) {
async function checkContentHash(syncContext) { async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
const lastSyncedPullId = await getLastSyncedPull(); const lastSyncedPullId = getLastSyncedPull();
if (lastSyncedPullId < resp.maxSyncId) { if (lastSyncedPullId < resp.maxSyncId) {
log.info(`There are some outstanding pulls (${lastSyncedPullId} vs. ${resp.maxSyncId}), skipping content check.`); log.info(`There are some outstanding pulls (${lastSyncedPullId} vs. ${resp.maxSyncId}), skipping content check.`);
@ -236,7 +236,7 @@ async function checkContentHash(syncContext) {
return true; return true;
} }
const notPushedSyncs = await sql.getValue("SELECT EXISTS(SELECT 1 FROM sync WHERE isSynced = 1 AND id > ?)", [await getLastSyncedPush()]); const notPushedSyncs = sql.getValue("SELECT EXISTS(SELECT 1 FROM sync WHERE isSynced = 1 AND id > ?)", [getLastSyncedPush()]);
if (notPushedSyncs) { if (notPushedSyncs) {
log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`); log.info(`There's ${notPushedSyncs} outstanding pushes, skipping content check.`);
@ -244,12 +244,12 @@ async function checkContentHash(syncContext) {
return true; return true;
} }
const failedChecks = await contentHashService.checkContentHashes(resp.entityHashes); const failedChecks = contentHashService.checkContentHashes(resp.entityHashes);
for (const {entityName, sector} of failedChecks) { for (const {entityName, sector} of failedChecks) {
const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName; const entityPrimaryKey = entityConstructor.getEntityFromEntityName(entityName).primaryKeyName;
await syncTableService.addEntitySyncsForSector(entityName, entityPrimaryKey, sector); syncTableService.addEntitySyncsForSector(entityName, entityPrimaryKey, sector);
await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${entityName}/${sector}`); await syncRequest(syncContext, 'POST', `/api/sync/queue-sector/${entityName}/${sector}`);
} }
@ -257,19 +257,19 @@ async function checkContentHash(syncContext) {
return failedChecks.length > 0; return failedChecks.length > 0;
} }
async function syncRequest(syncContext, method, requestPath, body) { function syncRequest(syncContext, method, requestPath, body) {
const timeout = await syncOptions.getSyncTimeout(); const timeout = syncOptions.getSyncTimeout();
const opts = { const opts = {
method, method,
url: await syncOptions.getSyncServerHost() + requestPath, url: syncOptions.getSyncServerHost() + requestPath,
cookieJar: syncContext.cookieJar, cookieJar: syncContext.cookieJar,
timeout: timeout, timeout: timeout,
body, body,
proxy: proxyToggle ? await syncOptions.getSyncProxy() : null proxy: proxyToggle ? syncOptions.getSyncProxy() : null
}; };
return await utils.timeLimit(request.exec(opts), timeout); return utils.timeLimit(request.exec(opts), timeout);
} }
const primaryKeys = { const primaryKeys = {
@ -284,9 +284,9 @@ const primaryKeys = {
"attributes": "attributeId" "attributes": "attributeId"
}; };
async function getEntityRow(entityName, entityId) { function getEntityRow(entityName, entityId) {
if (entityName === 'note_reordering') { if (entityName === 'note_reordering') {
return await sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [entityId]); return sql.getMap("SELECT branchId, notePosition FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [entityId]);
} }
else { else {
const primaryKey = primaryKeys[entityName]; const primaryKey = primaryKeys[entityName];
@ -295,7 +295,7 @@ async function getEntityRow(entityName, entityId) {
throw new Error("Unknown entity " + entityName); throw new Error("Unknown entity " + entityName);
} }
const entity = await sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]); const entity = sql.getRow(`SELECT * FROM ${entityName} WHERE ${primaryKey} = ?`, [entityId]);
if (!entity) { if (!entity) {
throw new Error(`Entity ${entityName} ${entityId} not found.`); throw new Error(`Entity ${entityName} ${entityId} not found.`);
@ -313,12 +313,12 @@ async function getEntityRow(entityName, entityId) {
} }
} }
async function getSyncRecords(syncs) { function getSyncRecords(syncs) {
const records = []; const records = [];
let length = 0; let length = 0;
for (const sync of syncs) { for (const sync of syncs) {
const entity = await getEntityRow(sync.entityName, sync.entityId); const entity = getEntityRow(sync.entityName, sync.entityId);
if (sync.entityName === 'options' && !entity.isSynced) { if (sync.entityName === 'options' && !entity.isSynced) {
records.push({sync}); records.push({sync});
@ -340,42 +340,40 @@ async function getSyncRecords(syncs) {
return records; return records;
} }
async function getLastSyncedPull() { function getLastSyncedPull() {
return parseInt(await optionService.getOption('lastSyncedPull')); return parseInt(optionService.getOption('lastSyncedPull'));
} }
async function setLastSyncedPull(syncId) { function setLastSyncedPull(syncId) {
await optionService.setOption('lastSyncedPull', syncId); optionService.setOption('lastSyncedPull', syncId);
} }
async function getLastSyncedPush() { function getLastSyncedPush() {
return parseInt(await optionService.getOption('lastSyncedPush')); return parseInt(optionService.getOption('lastSyncedPush'));
} }
async function setLastSyncedPush(lastSyncedPush) { function setLastSyncedPush(lastSyncedPush) {
await optionService.setOption('lastSyncedPush', lastSyncedPush); optionService.setOption('lastSyncedPush', lastSyncedPush);
} }
async function updatePushStats() { function updatePushStats() {
if (await syncOptions.isSyncSetup()) { if (syncOptions.isSyncSetup()) {
const lastSyncedPush = await optionService.getOption('lastSyncedPush'); const lastSyncedPush = optionService.getOption('lastSyncedPush');
stats.outstandingPushes = await sql.getValue("SELECT COUNT(1) FROM sync WHERE isSynced = 1 AND id > ?", [lastSyncedPush]); stats.outstandingPushes = sql.getValue("SELECT COUNT(1) FROM sync WHERE isSynced = 1 AND id > ?", [lastSyncedPush]);
} }
} }
async function getMaxSyncId() { function getMaxSyncId() {
return await sql.getValue('SELECT MAX(id) FROM sync'); return sql.getValue('SELECT MAX(id) FROM sync');
} }
sqlInit.dbReady.then(async () => { setInterval(cls.wrap(sync), 60000);
setInterval(cls.wrap(sync), 60000);
// kickoff initial sync immediately // kickoff initial sync immediately
setTimeout(cls.wrap(sync), 3000); setTimeout(cls.wrap(sync), 3000);
setInterval(cls.wrap(updatePushStats), 1000); setInterval(cls.wrap(updatePushStats), 1000);
});
module.exports = { module.exports = {
sync, sync,

View File

@ -10,7 +10,7 @@ async function doExclusively(func) {
const releaseMutex = await instance.acquire(); const releaseMutex = await instance.acquire();
try { try {
return await func(); return func();
} }
finally { finally {
releaseMutex(); releaseMutex();

View File

@ -10,19 +10,19 @@ const config = require('./config');
* to live sync server. * to live sync server.
*/ */
async function get(name) { function get(name) {
return (config['Sync'] && config['Sync'][name]) || await optionService.getOption(name); return (config['Sync'] && config['Sync'][name]) || optionService.getOption(name);
} }
module.exports = { module.exports = {
getSyncServerHost: async () => await get('syncServerHost'), getSyncServerHost: () => get('syncServerHost'),
isSyncSetup: async () => { isSyncSetup: () => {
const syncServerHost = await get('syncServerHost'); const syncServerHost = get('syncServerHost');
// special value "disabled" is here to support use case where document is configured with sync server // special value "disabled" is here to support use case where document is configured with sync server
// and we need to override it with config from config.ini // and we need to override it with config from config.ini
return !!syncServerHost && syncServerHost !== 'disabled'; return !!syncServerHost && syncServerHost !== 'disabled';
}, },
getSyncTimeout: async () => parseInt(await get('syncServerTimeout')) || 60000, getSyncTimeout: () => parseInt(get('syncServerTimeout')) || 60000,
getSyncProxy: async () => await get('syncProxy') getSyncProxy: () => get('syncProxy')
}; };

View File

@ -10,7 +10,7 @@ const cls = require('./cls');
let maxSyncId = 0; let maxSyncId = 0;
async function insertEntitySync(entityName, entityId, sourceId = null, isSynced = true) { function insertEntitySync(entityName, entityId, sourceId = null, isSynced = true) {
const sync = { const sync = {
entityName: entityName, entityName: entityName,
entityId: entityId, entityId: entityId,
@ -19,66 +19,66 @@ async function insertEntitySync(entityName, entityId, sourceId = null, isSynced
isSynced: isSynced ? 1 : 0 isSynced: isSynced ? 1 : 0
}; };
sync.id = await sql.replace("sync", sync); sync.id = sql.replace("sync", sync);
maxSyncId = Math.max(maxSyncId, sync.id); maxSyncId = Math.max(maxSyncId, sync.id);
return sync; return sync;
} }
async function addEntitySync(entityName, entityId, sourceId, isSynced) { function addEntitySync(entityName, entityId, sourceId, isSynced) {
const sync = await insertEntitySync(entityName, entityId, sourceId, isSynced); const sync = insertEntitySync(entityName, entityId, sourceId, isSynced);
cls.addSyncRow(sync); cls.addSyncRow(sync);
} }
async function addEntitySyncsForSector(entityName, entityPrimaryKey, sector) { function addEntitySyncsForSector(entityName, entityPrimaryKey, sector) {
const startTime = Date.now(); const startTime = Date.now();
await sql.transactional(async () => { sql.transactional(() => {
const entityIds = await sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName} WHERE SUBSTR(${entityPrimaryKey}, 1, 1) = ?`, [sector]); const entityIds = sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName} WHERE SUBSTR(${entityPrimaryKey}, 1, 1) = ?`, [sector]);
for (const entityId of entityIds) { for (const entityId of entityIds) {
if (entityName === 'options') { if (entityName === 'options') {
const isSynced = await sql.getValue(`SELECT isSynced FROM options WHERE name = ?`, [entityId]); const isSynced = sql.getValue(`SELECT isSynced FROM options WHERE name = ?`, [entityId]);
if (!isSynced) { if (!isSynced) {
continue; continue;
} }
} }
await insertEntitySync(entityName, entityId, 'content-check', true); insertEntitySync(entityName, entityId, 'content-check', true);
} }
}); });
log.info(`Added sector ${sector} of ${entityName} to sync queue in ${Date.now() - startTime}ms.`); log.info(`Added sector ${sector} of ${entityName} to sync queue in ${Date.now() - startTime}ms.`);
} }
async function cleanupSyncRowsForMissingEntities(entityName, entityPrimaryKey) { function cleanupSyncRowsForMissingEntities(entityName, entityPrimaryKey) {
await sql.execute(` sql.execute(`
DELETE DELETE
FROM sync FROM sync
WHERE sync.entityName = '${entityName}' WHERE sync.entityName = '${entityName}'
AND sync.entityId NOT IN (SELECT ${entityPrimaryKey} FROM ${entityName})`); AND sync.entityId NOT IN (SELECT ${entityPrimaryKey} FROM ${entityName})`);
} }
async function fillSyncRows(entityName, entityPrimaryKey, condition = '') { function fillSyncRows(entityName, entityPrimaryKey, condition = '') {
try { try {
await cleanupSyncRowsForMissingEntities(entityName, entityPrimaryKey); cleanupSyncRowsForMissingEntities(entityName, entityPrimaryKey);
const entityIds = await sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName}` const entityIds = sql.getColumn(`SELECT ${entityPrimaryKey} FROM ${entityName}`
+ (condition ? ` WHERE ${condition}` : '')); + (condition ? ` WHERE ${condition}` : ''));
let createdCount = 0; let createdCount = 0;
for (const entityId of entityIds) { for (const entityId of entityIds) {
const existingRows = await sql.getValue("SELECT COUNT(1) FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]); const existingRows = sql.getValue("SELECT COUNT(1) FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]);
// we don't want to replace existing entities (which would effectively cause full resync) // we don't want to replace existing entities (which would effectively cause full resync)
if (existingRows === 0) { if (existingRows === 0) {
createdCount++; createdCount++;
await sql.insert("sync", { sql.insert("sync", {
entityName: entityName, entityName: entityName,
entityId: entityId, entityId: entityId,
sourceId: "SYNC_FILL", sourceId: "SYNC_FILL",
@ -98,31 +98,31 @@ async function fillSyncRows(entityName, entityPrimaryKey, condition = '') {
} }
} }
async function fillAllSyncRows() { function fillAllSyncRows() {
await sql.execute("DELETE FROM sync"); sql.execute("DELETE FROM sync");
await fillSyncRows("notes", "noteId"); fillSyncRows("notes", "noteId");
await fillSyncRows("note_contents", "noteId"); fillSyncRows("note_contents", "noteId");
await fillSyncRows("branches", "branchId"); fillSyncRows("branches", "branchId");
await fillSyncRows("note_revisions", "noteRevisionId"); fillSyncRows("note_revisions", "noteRevisionId");
await fillSyncRows("note_revision_contents", "noteRevisionId"); fillSyncRows("note_revision_contents", "noteRevisionId");
await fillSyncRows("recent_notes", "noteId"); fillSyncRows("recent_notes", "noteId");
await fillSyncRows("attributes", "attributeId"); fillSyncRows("attributes", "attributeId");
await fillSyncRows("api_tokens", "apiTokenId"); fillSyncRows("api_tokens", "apiTokenId");
await fillSyncRows("options", "name", 'isSynced = 1'); fillSyncRows("options", "name", 'isSynced = 1');
} }
module.exports = { module.exports = {
addNoteSync: async (noteId, sourceId) => await addEntitySync("notes", noteId, sourceId), addNoteSync: (noteId, sourceId) => addEntitySync("notes", noteId, sourceId),
addNoteContentSync: async (noteId, sourceId) => await addEntitySync("note_contents", noteId, sourceId), addNoteContentSync: (noteId, sourceId) => addEntitySync("note_contents", noteId, sourceId),
addBranchSync: async (branchId, sourceId) => await addEntitySync("branches", branchId, sourceId), addBranchSync: (branchId, sourceId) => addEntitySync("branches", branchId, sourceId),
addNoteReorderingSync: async (parentNoteId, sourceId) => await addEntitySync("note_reordering", parentNoteId, sourceId), addNoteReorderingSync: (parentNoteId, sourceId) => addEntitySync("note_reordering", parentNoteId, sourceId),
addNoteRevisionSync: async (noteRevisionId, sourceId) => await addEntitySync("note_revisions", noteRevisionId, sourceId), addNoteRevisionSync: (noteRevisionId, sourceId) => addEntitySync("note_revisions", noteRevisionId, sourceId),
addNoteRevisionContentSync: async (noteRevisionId, sourceId) => await addEntitySync("note_revision_contents", noteRevisionId, sourceId), addNoteRevisionContentSync: (noteRevisionId, sourceId) => addEntitySync("note_revision_contents", noteRevisionId, sourceId),
addOptionsSync: async (name, sourceId, isSynced) => await addEntitySync("options", name, sourceId, isSynced), addOptionsSync: (name, sourceId, isSynced) => addEntitySync("options", name, sourceId, isSynced),
addRecentNoteSync: async (noteId, sourceId) => await addEntitySync("recent_notes", noteId, sourceId), addRecentNoteSync: (noteId, sourceId) => addEntitySync("recent_notes", noteId, sourceId),
addAttributeSync: async (attributeId, sourceId) => await addEntitySync("attributes", attributeId, sourceId), addAttributeSync: (attributeId, sourceId) => addEntitySync("attributes", attributeId, sourceId),
addApiTokenSync: async (apiTokenId, sourceId) => await addEntitySync("api_tokens", apiTokenId, sourceId), addApiTokenSync: (apiTokenId, sourceId) => addEntitySync("api_tokens", apiTokenId, sourceId),
addEntitySync, addEntitySync,
fillAllSyncRows, fillAllSyncRows,
addEntitySyncsForSector, addEntitySyncsForSector,

View File

@ -3,7 +3,7 @@ const log = require('./log');
const syncTableService = require('./sync_table'); const syncTableService = require('./sync_table');
const eventService = require('./events'); const eventService = require('./events');
async function updateEntity(sync, entity, sourceId) { function updateEntity(sync, entity, sourceId) {
// can be undefined for options with isSynced=false // can be undefined for options with isSynced=false
if (!entity) { if (!entity) {
return false; return false;
@ -13,34 +13,34 @@ async function updateEntity(sync, entity, sourceId) {
let updated; let updated;
if (entityName === 'notes') { if (entityName === 'notes') {
updated = await updateNote(entity, sourceId); updated = updateNote(entity, sourceId);
} }
else if (entityName === 'note_contents') { else if (entityName === 'note_contents') {
updated = await updateNoteContent(entity, sourceId); updated = updateNoteContent(entity, sourceId);
} }
else if (entityName === 'branches') { else if (entityName === 'branches') {
updated = await updateBranch(entity, sourceId); updated = updateBranch(entity, sourceId);
} }
else if (entityName === 'note_revisions') { else if (entityName === 'note_revisions') {
updated = await updateNoteRevision(entity, sourceId); updated = updateNoteRevision(entity, sourceId);
} }
else if (entityName === 'note_revision_contents') { else if (entityName === 'note_revision_contents') {
updated = await updateNoteRevisionContent(entity, sourceId); updated = updateNoteRevisionContent(entity, sourceId);
} }
else if (entityName === 'note_reordering') { else if (entityName === 'note_reordering') {
updated = await updateNoteReordering(sync.entityId, entity, sourceId); updated = updateNoteReordering(sync.entityId, entity, sourceId);
} }
else if (entityName === 'options') { else if (entityName === 'options') {
updated = await updateOptions(entity, sourceId); updated = updateOptions(entity, sourceId);
} }
else if (entityName === 'recent_notes') { else if (entityName === 'recent_notes') {
updated = await updateRecentNotes(entity, sourceId); updated = updateRecentNotes(entity, sourceId);
} }
else if (entityName === 'attributes') { else if (entityName === 'attributes') {
updated = await updateAttribute(entity, sourceId); updated = updateAttribute(entity, sourceId);
} }
else if (entityName === 'api_tokens') { else if (entityName === 'api_tokens') {
updated = await updateApiToken(entity, sourceId); updated = updateApiToken(entity, sourceId);
} }
else { else {
throw new Error(`Unrecognized entity type ${entityName}`); throw new Error(`Unrecognized entity type ${entityName}`);
@ -50,7 +50,7 @@ async function updateEntity(sync, entity, sourceId) {
// the title and content are not available decrypted as listeners would expect // the title and content are not available decrypted as listeners would expect
if (updated && if (updated &&
(!['notes', 'note_contents', 'note_revisions', 'note_revision_contents'].includes(entityName) || !entity.isProtected)) { (!['notes', 'note_contents', 'note_revisions', 'note_revision_contents'].includes(entityName) || !entity.isProtected)) {
await eventService.emit(eventService.ENTITY_SYNCED, { eventService.emit(eventService.ENTITY_SYNCED, {
entityName, entityName,
entity entity
}); });
@ -79,14 +79,14 @@ function shouldWeUpdateEntity(localEntity, remoteEntity) {
return false; return false;
} }
async function updateNote(remoteEntity, sourceId) { function updateNote(remoteEntity, sourceId) {
const localEntity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [remoteEntity.noteId]); const localEntity = sql.getRow("SELECT * FROM notes WHERE noteId = ?", [remoteEntity.noteId]);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) { if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => { sql.transactional(() => {
await sql.replace("notes", remoteEntity); sql.replace("notes", remoteEntity);
await syncTableService.addNoteSync(remoteEntity.noteId, sourceId); syncTableService.addNoteSync(remoteEntity.noteId, sourceId);
}); });
return true; return true;
@ -95,16 +95,16 @@ async function updateNote(remoteEntity, sourceId) {
return false; return false;
} }
async function updateNoteContent(remoteEntity, sourceId) { function updateNoteContent(remoteEntity, sourceId) {
const localEntity = await sql.getRow("SELECT * FROM note_contents WHERE noteId = ?", [remoteEntity.noteId]); const localEntity = sql.getRow("SELECT * FROM note_contents WHERE noteId = ?", [remoteEntity.noteId]);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) { if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
remoteEntity.content = remoteEntity.content === null ? null : Buffer.from(remoteEntity.content, 'base64'); remoteEntity.content = remoteEntity.content === null ? null : Buffer.from(remoteEntity.content, 'base64');
await sql.transactional(async () => { sql.transactional(() => {
await sql.replace("note_contents", remoteEntity); sql.replace("note_contents", remoteEntity);
await syncTableService.addNoteContentSync(remoteEntity.noteId, sourceId); syncTableService.addNoteContentSync(remoteEntity.noteId, sourceId);
}); });
return true; return true;
@ -113,20 +113,20 @@ async function updateNoteContent(remoteEntity, sourceId) {
return false; return false;
} }
async function updateBranch(remoteEntity, sourceId) { function updateBranch(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM branches WHERE branchId = ?", [remoteEntity.branchId]); const localEntity = sql.getRowOrNull("SELECT * FROM branches WHERE branchId = ?", [remoteEntity.branchId]);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) { if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => { sql.transactional(() => {
// isExpanded is not synced unless it's a new branch instance // isExpanded is not synced unless it's a new branch instance
// otherwise in case of full new sync we'll get all branches (even root) collapsed. // otherwise in case of full new sync we'll get all branches (even root) collapsed.
if (localEntity) { if (localEntity) {
delete remoteEntity.isExpanded; delete remoteEntity.isExpanded;
} }
await sql.replace('branches', remoteEntity); sql.replace('branches', remoteEntity);
await syncTableService.addBranchSync(remoteEntity.branchId, sourceId); syncTableService.addBranchSync(remoteEntity.branchId, sourceId);
}); });
return true; return true;
@ -135,30 +135,30 @@ async function updateBranch(remoteEntity, sourceId) {
return false; return false;
} }
async function updateNoteRevision(remoteEntity, sourceId) { function updateNoteRevision(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [remoteEntity.noteRevisionId]); const localEntity = sql.getRowOrNull("SELECT * FROM note_revisions WHERE noteRevisionId = ?", [remoteEntity.noteRevisionId]);
await sql.transactional(async () => { sql.transactional(() => {
if (shouldWeUpdateEntity(localEntity, remoteEntity)) { if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.replace('note_revisions', remoteEntity); sql.replace('note_revisions', remoteEntity);
await syncTableService.addNoteRevisionSync(remoteEntity.noteRevisionId, sourceId); syncTableService.addNoteRevisionSync(remoteEntity.noteRevisionId, sourceId);
log.info("Update/sync note revision " + remoteEntity.noteRevisionId); log.info("Update/sync note revision " + remoteEntity.noteRevisionId);
} }
}); });
} }
async function updateNoteRevisionContent(remoteEntity, sourceId) { function updateNoteRevisionContent(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM note_revision_contents WHERE noteRevisionId = ?", [remoteEntity.noteRevisionId]); const localEntity = sql.getRowOrNull("SELECT * FROM note_revision_contents WHERE noteRevisionId = ?", [remoteEntity.noteRevisionId]);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) { if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => { sql.transactional(() => {
remoteEntity.content = remoteEntity.content === null ? null : Buffer.from(remoteEntity.content, 'base64'); remoteEntity.content = remoteEntity.content === null ? null : Buffer.from(remoteEntity.content, 'base64');
await sql.replace('note_revision_contents', remoteEntity); sql.replace('note_revision_contents', remoteEntity);
await syncTableService.addNoteRevisionContentSync(remoteEntity.noteRevisionId, sourceId); syncTableService.addNoteRevisionContentSync(remoteEntity.noteRevisionId, sourceId);
}); });
return true; return true;
@ -167,30 +167,30 @@ async function updateNoteRevisionContent(remoteEntity, sourceId) {
return false; return false;
} }
async function updateNoteReordering(entityId, remote, sourceId) { function updateNoteReordering(entityId, remote, sourceId) {
await sql.transactional(async () => { sql.transactional(() => {
for (const key in remote) { for (const key in remote) {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remote[key], key]); sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [remote[key], key]);
} }
await syncTableService.addNoteReorderingSync(entityId, sourceId); syncTableService.addNoteReorderingSync(entityId, sourceId);
}); });
return true; return true;
} }
async function updateOptions(remoteEntity, sourceId) { function updateOptions(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM options WHERE name = ?", [remoteEntity.name]); const localEntity = sql.getRowOrNull("SELECT * FROM options WHERE name = ?", [remoteEntity.name]);
if (localEntity && !localEntity.isSynced) { if (localEntity && !localEntity.isSynced) {
return; return;
} }
if (shouldWeUpdateEntity(localEntity, remoteEntity)) { if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => { sql.transactional(() => {
await sql.replace('options', remoteEntity); sql.replace('options', remoteEntity);
await syncTableService.addOptionsSync(remoteEntity.name, sourceId, true); syncTableService.addOptionsSync(remoteEntity.name, sourceId, true);
}); });
return true; return true;
@ -199,14 +199,14 @@ async function updateOptions(remoteEntity, sourceId) {
return false; return false;
} }
async function updateRecentNotes(remoteEntity, sourceId) { function updateRecentNotes(remoteEntity, sourceId) {
const localEntity = await sql.getRowOrNull("SELECT * FROM recent_notes WHERE noteId = ?", [remoteEntity.noteId]); const localEntity = sql.getRowOrNull("SELECT * FROM recent_notes WHERE noteId = ?", [remoteEntity.noteId]);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) { if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => { sql.transactional(() => {
await sql.replace('recent_notes', remoteEntity); sql.replace('recent_notes', remoteEntity);
await syncTableService.addRecentNoteSync(remoteEntity.noteId, sourceId); syncTableService.addRecentNoteSync(remoteEntity.noteId, sourceId);
}); });
return true; return true;
@ -215,14 +215,14 @@ async function updateRecentNotes(remoteEntity, sourceId) {
return false; return false;
} }
async function updateAttribute(remoteEntity, sourceId) { function updateAttribute(remoteEntity, sourceId) {
const localEntity = await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [remoteEntity.attributeId]); const localEntity = sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [remoteEntity.attributeId]);
if (shouldWeUpdateEntity(localEntity, remoteEntity)) { if (shouldWeUpdateEntity(localEntity, remoteEntity)) {
await sql.transactional(async () => { sql.transactional(() => {
await sql.replace("attributes", remoteEntity); sql.replace("attributes", remoteEntity);
await syncTableService.addAttributeSync(remoteEntity.attributeId, sourceId); syncTableService.addAttributeSync(remoteEntity.attributeId, sourceId);
}); });
return true; return true;
@ -231,14 +231,14 @@ async function updateAttribute(remoteEntity, sourceId) {
return false; return false;
} }
async function updateApiToken(entity, sourceId) { function updateApiToken(entity, sourceId) {
const apiTokenId = await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]); const apiTokenId = sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]);
if (!apiTokenId) { if (!apiTokenId) {
await sql.transactional(async () => { sql.transactional(() => {
await sql.replace("api_tokens", entity); sql.replace("api_tokens", entity);
await syncTableService.addApiTokenSync(entity.apiTokenId, sourceId); syncTableService.addApiTokenSync(entity.apiTokenId, sourceId);
}); });
return true; return true;

View File

@ -7,9 +7,9 @@ const syncTableService = require('./sync_table');
const protectedSessionService = require('./protected_session'); const protectedSessionService = require('./protected_session');
const noteCacheService = require('./note_cache/note_cache.js'); const noteCacheService = require('./note_cache/note_cache.js');
async function getNotes(noteIds) { function getNotes(noteIds) {
// we return also deleted notes which have been specifically asked for // we return also deleted notes which have been specifically asked for
const notes = await sql.getManyRows(` const notes = sql.getManyRows(`
SELECT SELECT
noteId, noteId,
title, title,
@ -23,7 +23,7 @@ async function getNotes(noteIds) {
protectedSessionService.decryptNotes(notes); protectedSessionService.decryptNotes(notes);
await noteCacheService.loadedPromise; noteCacheService.loadedPromise;
notes.forEach(note => { notes.forEach(note => {
note.isProtected = !!note.isProtected note.isProtected = !!note.isProtected
@ -32,7 +32,7 @@ async function getNotes(noteIds) {
return notes; return notes;
} }
async function validateParentChild(parentNoteId, childNoteId, branchId = null) { function validateParentChild(parentNoteId, childNoteId, branchId = null) {
if (childNoteId === 'root') { if (childNoteId === 'root') {
return { success: false, message: 'Cannot move root note.'}; return { success: false, message: 'Cannot move root note.'};
} }
@ -42,7 +42,7 @@ async function validateParentChild(parentNoteId, childNoteId, branchId = null) {
return { success: false, message: 'Cannot move anything into root parent.' }; return { success: false, message: 'Cannot move anything into root parent.' };
} }
const existing = await getExistingBranch(parentNoteId, childNoteId); const existing = getExistingBranch(parentNoteId, childNoteId);
if (existing && (branchId === null || existing.branchId !== branchId)) { if (existing && (branchId === null || existing.branchId !== branchId)) {
return { return {
@ -51,7 +51,7 @@ async function validateParentChild(parentNoteId, childNoteId, branchId = null) {
}; };
} }
if (!await checkTreeCycle(parentNoteId, childNoteId)) { if (!checkTreeCycle(parentNoteId, childNoteId)) {
return { return {
success: false, success: false,
message: 'Moving/cloning note here would create cycle.' message: 'Moving/cloning note here would create cycle.'
@ -61,20 +61,20 @@ async function validateParentChild(parentNoteId, childNoteId, branchId = null) {
return { success: true }; return { success: true };
} }
async function getExistingBranch(parentNoteId, childNoteId) { function getExistingBranch(parentNoteId, childNoteId) {
return await repository.getEntity('SELECT * FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0', [childNoteId, parentNoteId]); return repository.getEntity('SELECT * FROM branches WHERE noteId = ? AND parentNoteId = ? AND isDeleted = 0', [childNoteId, parentNoteId]);
} }
/** /**
* Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases. * Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases.
*/ */
async function checkTreeCycle(parentNoteId, childNoteId) { function checkTreeCycle(parentNoteId, childNoteId) {
const subtreeNoteIds = []; const subtreeNoteIds = [];
// we'll load the whole sub tree - because the cycle can start in one of the notes in the sub tree // we'll load the whole sub tree - because the cycle can start in one of the notes in the sub tree
await loadSubtreeNoteIds(childNoteId, subtreeNoteIds); loadSubtreeNoteIds(childNoteId, subtreeNoteIds);
async function checkTreeCycleInner(parentNoteId) { function checkTreeCycleInner(parentNoteId) {
if (parentNoteId === 'root') { if (parentNoteId === 'root') {
return true; return true;
} }
@ -85,10 +85,10 @@ async function checkTreeCycle(parentNoteId, childNoteId) {
return false; return false;
} }
const parentNoteIds = await sql.getColumn("SELECT DISTINCT parentNoteId FROM branches WHERE noteId = ? AND isDeleted = 0", [parentNoteId]); const parentNoteIds = sql.getColumn("SELECT DISTINCT parentNoteId FROM branches WHERE noteId = ? AND isDeleted = 0", [parentNoteId]);
for (const pid of parentNoteIds) { for (const pid of parentNoteIds) {
if (!await checkTreeCycleInner(pid)) { if (!checkTreeCycleInner(pid)) {
return false; return false;
} }
} }
@ -96,22 +96,22 @@ async function checkTreeCycle(parentNoteId, childNoteId) {
return true; return true;
} }
return await checkTreeCycleInner(parentNoteId); return checkTreeCycleInner(parentNoteId);
} }
async function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) { function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) {
subtreeNoteIds.push(parentNoteId); subtreeNoteIds.push(parentNoteId);
const children = await sql.getColumn("SELECT noteId FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]); const children = sql.getColumn("SELECT noteId FROM branches WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]);
for (const childNoteId of children) { for (const childNoteId of children) {
await loadSubtreeNoteIds(childNoteId, subtreeNoteIds); loadSubtreeNoteIds(childNoteId, subtreeNoteIds);
} }
} }
async function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) { function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
await sql.transactional(async () => { sql.transactional(() => {
const notes = await sql.getRows( const notes = sql.getRows(
`SELECT branches.branchId, notes.noteId, title, isProtected, `SELECT branches.branchId, notes.noteId, title, isProtected,
CASE WHEN COUNT(childBranches.noteId) > 0 THEN 1 ELSE 0 END AS hasChildren CASE WHEN COUNT(childBranches.noteId) > 0 THEN 1 ELSE 0 END AS hasChildren
FROM notes FROM notes
@ -135,28 +135,28 @@ async function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) {
let position = 10; let position = 10;
for (const note of notes) { for (const note of notes) {
await sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?",
[position, note.branchId]); [position, note.branchId]);
position += 10; position += 10;
} }
await syncTableService.addNoteReorderingSync(parentNoteId); syncTableService.addNoteReorderingSync(parentNoteId);
}); });
} }
/** /**
* @deprecated - this will be removed in the future * @deprecated - this will be removed in the future
*/ */
async function setNoteToParent(noteId, prefix, parentNoteId) { function setNoteToParent(noteId, prefix, parentNoteId) {
const parentNote = await repository.getNote(parentNoteId); const parentNote = repository.getNote(parentNoteId);
if (parentNote && parentNote.isDeleted) { if (parentNote && parentNote.isDeleted) {
throw new Error(`Cannot move note to deleted parent note ${parentNoteId}`); throw new Error(`Cannot move note to deleted parent note ${parentNoteId}`);
} }
// case where there might be more such branches is ignored. It's expected there should be just one // case where there might be more such branches is ignored. It's expected there should be just one
const branch = await repository.getEntity("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ? AND prefix = ?", [noteId, prefix]); const branch = repository.getEntity("SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ? AND prefix = ?", [noteId, prefix]);
if (branch) { if (branch) {
if (!parentNoteId) { if (!parentNoteId) {
@ -167,23 +167,23 @@ async function setNoteToParent(noteId, prefix, parentNoteId) {
branch.prefix = prefix; branch.prefix = prefix;
} }
await branch.save(); branch.save();
} }
else if (parentNoteId) { else if (parentNoteId) {
const note = await repository.getNote(noteId); const note = repository.getNote(noteId);
if (note.isDeleted) { if (note.isDeleted) {
throw new Error(`Cannot create a branch for ${noteId} which is deleted.`); throw new Error(`Cannot create a branch for ${noteId} which is deleted.`);
} }
const branch = await repository.getEntity('SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ? AND parentNoteId = ?', [noteId, parentNoteId]); const branch = repository.getEntity('SELECT * FROM branches WHERE isDeleted = 0 AND noteId = ? AND parentNoteId = ?', [noteId, parentNoteId]);
if (branch) { if (branch) {
branch.prefix = prefix; branch.prefix = prefix;
await branch.save(); branch.save();
} }
else { else {
await new Branch({ new Branch({
noteId: noteId, noteId: noteId,
parentNoteId: parentNoteId, parentNoteId: parentNoteId,
prefix: prefix prefix: prefix

View File

@ -69,10 +69,10 @@ function prepareSqlForLike(prefix, str, suffix) {
return `'${prefix}${value}${suffix}' ESCAPE '\\'`; return `'${prefix}${value}${suffix}' ESCAPE '\\'`;
} }
async function stopWatch(what, func) { function stopWatch(what, func) {
const start = new Date(); const start = new Date();
const ret = await func(); const ret = func();
const tookMs = Date.now() - start.getTime(); const tookMs = Date.now() - start.getTime();

View File

@ -24,9 +24,9 @@ async function createExtraWindow(notePath) {
webPreferences: { webPreferences: {
enableRemoteModule: true, enableRemoteModule: true,
nodeIntegration: true, nodeIntegration: true,
spellcheck: await optionService.getOptionBool('spellCheckEnabled') spellcheck: optionService.getOptionBool('spellCheckEnabled')
}, },
frame: await optionService.getOptionBool('nativeTitleBarVisible'), frame: optionService.getOptionBool('nativeTitleBarVisible'),
icon: getIcon() icon: getIcon()
}); });
@ -47,7 +47,7 @@ async function createMainWindow() {
defaultHeight: 800 defaultHeight: 800
}); });
const spellcheckEnabled = await optionService.getOptionBool('spellCheckEnabled'); const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
const {BrowserWindow} = require('electron'); // should not be statically imported const {BrowserWindow} = require('electron'); // should not be statically imported
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
@ -61,7 +61,7 @@ async function createMainWindow() {
nodeIntegration: true, nodeIntegration: true,
spellcheck: spellcheckEnabled spellcheck: spellcheckEnabled
}, },
frame: await optionService.getOptionBool('nativeTitleBarVisible'), frame: optionService.getOptionBool('nativeTitleBarVisible'),
icon: getIcon() icon: getIcon()
}); });
@ -91,7 +91,7 @@ async function createMainWindow() {
}); });
if (spellcheckEnabled) { if (spellcheckEnabled) {
const languageCodes = (await optionService.getOption('spellCheckLanguageCode')) const languageCodes = (optionService.getOption('spellCheckLanguageCode'))
.split(',') .split(',')
.map(code => code.trim()); .map(code => code.trim());
@ -127,12 +127,12 @@ function closeSetupWindow() {
} }
} }
async function registerGlobalShortcuts() { function registerGlobalShortcuts() {
const {globalShortcut} = require('electron'); const {globalShortcut} = require('electron');
await sqlInit.dbReady; sqlInit.dbReady;
const allActions = await keyboardActionsService.getKeyboardActions(); const allActions = keyboardActionsService.getKeyboardActions();
for (const action of allActions) { for (const action of allActions) {
if (!action.effectiveShortcuts) { if (!action.effectiveShortcuts) {
@ -143,7 +143,7 @@ async function registerGlobalShortcuts() {
if (shortcut.startsWith('global:')) { if (shortcut.startsWith('global:')) {
const translatedShortcut = shortcut.substr(7); const translatedShortcut = shortcut.substr(7);
const result = globalShortcut.register(translatedShortcut, cls.wrap(async () => { const result = globalShortcut.register(translatedShortcut, cls.wrap(() => {
// window may be hidden / not in focus // window may be hidden / not in focus
mainWindow.focus(); mainWindow.focus();

View File

@ -32,7 +32,7 @@ function init(httpServer, sessionParser) {
console.log(`websocket client connected`); console.log(`websocket client connected`);
ws.on('message', messageJson => { ws.on('message', async messageJson => {
const message = JSON.parse(messageJson); const message = JSON.parse(messageJson);
if (message.type === 'log-error') { if (message.type === 'log-error') {
@ -41,7 +41,7 @@ function init(httpServer, sessionParser) {
else if (message.type === 'ping') { else if (message.type === 'ping') {
lastAcceptedSyncIds[ws.id] = message.lastSyncId; lastAcceptedSyncIds[ws.id] = message.lastSyncId;
syncMutexService.doExclusively(async () => await sendPing(ws)); await syncMutexService.doExclusively(() => sendPing(ws));
} }
else { else {
log.error('Unrecognized message: '); log.error('Unrecognized message: ');
@ -73,36 +73,36 @@ function sendMessageToAllClients(message) {
} }
} }
async function fillInAdditionalProperties(sync) { function fillInAdditionalProperties(sync) {
// fill in some extra data needed by the frontend // fill in some extra data needed by the frontend
if (sync.entityName === 'attributes') { if (sync.entityName === 'attributes') {
sync.entity = await sql.getRow(`SELECT * FROM attributes WHERE attributeId = ?`, [sync.entityId]); sync.entity = sql.getRow(`SELECT * FROM attributes WHERE attributeId = ?`, [sync.entityId]);
} else if (sync.entityName === 'branches') { } else if (sync.entityName === 'branches') {
sync.entity = await sql.getRow(`SELECT * FROM branches WHERE branchId = ?`, [sync.entityId]); sync.entity = sql.getRow(`SELECT * FROM branches WHERE branchId = ?`, [sync.entityId]);
} else if (sync.entityName === 'notes') { } else if (sync.entityName === 'notes') {
sync.entity = await sql.getRow(`SELECT * FROM notes WHERE noteId = ?`, [sync.entityId]); sync.entity = sql.getRow(`SELECT * FROM notes WHERE noteId = ?`, [sync.entityId]);
if (sync.entity.isProtected) { if (sync.entity.isProtected) {
sync.entity.title = protectedSessionService.decryptString(sync.entity.title); sync.entity.title = protectedSessionService.decryptString(sync.entity.title);
} }
} else if (sync.entityName === 'note_revisions') { } else if (sync.entityName === 'note_revisions') {
sync.noteId = await sql.getValue(`SELECT noteId sync.noteId = sql.getValue(`SELECT noteId
FROM note_revisions FROM note_revisions
WHERE noteRevisionId = ?`, [sync.entityId]); WHERE noteRevisionId = ?`, [sync.entityId]);
} else if (sync.entityName === 'note_reordering') { } else if (sync.entityName === 'note_reordering') {
sync.positions = await sql.getMap(`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [sync.entityId]); sync.positions = sql.getMap(`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [sync.entityId]);
} }
else if (sync.entityName === 'options') { else if (sync.entityName === 'options') {
sync.entity = await sql.getRow(`SELECT * FROM options WHERE name = ?`, [sync.entityId]); sync.entity = sql.getRow(`SELECT * FROM options WHERE name = ?`, [sync.entityId]);
} }
} }
async function sendPing(client) { function sendPing(client) {
const syncRows = cls.getSyncRows(); const syncRows = cls.getSyncRows();
for (const sync of syncRows) { for (const sync of syncRows) {
try { try {
await fillInAdditionalProperties(sync); fillInAdditionalProperties(sync);
} }
catch (e) { catch (e) {
log.error("Could not fill additional properties for sync " + JSON.stringify(sync) log.error("Could not fill additional properties for sync " + JSON.stringify(sync)

View File

@ -37,7 +37,7 @@ let httpServer;
async function startTrilium() { async function startTrilium() {
const usedPort = await port; const usedPort = await port;
const usedHost = await host; const usedHost = host;
app.set('port', usedPort); app.set('port', usedPort);
app.set('host', usedHost); app.set('host', usedHost);
@ -98,10 +98,11 @@ async function startTrilium() {
throw error; throw error;
} }
} }
); )
httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port)); httpServer.on('listening', () => debug('Listening on port' + httpServer.address().port));
sqlInit.dbReady.then(() => ws.init(httpServer, sessionParser)); ws.init(httpServer, sessionParser);
if (utils.isElectron()) { if (utils.isElectron()) {
const electronRouting = require('./routes/electron'); const electronRouting = require('./routes/electron');