mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 09:58:32 +02:00
Merge remote-tracking branch 'origin/next49'
This commit is contained in:
commit
8996f35cc0
49
package-lock.json
generated
49
package-lock.json
generated
@ -1896,9 +1896,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001265",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001265.tgz",
|
||||
"integrity": "sha512-YzBnspggWV5hep1m9Z6sZVLOt7vrju8xWooFAgN6BA5qvy98qPAPb7vNUzypFaoh2pb3vlfzbDO8tB57UPGbtw==",
|
||||
"version": "1.0.30001269",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001269.tgz",
|
||||
"integrity": "sha512-UOy8okEVs48MyHYgV+RdW1Oiudl1H6KolybD6ZquD0VcrPSgj25omXO1S7rDydjpqaISCwA8Pyx+jUQKZwWO5w==",
|
||||
"dev": true
|
||||
},
|
||||
"caseless": {
|
||||
@ -2188,6 +2188,12 @@
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||
},
|
||||
"colorette": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz",
|
||||
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
||||
"dev": true
|
||||
},
|
||||
"colors": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
|
||||
@ -3546,9 +3552,9 @@
|
||||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.3.867",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.867.tgz",
|
||||
"integrity": "sha512-WbTXOv7hsLhjJyl7jBfDkioaY++iVVZomZ4dU6TMe/SzucV6mUAs2VZn/AehBwuZMiNEQDaPuTGn22YK5o+aDw==",
|
||||
"version": "1.3.872",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.872.tgz",
|
||||
"integrity": "sha512-qG96atLFY0agKyEETiBFNhpRLSXGSXOBuhXWpbkYqrLKKASpRyRBUtfkn0ZjIf/yXfA7FA4nScVOMpXSHFlUCQ==",
|
||||
"dev": true
|
||||
},
|
||||
"electron-window-state": {
|
||||
@ -4955,9 +4961,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"jest-worker": {
|
||||
"version": "27.2.5",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.2.5.tgz",
|
||||
"integrity": "sha512-HTjEPZtcNKZ4LnhSp02NEH4vE+5OpJ0EsOWYvGQpHgUMLngydESAAMH5Wd/asPf29+XUDQZszxpLg1BkIIA2aw==",
|
||||
"version": "27.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.3.1.tgz",
|
||||
"integrity": "sha512-ks3WCzsiZaOPJl/oMsDjaf0TRiSv7ctNgs0FqRr2nARsovz6AWWy4oLElwcquGSz692DzgZQrCLScPNs5YlC4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
@ -7960,12 +7966,6 @@
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz",
|
||||
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ=="
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
|
||||
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
|
||||
"dev": true
|
||||
},
|
||||
"validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
@ -8032,9 +8032,9 @@
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||
},
|
||||
"webpack": {
|
||||
"version": "5.58.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.58.2.tgz",
|
||||
"integrity": "sha512-3S6e9Vo1W2ijk4F4PPWRIu6D/uGgqaPmqw+av3W3jLDujuNkdxX5h5c+RQ6GkjVR+WwIPOfgY8av+j5j4tMqJw==",
|
||||
"version": "5.59.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.59.0.tgz",
|
||||
"integrity": "sha512-2HiFHKnWIb/cBfOfgssQn8XIRvntISXiz//F1q1+hKMs+uzC1zlVCJZEP7XqI1wzrDyc/ZdB4G+MYtz5biJxCA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/eslint-scope": "^3.7.0",
|
||||
@ -8064,9 +8064,9 @@
|
||||
}
|
||||
},
|
||||
"webpack-cli": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.0.tgz",
|
||||
"integrity": "sha512-n/jZZBMzVEl4PYIBs+auy2WI0WTQ74EnJDiyD98O2JZY6IVIHJNitkYp/uTXOviIOMfgzrNvC9foKv/8o8KSZw==",
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.1.tgz",
|
||||
"integrity": "sha512-JYRFVuyFpzDxMDB+v/nanUdQYcZtqFPGzmlW4s+UkPMFhSpfRNmf1z4AwYcHJVdvEFAM7FFCQdNTpsBYhDLusQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
@ -8080,16 +8080,9 @@
|
||||
"import-local": "^3.0.2",
|
||||
"interpret": "^2.2.0",
|
||||
"rechoir": "^0.7.0",
|
||||
"v8-compile-cache": "^2.2.0",
|
||||
"webpack-merge": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"colorette": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz",
|
||||
"integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==",
|
||||
"dev": true
|
||||
},
|
||||
"commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
|
@ -90,8 +90,8 @@
|
||||
"jsdoc": "3.6.7",
|
||||
"lorem-ipsum": "2.0.4",
|
||||
"rcedit": "3.0.1",
|
||||
"webpack": "5.58.2",
|
||||
"webpack-cli": "4.9.0"
|
||||
"webpack": "5.59.0",
|
||||
"webpack-cli": "4.9.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"electron-installer-debian": "3.1.0"
|
||||
|
@ -3,6 +3,9 @@
|
||||
const sql = require("../services/sql.js");
|
||||
const NoteSet = require("../services/search/note_set");
|
||||
|
||||
/**
|
||||
* Becca is a backend cache of all notes, branches and attributes. There's a similar frontend cache Froca.
|
||||
*/
|
||||
class Becca {
|
||||
constructor() {
|
||||
this.reset();
|
||||
|
@ -29,15 +29,15 @@ function load() {
|
||||
// using raw query and passing arrays to avoid allocating new objects
|
||||
// this is worth it for becca load since it happens every run and blocks the app until finished
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes WHERE isDeleted = 0`, [])) {
|
||||
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime, isProtected, dateCreated, dateModified, utcDateCreated, utcDateModified FROM notes WHERE isDeleted = 0`)) {
|
||||
new Note().update(row).init();
|
||||
}
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified FROM branches WHERE isDeleted = 0`, [])) {
|
||||
for (const row of sql.getRawRows(`SELECT branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified FROM branches WHERE isDeleted = 0`)) {
|
||||
new Branch().update(row).init();
|
||||
}
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified FROM attributes WHERE isDeleted = 0`, [])) {
|
||||
for (const row of sql.getRawRows(`SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified FROM attributes WHERE isDeleted = 0`)) {
|
||||
new Attribute().update(row).init();
|
||||
}
|
||||
|
||||
|
@ -6,12 +6,14 @@ import appContext from "./app_context.js";
|
||||
import NoteComplement from "../entities/note_complement.js";
|
||||
|
||||
/**
|
||||
* Froca keeps a read only cache of note tree structure in frontend's memory.
|
||||
* Froca (FROntend CAche) keeps a read only cache of note tree structure in frontend's memory.
|
||||
* - notes are loaded lazily when unknown noteId is requested
|
||||
* - when note is loaded, all its parent and child branches are loaded as well. For a branch to be used, it's not must be loaded before
|
||||
* - deleted notes are present in the cache as well, but they don't have any branches. As a result check for deleted branch is done by presence check - if the branch is not there even though the corresponding note has been loaded, we can infer it is deleted.
|
||||
*
|
||||
* Note and branch deletions are corner cases and usually not needed.
|
||||
*
|
||||
* Backend has a similar cache called Becca
|
||||
*/
|
||||
class Froca {
|
||||
constructor() {
|
||||
|
81
src/public/stylesheets/share.css
Normal file
81
src/public/stylesheets/share.css
Normal file
@ -0,0 +1,81 @@
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
*, *:before, *:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body, h1, h2, h3, h4, h5, h6, p, ol, ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#layout {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
#menu {
|
||||
padding: 20px;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
background-color: #ccc;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#main {
|
||||
flex-basis: 0;
|
||||
flex-grow: 3;
|
||||
background-color:#eee;
|
||||
}
|
||||
|
||||
#title, #content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#menuLink {
|
||||
position: fixed;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 1.4em;
|
||||
background: #000;
|
||||
background: rgba(0,0,0,0.7);
|
||||
font-size: 2rem;
|
||||
z-index: 10;
|
||||
height: auto;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 48em) {
|
||||
#layout.active #menu {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#layout.active #main {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#layout.active #menuLink::after {
|
||||
content: "«";
|
||||
}
|
||||
|
||||
#menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#menuLink::after {
|
||||
content: "»";
|
||||
}
|
||||
}
|
@ -39,6 +39,7 @@ const keysRoute = require('./api/keys');
|
||||
const backendLogRoute = require('./api/backend_log');
|
||||
const statsRoute = require('./api/stats');
|
||||
const fontsRoute = require('./api/fonts');
|
||||
const shareRoutes = require('../share/routes');
|
||||
|
||||
const log = require('../services/log');
|
||||
const express = require('express');
|
||||
@ -366,6 +367,8 @@ function register(app) {
|
||||
|
||||
route(GET, '/api/fonts', [auth.checkApiAuthOrElectron], fontsRoute.getFontCss);
|
||||
|
||||
shareRoutes.register(router);
|
||||
|
||||
app.use('', router);
|
||||
}
|
||||
|
||||
|
58
src/share/routes.js
Normal file
58
src/share/routes.js
Normal file
@ -0,0 +1,58 @@
|
||||
const shaca = require("./shaca/shaca");
|
||||
const shacaLoader = require("./shaca/shaca_loader");
|
||||
const shareRoot = require("./share_root");
|
||||
|
||||
function getSubRoot(note) {
|
||||
if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parentNote = note.getParentNotes()[0];
|
||||
|
||||
if (parentNote.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
return note;
|
||||
}
|
||||
|
||||
return getSubRoot(parentNote);
|
||||
}
|
||||
|
||||
function register(router) {
|
||||
router.get('/share/:noteId', (req, res, next) => {
|
||||
const {noteId} = req.params;
|
||||
|
||||
shacaLoader.ensureLoad();
|
||||
|
||||
if (noteId in shaca.notes) {
|
||||
const note = shaca.notes[noteId];
|
||||
|
||||
const subRoot = getSubRoot(note);
|
||||
|
||||
res.render("share", {
|
||||
note,
|
||||
subRoot
|
||||
});
|
||||
}
|
||||
else {
|
||||
res.send("FFF");
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
|
||||
const image = shaca.getNote(req.params.noteId);
|
||||
|
||||
if (!image) {
|
||||
return res.sendStatus(404);
|
||||
}
|
||||
else if (image.type !== 'image') {
|
||||
return res.sendStatus(400);
|
||||
}
|
||||
|
||||
res.set('Content-Type', image.mime);
|
||||
|
||||
res.send(image.getContent());
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
register
|
||||
}
|
13
src/share/shaca/entities/abstract_entity.js
Normal file
13
src/share/shaca/entities/abstract_entity.js
Normal file
@ -0,0 +1,13 @@
|
||||
let shaca;
|
||||
|
||||
class AbstractEntity {
|
||||
get shaca() {
|
||||
if (!shaca) {
|
||||
shaca = require("../shaca");
|
||||
}
|
||||
|
||||
return shaca;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AbstractEntity;
|
90
src/share/shaca/entities/attribute.js
Normal file
90
src/share/shaca/entities/attribute.js
Normal file
@ -0,0 +1,90 @@
|
||||
"use strict";
|
||||
|
||||
const AbstractEntity = require('./abstract_entity');
|
||||
|
||||
class Attribute extends AbstractEntity {
|
||||
constructor([attributeId, noteId, type, name, value, isInheritable, position]) {
|
||||
super();
|
||||
|
||||
/** @param {string} */
|
||||
this.attributeId = attributeId;
|
||||
/** @param {string} */
|
||||
this.noteId = noteId;
|
||||
/** @param {string} */
|
||||
this.type = type;
|
||||
/** @param {string} */
|
||||
this.name = name;
|
||||
/** @param {int} */
|
||||
this.position = position;
|
||||
/** @param {string} */
|
||||
this.value = value;
|
||||
/** @param {boolean} */
|
||||
this.isInheritable = !!isInheritable;
|
||||
|
||||
this.shaca.attributes[this.attributeId] = this;
|
||||
this.shaca.notes[this.noteId].ownedAttributes.push(this);
|
||||
|
||||
const targetNote = this.targetNote;
|
||||
|
||||
if (targetNote) {
|
||||
targetNote.targetRelations.push(this);
|
||||
}
|
||||
|
||||
if (this.type === 'relation' && this.name === 'imageLink') {
|
||||
const linkedChildNote = this.note.getChildNotes().find(childNote => childNote.noteId === this.value);
|
||||
|
||||
if (linkedChildNote) {
|
||||
this.note.children = this.note.children.filter(childNote => childNote.noteId !== this.value);
|
||||
|
||||
linkedChildNote.parents = linkedChildNote.parents.filter(parentNote => parentNote.noteId !== this.noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get isAffectingSubtree() {
|
||||
return this.isInheritable
|
||||
|| (this.type === 'relation' && this.name === 'template');
|
||||
}
|
||||
|
||||
get targetNoteId() { // alias
|
||||
return this.type === 'relation' ? this.value : undefined;
|
||||
}
|
||||
|
||||
isAutoLink() {
|
||||
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
|
||||
}
|
||||
|
||||
get note() {
|
||||
return this.shaca.notes[this.noteId];
|
||||
}
|
||||
|
||||
get targetNote() {
|
||||
if (this.type === 'relation') {
|
||||
return this.shaca.notes[this.value];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Note|null}
|
||||
*/
|
||||
getNote() {
|
||||
return this.shaca.getNote(this.noteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Note|null}
|
||||
*/
|
||||
getTargetNote() {
|
||||
if (this.type !== 'relation') {
|
||||
throw new Error(`Attribute ${this.attributeId} is not relation`);
|
||||
}
|
||||
|
||||
if (!this.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.shaca.getNote(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Attribute;
|
65
src/share/shaca/entities/branch.js
Normal file
65
src/share/shaca/entities/branch.js
Normal file
@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
|
||||
const AbstractEntity = require('./abstract_entity');
|
||||
const shareRoot = require("../../share_root");
|
||||
|
||||
class Branch extends AbstractEntity {
|
||||
constructor([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded]) {
|
||||
super();
|
||||
|
||||
/** @param {string} */
|
||||
this.branchId = branchId;
|
||||
/** @param {string} */
|
||||
this.noteId = noteId;
|
||||
/** @param {string} */
|
||||
this.parentNoteId = parentNoteId;
|
||||
/** @param {string} */
|
||||
this.prefix = prefix;
|
||||
/** @param {int} */
|
||||
this.notePosition = notePosition;
|
||||
/** @param {boolean} */
|
||||
this.isExpanded = !!isExpanded;
|
||||
|
||||
if (this.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
const childNote = this.childNote;
|
||||
const parentNote = this.parentNote;
|
||||
|
||||
if (!childNote.parents.includes(parentNote)) {
|
||||
childNote.parents.push(parentNote);
|
||||
}
|
||||
|
||||
if (!childNote.parentBranches.includes(this)) {
|
||||
childNote.parentBranches.push(this);
|
||||
}
|
||||
|
||||
if (!parentNote) {
|
||||
console.log(this);
|
||||
}
|
||||
|
||||
if (!parentNote.children.includes(childNote)) {
|
||||
parentNote.children.push(childNote);
|
||||
}
|
||||
|
||||
this.shaca.branches[this.branchId] = this;
|
||||
this.shaca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
|
||||
}
|
||||
|
||||
/** @return {Note} */
|
||||
get childNote() {
|
||||
return this.shaca.notes[this.noteId];
|
||||
}
|
||||
|
||||
getNote() {
|
||||
return this.childNote;
|
||||
}
|
||||
|
||||
/** @return {Note} */
|
||||
get parentNote() {
|
||||
return this.shaca.notes[this.parentNoteId];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Branch;
|
562
src/share/shaca/entities/note.js
Normal file
562
src/share/shaca/entities/note.js
Normal file
@ -0,0 +1,562 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('../../sql');
|
||||
const utils = require('../../../services/utils');
|
||||
const AbstractEntity = require('./abstract_entity');
|
||||
|
||||
const LABEL = 'label';
|
||||
const RELATION = 'relation';
|
||||
|
||||
class Note extends AbstractEntity {
|
||||
constructor([noteId, title, type, mime]) {
|
||||
super();
|
||||
|
||||
/** @param {string} */
|
||||
this.noteId = noteId;
|
||||
/** @param {string} */
|
||||
this.title = title;
|
||||
/** @param {string} */
|
||||
this.type = type;
|
||||
/** @param {string} */
|
||||
this.mime = mime;
|
||||
|
||||
/** @param {Branch[]} */
|
||||
this.parentBranches = [];
|
||||
/** @param {Note[]} */
|
||||
this.parents = [];
|
||||
/** @param {Note[]} */
|
||||
this.children = [];
|
||||
/** @param {Attribute[]} */
|
||||
this.ownedAttributes = [];
|
||||
|
||||
/** @param {Attribute[]|null} */
|
||||
this.__attributeCache = null;
|
||||
/** @param {Attribute[]|null} */
|
||||
this.inheritableAttributeCache = null;
|
||||
|
||||
/** @param {Attribute[]} */
|
||||
this.targetRelations = [];
|
||||
|
||||
this.shaca.notes[this.noteId] = this;
|
||||
|
||||
/** @param {Note[]|null} */
|
||||
this.ancestorCache = null;
|
||||
}
|
||||
|
||||
getParentBranches() {
|
||||
return this.parentBranches;
|
||||
}
|
||||
|
||||
getBranches() {
|
||||
return this.parentBranches;
|
||||
}
|
||||
|
||||
getParentNotes() {
|
||||
return this.parents;
|
||||
}
|
||||
|
||||
getChildNotes() {
|
||||
return this.children;
|
||||
}
|
||||
|
||||
hasChildren() {
|
||||
return this.children && this.children.length > 0;
|
||||
}
|
||||
|
||||
getChildBranches() {
|
||||
return this.children.map(childNote => this.shaca.getBranchFromChildAndParent(childNote.noteId, this.noteId));
|
||||
}
|
||||
|
||||
getContent(silentNotFoundError = false) {
|
||||
const row = sql.getRow(`SELECT content FROM note_contents WHERE noteId = ?`, [this.noteId]);
|
||||
|
||||
if (!row) {
|
||||
if (silentNotFoundError) {
|
||||
return undefined;
|
||||
}
|
||||
else {
|
||||
throw new Error("Cannot find note content for noteId=" + this.noteId);
|
||||
}
|
||||
}
|
||||
|
||||
let content = row.content;
|
||||
|
||||
if (this.isStringNote()) {
|
||||
return content === null
|
||||
? ""
|
||||
: content.toString("UTF-8");
|
||||
}
|
||||
else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns {*} */
|
||||
getJsonContent() {
|
||||
const content = this.getContent();
|
||||
|
||||
if (!content || !content.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is of application/json content type */
|
||||
isJson() {
|
||||
return this.mime === "application/json";
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is JavaScript (code or attachment) */
|
||||
isJavaScript() {
|
||||
return (this.type === "code" || this.type === "file")
|
||||
&& (this.mime.startsWith("application/javascript")
|
||||
|| this.mime === "application/x-javascript"
|
||||
|| this.mime === "text/javascript");
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if this note is HTML */
|
||||
isHtml() {
|
||||
return ["code", "file", "render"].includes(this.type)
|
||||
&& this.mime === "text/html";
|
||||
}
|
||||
|
||||
/** @returns {boolean} true if the note has string content (not binary) */
|
||||
isStringNote() {
|
||||
return utils.isStringNote(this.type, this.mime);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [type] - (optional) attribute type to filter
|
||||
* @param {string} [name] - (optional) attribute name to filter
|
||||
* @returns {Attribute[]} all note's attributes, including inherited ones
|
||||
*/
|
||||
getAttributes(type, name) {
|
||||
this.__getAttributes([]);
|
||||
|
||||
if (type && name) {
|
||||
return this.__attributeCache.filter(attr => attr.type === type && attr.name === name);
|
||||
}
|
||||
else if (type) {
|
||||
return this.__attributeCache.filter(attr => attr.type === type);
|
||||
}
|
||||
else if (name) {
|
||||
return this.__attributeCache.filter(attr => attr.name === name);
|
||||
}
|
||||
else {
|
||||
return this.__attributeCache.slice();
|
||||
}
|
||||
}
|
||||
|
||||
__getAttributes(path) {
|
||||
if (path.includes(this.noteId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.__attributeCache) {
|
||||
const parentAttributes = this.ownedAttributes.slice();
|
||||
const newPath = [...path, this.noteId];
|
||||
|
||||
if (this.noteId !== 'root') {
|
||||
for (const parentNote of this.parents) {
|
||||
parentAttributes.push(...parentNote.__getInheritableAttributes(newPath));
|
||||
}
|
||||
}
|
||||
|
||||
const templateAttributes = [];
|
||||
|
||||
for (const ownedAttr of parentAttributes) { // parentAttributes so we process also inherited templates
|
||||
if (ownedAttr.type === 'relation' && ownedAttr.name === 'template') {
|
||||
const templateNote = this.shaca.notes[ownedAttr.value];
|
||||
|
||||
if (templateNote) {
|
||||
templateAttributes.push(...templateNote.__getAttributes(newPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.__attributeCache = [];
|
||||
|
||||
const addedAttributeIds = new Set();
|
||||
|
||||
for (const attr of parentAttributes.concat(templateAttributes)) {
|
||||
if (!addedAttributeIds.has(attr.attributeId)) {
|
||||
addedAttributeIds.add(attr.attributeId);
|
||||
|
||||
this.__attributeCache.push(attr);
|
||||
}
|
||||
}
|
||||
|
||||
this.inheritableAttributeCache = [];
|
||||
|
||||
for (const attr of this.__attributeCache) {
|
||||
if (attr.isInheritable) {
|
||||
this.inheritableAttributeCache.push(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.__attributeCache;
|
||||
}
|
||||
|
||||
/** @return {Attribute[]} */
|
||||
__getInheritableAttributes(path) {
|
||||
if (path.includes(this.noteId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.inheritableAttributeCache) {
|
||||
this.__getAttributes(path); // will refresh also this.inheritableAttributeCache
|
||||
}
|
||||
|
||||
return this.inheritableAttributeCache;
|
||||
}
|
||||
|
||||
hasAttribute(type, name) {
|
||||
return !!this.getAttributes().find(attr => attr.type === type && attr.name === name);
|
||||
}
|
||||
|
||||
getAttributeCaseInsensitive(type, name, value) {
|
||||
name = name.toLowerCase();
|
||||
value = value ? value.toLowerCase() : null;
|
||||
|
||||
return this.getAttributes().find(
|
||||
attr => attr.type === type
|
||||
&& attr.name.toLowerCase() === name
|
||||
&& (!value || attr.value.toLowerCase() === value));
|
||||
}
|
||||
|
||||
getRelationTarget(name) {
|
||||
const relation = this.getAttributes().find(attr => attr.type === 'relation' && attr.name === name);
|
||||
|
||||
return relation ? relation.targetNote : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {boolean} true if label exists (including inherited)
|
||||
*/
|
||||
hasLabel(name) { return this.hasAttribute(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {boolean} true if label exists (excluding inherited)
|
||||
*/
|
||||
hasOwnedLabel(name) { return this.hasOwnedAttribute(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {boolean} true if relation exists (including inherited)
|
||||
*/
|
||||
hasRelation(name) { return this.hasAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {boolean} true if relation exists (excluding inherited)
|
||||
*/
|
||||
hasOwnedRelation(name) { return this.hasOwnedAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {Attribute|null} label if it exists, null otherwise
|
||||
*/
|
||||
getLabel(name) { return this.getAttribute(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {Attribute|null} label if it exists, null otherwise
|
||||
*/
|
||||
getOwnedLabel(name) { return this.getOwnedAttribute(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {Attribute|null} relation if it exists, null otherwise
|
||||
*/
|
||||
getRelation(name) { return this.getAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {Attribute|null} relation if it exists, null otherwise
|
||||
*/
|
||||
getOwnedRelation(name) { return this.getOwnedAttribute(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {string|null} label value if label exists, null otherwise
|
||||
*/
|
||||
getLabelValue(name) { return this.getAttributeValue(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - label name
|
||||
* @returns {string|null} label value if label exists, null otherwise
|
||||
*/
|
||||
getOwnedLabelValue(name) { return this.getOwnedAttributeValue(LABEL, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {string|null} relation value if relation exists, null otherwise
|
||||
*/
|
||||
getRelationValue(name) { return this.getAttributeValue(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} name - relation name
|
||||
* @returns {string|null} relation value if relation exists, null otherwise
|
||||
*/
|
||||
getOwnedRelationValue(name) { return this.getOwnedAttributeValue(RELATION, name); }
|
||||
|
||||
/**
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {boolean} true if note has an attribute with given type and name (excluding inherited)
|
||||
*/
|
||||
hasOwnedAttribute(type, name) {
|
||||
return !!this.getOwnedAttribute(type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {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.
|
||||
*/
|
||||
getAttribute(type, name) {
|
||||
const attributes = this.getAttributes();
|
||||
|
||||
return attributes.find(attr => attr.type === type && attr.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {string|null} attribute value of given type and name or null if no such attribute exists.
|
||||
*/
|
||||
getAttributeValue(type, name) {
|
||||
const attr = this.getAttribute(type, name);
|
||||
|
||||
return attr ? attr.value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} type - attribute type (label, relation, etc.)
|
||||
* @param {string} name - attribute name
|
||||
* @returns {string|null} attribute value of given type and name or null if no such attribute exists.
|
||||
*/
|
||||
getOwnedAttributeValue(type, name) {
|
||||
const attr = this.getOwnedAttribute(type, name);
|
||||
|
||||
return attr ? attr.value : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - label name to filter
|
||||
* @returns {Attribute[]} all note's labels (attributes with type label), including inherited ones
|
||||
*/
|
||||
getLabels(name) {
|
||||
return this.getAttributes(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - label name to filter
|
||||
* @returns {string[]} all note's label values, including inherited ones
|
||||
*/
|
||||
getLabelValues(name) {
|
||||
return this.getLabels(name).map(l => l.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - label name to filter
|
||||
* @returns {Attribute[]} all note's labels (attributes with type label), excluding inherited ones
|
||||
*/
|
||||
getOwnedLabels(name) {
|
||||
return this.getOwnedAttributes(LABEL, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - label name to filter
|
||||
* @returns {string[]} all note's label values, excluding inherited ones
|
||||
*/
|
||||
getOwnedLabelValues(name) {
|
||||
return this.getOwnedAttributes(LABEL, name).map(l => l.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - relation name to filter
|
||||
* @returns {Attribute[]} all note's relations (attributes with type relation), including inherited ones
|
||||
*/
|
||||
getRelations(name) {
|
||||
return this.getAttributes(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [name] - relation name to filter
|
||||
* @returns {Attribute[]} all note's relations (attributes with type relation), excluding inherited ones
|
||||
*/
|
||||
getOwnedRelations(name) {
|
||||
return this.getOwnedAttributes(RELATION, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} [type] - (optional) attribute type to filter
|
||||
* @param {string} [name] - (optional) attribute name to filter
|
||||
* @returns {Attribute[]} note's "owned" attributes - excluding inherited ones
|
||||
*/
|
||||
getOwnedAttributes(type, name) {
|
||||
// it's a common mistake to include # or ~ into attribute name
|
||||
if (name && ["#", "~"].includes(name[0])) {
|
||||
name = name.substr(1);
|
||||
}
|
||||
|
||||
if (type && name) {
|
||||
return this.ownedAttributes.filter(attr => attr.type === type && attr.name === name);
|
||||
}
|
||||
else if (type) {
|
||||
return this.ownedAttributes.filter(attr => attr.type === type);
|
||||
}
|
||||
else if (name) {
|
||||
return this.ownedAttributes.filter(attr => attr.name === name);
|
||||
}
|
||||
else {
|
||||
return this.ownedAttributes.slice();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Attribute} attribute belonging to this specific note (excludes inherited attributes)
|
||||
*
|
||||
* This method can be significantly faster than the getAttribute()
|
||||
*/
|
||||
getOwnedAttribute(type, name) {
|
||||
const attrs = this.getOwnedAttributes(type, name);
|
||||
|
||||
return attrs.length > 0 ? attrs[0] : null;
|
||||
}
|
||||
|
||||
get isArchived() {
|
||||
return this.hasAttribute('label', 'archived');
|
||||
}
|
||||
|
||||
hasInheritableOwnedArchivedLabel() {
|
||||
return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable);
|
||||
}
|
||||
|
||||
// will sort the parents so that non-search & non-archived are first and archived at the end
|
||||
// this is done so that non-search & non-archived paths are always explored as first when looking for note path
|
||||
resortParents() {
|
||||
this.parentBranches.sort((a, b) =>
|
||||
a.branchId.startsWith('virt-')
|
||||
|| a.parentNote.hasInheritableOwnedArchivedLabel() ? 1 : -1);
|
||||
|
||||
this.parents = this.parentBranches.map(branch => branch.parentNote);
|
||||
}
|
||||
|
||||
isTemplate() {
|
||||
return !!this.targetRelations.find(rel => rel.name === 'template');
|
||||
}
|
||||
|
||||
/** @return {Note[]} */
|
||||
getSubtreeNotesIncludingTemplated() {
|
||||
const arr = [[this]];
|
||||
|
||||
for (const childNote of this.children) {
|
||||
arr.push(childNote.getSubtreeNotesIncludingTemplated());
|
||||
}
|
||||
|
||||
for (const targetRelation of this.targetRelations) {
|
||||
if (targetRelation.name === 'template') {
|
||||
const note = targetRelation.note;
|
||||
|
||||
if (note) {
|
||||
arr.push(note.getSubtreeNotesIncludingTemplated());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arr.flat();
|
||||
}
|
||||
|
||||
/** @return {Note[]} */
|
||||
getSubtreeNotes(includeArchived = true) {
|
||||
const noteSet = new Set();
|
||||
|
||||
function addSubtreeNotesInner(note) {
|
||||
if (!includeArchived && note.isArchived) {
|
||||
return;
|
||||
}
|
||||
|
||||
noteSet.add(note);
|
||||
|
||||
for (const childNote of note.children) {
|
||||
addSubtreeNotesInner(childNote);
|
||||
}
|
||||
}
|
||||
|
||||
addSubtreeNotesInner(this);
|
||||
|
||||
return Array.from(noteSet);
|
||||
}
|
||||
|
||||
/** @return {String[]} */
|
||||
getSubtreeNoteIds() {
|
||||
return this.getSubtreeNotes().map(note => note.noteId);
|
||||
}
|
||||
|
||||
getDescendantNoteIds() {
|
||||
return this.getSubtreeNoteIds();
|
||||
}
|
||||
|
||||
getAncestors() {
|
||||
if (!this.ancestorCache) {
|
||||
const noteIds = new Set();
|
||||
this.ancestorCache = [];
|
||||
|
||||
for (const parent of this.parents) {
|
||||
if (!noteIds.has(parent.noteId)) {
|
||||
this.ancestorCache.push(parent);
|
||||
noteIds.add(parent.noteId);
|
||||
}
|
||||
|
||||
for (const ancestorNote of parent.getAncestors()) {
|
||||
if (!noteIds.has(ancestorNote.noteId)) {
|
||||
this.ancestorCache.push(ancestorNote);
|
||||
noteIds.add(ancestorNote.noteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this.ancestorCache;
|
||||
}
|
||||
|
||||
getTargetRelations() {
|
||||
return this.targetRelations;
|
||||
}
|
||||
|
||||
/** @return {Note[]} - returns only notes which are templated, does not include their subtrees
|
||||
* in effect returns notes which are influenced by note's non-inheritable attributes */
|
||||
getTemplatedNotes() {
|
||||
const arr = [this];
|
||||
|
||||
for (const targetRelation of this.targetRelations) {
|
||||
if (targetRelation.name === 'template') {
|
||||
const note = targetRelation.note;
|
||||
|
||||
if (note) {
|
||||
arr.push(note);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ancestorNoteId
|
||||
* @return {boolean} - true if ancestorNoteId occurs in at least one of the note's paths
|
||||
*/
|
||||
isDescendantOfNote(ancestorNoteId) {
|
||||
const notePaths = this.getAllNotePaths();
|
||||
|
||||
return notePaths.some(path => path.includes(ancestorNoteId));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Note;
|
75
src/share/shaca/shaca.js
Normal file
75
src/share/shaca/shaca.js
Normal file
@ -0,0 +1,75 @@
|
||||
"use strict";
|
||||
|
||||
class Shaca {
|
||||
constructor() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
reset() {
|
||||
/** @type {Object.<String, Note>} */
|
||||
this.notes = {};
|
||||
/** @type {Object.<String, Branch>} */
|
||||
this.branches = {};
|
||||
/** @type {Object.<String, Branch>} */
|
||||
this.childParentToBranch = {};
|
||||
/** @type {Object.<String, Attribute>} */
|
||||
this.attributes = {};
|
||||
|
||||
this.loaded = false;
|
||||
}
|
||||
|
||||
getNote(noteId) {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
getNotes(noteIds, ignoreMissing = false) {
|
||||
const filteredNotes = [];
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (!note) {
|
||||
if (ignoreMissing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Note '${noteId}' was not found in becca.`);
|
||||
}
|
||||
|
||||
filteredNotes.push(note);
|
||||
}
|
||||
|
||||
return filteredNotes;
|
||||
}
|
||||
|
||||
getBranch(branchId) {
|
||||
return this.branches[branchId];
|
||||
}
|
||||
|
||||
getAttribute(attributeId) {
|
||||
return this.attributes[attributeId];
|
||||
}
|
||||
|
||||
getBranchFromChildAndParent(childNoteId, parentNoteId) {
|
||||
return this.childParentToBranch[`${childNoteId}-${parentNoteId}`];
|
||||
}
|
||||
|
||||
getEntity(entityName, entityId) {
|
||||
if (!entityName || !entityId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const camelCaseEntityName = entityName.toLowerCase().replace(/(_[a-z])/g,
|
||||
group =>
|
||||
group
|
||||
.toUpperCase()
|
||||
.replace('_', '')
|
||||
);
|
||||
|
||||
return this[camelCaseEntityName][entityId];
|
||||
}
|
||||
}
|
||||
|
||||
const shaca = new Shaca();
|
||||
|
||||
module.exports = shaca;
|
64
src/share/shaca/shaca_loader.js
Normal file
64
src/share/shaca/shaca_loader.js
Normal file
@ -0,0 +1,64 @@
|
||||
"use strict";
|
||||
|
||||
const sql = require('../sql');
|
||||
const shaca = require('./shaca.js');
|
||||
const log = require('../../services/log');
|
||||
const Note = require('./entities/note');
|
||||
const Branch = require('./entities/branch');
|
||||
const Attribute = require('./entities/attribute');
|
||||
const shareRoot = require('../share_root');
|
||||
|
||||
function load() {
|
||||
const start = Date.now();
|
||||
shaca.reset();
|
||||
|
||||
// using raw query and passing arrays to avoid allocating new objects
|
||||
|
||||
const noteIds = sql.getColumn(`
|
||||
WITH RECURSIVE
|
||||
tree(noteId) AS (
|
||||
SELECT ?
|
||||
UNION
|
||||
SELECT branches.noteId FROM branches
|
||||
JOIN tree ON branches.parentNoteId = tree.noteId
|
||||
WHERE branches.isDeleted = 0
|
||||
)
|
||||
SELECT noteId FROM tree`, [shareRoot.SHARE_ROOT_NOTE_ID]);
|
||||
|
||||
if (noteIds.length === 0) {
|
||||
shaca.loaded = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const noteIdStr = noteIds.map(noteId => `'${noteId}'`).join(",");
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT noteId, title, type, mime FROM notes WHERE isDeleted = 0 AND noteId IN (${noteIdStr})`)) {
|
||||
new Note(row);
|
||||
}
|
||||
|
||||
for (const row of sql.getRawRows(`SELECT branchId, noteId, parentNoteId, prefix, notePosition, isExpanded, utcDateModified FROM branches WHERE isDeleted = 0 AND noteId IN (${noteIdStr})`)) {
|
||||
new Branch(row);
|
||||
}
|
||||
|
||||
// TODO: add filter for allowed attributes
|
||||
for (const row of sql.getRawRows(`SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified FROM attributes WHERE isDeleted = 0 AND noteId IN (${noteIdStr})`, [])) {
|
||||
new Attribute(row);
|
||||
}
|
||||
|
||||
shaca.loaded = true;
|
||||
|
||||
log.info(`Shaca load took ${Date.now() - start}ms`);
|
||||
}
|
||||
|
||||
function ensureLoad() {
|
||||
if (!shaca.loaded) {
|
||||
load();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
load,
|
||||
ensureLoad
|
||||
};
|
3
src/share/share_root.js
Normal file
3
src/share/share_root.js
Normal file
@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
SHARE_ROOT_NOTE_ID: 'root'
|
||||
}
|
167
src/share/sql.js
Normal file
167
src/share/sql.js
Normal file
@ -0,0 +1,167 @@
|
||||
"use strict";
|
||||
|
||||
const log = require('../services/log');
|
||||
const Database = require('better-sqlite3');
|
||||
const dataDir = require('../services/data_dir');
|
||||
|
||||
const dbConnection = new Database(dataDir.DOCUMENT_PATH, { readonly: true });
|
||||
|
||||
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach(eventType => {
|
||||
process.on(eventType, () => {
|
||||
if (dbConnection) {
|
||||
// closing connection is especially important to fold -wal file into the main DB file
|
||||
// (see https://sqlite.org/tempfiles.html for details)
|
||||
dbConnection.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const statementCache = {};
|
||||
|
||||
function stmt(sql) {
|
||||
if (!(sql in statementCache)) {
|
||||
statementCache[sql] = dbConnection.prepare(sql);
|
||||
}
|
||||
|
||||
return statementCache[sql];
|
||||
}
|
||||
|
||||
function getRow(query, params = []) {
|
||||
return wrap(query, s => s.get(params));
|
||||
}
|
||||
|
||||
function getRowOrNull(query, params = []) {
|
||||
const all = getRows(query, params);
|
||||
|
||||
return all.length > 0 ? all[0] : null;
|
||||
}
|
||||
|
||||
function getValue(query, params = []) {
|
||||
const row = getRowOrNull(query, params);
|
||||
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return row[Object.keys(row)[0]];
|
||||
}
|
||||
|
||||
// smaller values can result in better performance due to better usage of statement cache
|
||||
const PARAM_LIMIT = 100;
|
||||
|
||||
function getManyRows(query, params) {
|
||||
let results = [];
|
||||
|
||||
while (params.length > 0) {
|
||||
const curParams = params.slice(0, Math.min(params.length, PARAM_LIMIT));
|
||||
params = params.slice(curParams.length);
|
||||
|
||||
const curParamsObj = {};
|
||||
|
||||
let j = 1;
|
||||
for (const param of curParams) {
|
||||
curParamsObj['param' + j++] = param;
|
||||
}
|
||||
|
||||
let i = 1;
|
||||
const questionMarks = curParams.map(() => ":param" + i++).join(",");
|
||||
const curQuery = query.replace(/\?\?\?/g, questionMarks);
|
||||
|
||||
const statement = curParams.length === PARAM_LIMIT
|
||||
? stmt(curQuery)
|
||||
: dbConnection.prepare(curQuery);
|
||||
|
||||
const subResults = statement.all(curParamsObj);
|
||||
results = results.concat(subResults);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function getRows(query, params = []) {
|
||||
return wrap(query, s => s.all(params));
|
||||
}
|
||||
|
||||
function getRawRows(query, params = []) {
|
||||
return wrap(query, s => s.raw().all(params));
|
||||
}
|
||||
|
||||
function iterateRows(query, params = []) {
|
||||
return stmt(query).iterate(params);
|
||||
}
|
||||
|
||||
function getMap(query, params = []) {
|
||||
const map = {};
|
||||
const results = getRows(query, params);
|
||||
|
||||
for (const row of results) {
|
||||
const keys = Object.keys(row);
|
||||
|
||||
map[row[keys[0]]] = row[keys[1]];
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function getColumn(query, params = []) {
|
||||
const list = [];
|
||||
const result = getRows(query, params);
|
||||
|
||||
if (result.length === 0) {
|
||||
return list;
|
||||
}
|
||||
|
||||
const key = Object.keys(result[0])[0];
|
||||
|
||||
for (const row of result) {
|
||||
list.push(row[key]);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function wrap(query, func) {
|
||||
const startTimestamp = Date.now();
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = func(stmt(query));
|
||||
}
|
||||
catch (e) {
|
||||
if (e.message.includes("The database connection is not open")) {
|
||||
// this often happens on killing the app which puts these alerts in front of user
|
||||
// in these cases error should be simply ignored.
|
||||
console.log(e.message);
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
const milliseconds = Date.now() - startTimestamp;
|
||||
|
||||
if (milliseconds >= 20) {
|
||||
if (query.includes("WITH RECURSIVE")) {
|
||||
log.info(`Slow recursive query took ${milliseconds}ms.`);
|
||||
}
|
||||
else {
|
||||
log.info(`Slow query took ${milliseconds}ms: ${query.trim().replace(/\s+/g, " ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dbConnection,
|
||||
getValue,
|
||||
getRow,
|
||||
getRowOrNull,
|
||||
getRows,
|
||||
getRawRows,
|
||||
iterateRows,
|
||||
getManyRows,
|
||||
getMap,
|
||||
getColumn
|
||||
};
|
17
src/views/share-tree-item.ejs
Normal file
17
src/views/share-tree-item.ejs
Normal file
@ -0,0 +1,17 @@
|
||||
<p>
|
||||
<% if (activeNote.noteId === note.noteId) { %>
|
||||
<strong><%= note.title %></strong>
|
||||
<% } else { %>
|
||||
<a href="./<%= note.noteId %>"><%= note.title %></a>
|
||||
<% } %>
|
||||
</p>
|
||||
|
||||
<% if (note.hasChildren()) { %>
|
||||
<ul>
|
||||
<% note.getChildNotes().forEach(function (childNote) { %>
|
||||
<li>
|
||||
<%- include('share-tree-item', {note: childNote}) %>
|
||||
</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
<% } %>
|
90
src/views/share.ejs
Normal file
90
src/views/share.ejs
Normal file
@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="shortcut icon" href="../favicon.ico">
|
||||
<link href="../stylesheets/share.css" rel="stylesheet">
|
||||
<% if (note.type === 'text' || note.type === 'book') { %>
|
||||
<link href="../libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
|
||||
<% } %>
|
||||
<title><%= note.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="layout">
|
||||
<button id="menuLink"></button>
|
||||
|
||||
<div id="menu">
|
||||
<div class="pure-menu">
|
||||
<%- include('share-tree-item', {note: subRoot, activeNote: note}) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="main">
|
||||
<h1 id="title"><%= note.title %></h1>
|
||||
|
||||
<div id="content">
|
||||
<% if (note.type === 'text') { %>
|
||||
<div class="ck-content"><%- note.getContent() %></div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function (window, document) {
|
||||
|
||||
// we fetch the elements each time because docusaurus removes the previous
|
||||
// element references on page navigation
|
||||
function getElements() {
|
||||
return {
|
||||
layout: document.getElementById('layout'),
|
||||
menu: document.getElementById('menu'),
|
||||
menuLink: document.getElementById('menuLink')
|
||||
};
|
||||
}
|
||||
|
||||
function toggleClass(element, className) {
|
||||
var classes = element.className.split(/\s+/);
|
||||
var length = classes.length;
|
||||
var i = 0;
|
||||
|
||||
for (; i < length; i++) {
|
||||
if (classes[i] === className) {
|
||||
classes.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The className is not found
|
||||
if (length === classes.length) {
|
||||
classes.push(className);
|
||||
}
|
||||
|
||||
element.className = classes.join(' ');
|
||||
}
|
||||
|
||||
function toggleAll() {
|
||||
var active = 'active';
|
||||
var elements = getElements();
|
||||
|
||||
toggleClass(elements.layout, active);
|
||||
toggleClass(elements.menu, active);
|
||||
toggleClass(elements.menuLink, active);
|
||||
}
|
||||
|
||||
function handleEvent(e) {
|
||||
var elements = getElements();
|
||||
|
||||
if (e.target.id === elements.menuLink.id) {
|
||||
toggleAll();
|
||||
e.preventDefault();
|
||||
} else if (elements.menu.className.indexOf('active') !== -1) {
|
||||
toggleAll();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', handleEvent);
|
||||
|
||||
}(this, this.document));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user