mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 03:46:37 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/next49'
This commit is contained in:
		
							
								
								
									
										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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user