mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
sharing WIP
This commit is contained in:
parent
2cc4367b37
commit
a14aa461ca
@ -3,6 +3,9 @@
|
|||||||
const sql = require("../services/sql.js");
|
const sql = require("../services/sql.js");
|
||||||
const NoteSet = require("../services/search/note_set");
|
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 {
|
class Becca {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.reset();
|
this.reset();
|
||||||
|
@ -6,12 +6,14 @@ import appContext from "./app_context.js";
|
|||||||
import NoteComplement from "../entities/note_complement.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
|
* - 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
|
* - 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.
|
* - 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.
|
* Note and branch deletions are corner cases and usually not needed.
|
||||||
|
*
|
||||||
|
* Backend has a similar cache called Becca
|
||||||
*/
|
*/
|
||||||
class Froca {
|
class Froca {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
112
src/share/entities/attribute.js
Normal file
112
src/share/entities/attribute.js
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Note = require('./note.js');
|
||||||
|
const sql = require("../sql.js");
|
||||||
|
|
||||||
|
class Attribute {
|
||||||
|
constructor(row) {
|
||||||
|
this.updateFromRow(row);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromRow(row) {
|
||||||
|
this.update([
|
||||||
|
row.attributeId,
|
||||||
|
row.noteId,
|
||||||
|
row.type,
|
||||||
|
row.name,
|
||||||
|
row.value,
|
||||||
|
row.isInheritable,
|
||||||
|
row.position
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
update([attributeId, noteId, type, name, value, isInheritable, position]) {
|
||||||
|
/** @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;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.attributeId) {
|
||||||
|
this.becca.attributes[this.attributeId] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(this.noteId in this.becca.notes)) {
|
||||||
|
// entities can come out of order in sync, create skeleton which will be filled later
|
||||||
|
this.becca.addNote(this.noteId, new Note({noteId: this.noteId}));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.becca.notes[this.noteId].ownedAttributes.push(this);
|
||||||
|
|
||||||
|
const key = `${this.type}-${this.name.toLowerCase()}`;
|
||||||
|
this.becca.attributeIndex[key] = this.becca.attributeIndex[key] || [];
|
||||||
|
this.becca.attributeIndex[key].push(this);
|
||||||
|
|
||||||
|
const targetNote = this.targetNote;
|
||||||
|
|
||||||
|
if (targetNote) {
|
||||||
|
targetNote.targetRelations.push(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.becca.notes[this.noteId];
|
||||||
|
}
|
||||||
|
|
||||||
|
get targetNote() {
|
||||||
|
if (this.type === 'relation') {
|
||||||
|
return this.becca.notes[this.value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Note|null}
|
||||||
|
*/
|
||||||
|
getNote() {
|
||||||
|
return this.becca.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.becca.getNote(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Attribute;
|
90
src/share/entities/branch.js
Normal file
90
src/share/entities/branch.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Note = require('./note.js');
|
||||||
|
const sql = require("../sql.js");
|
||||||
|
|
||||||
|
class Branch {
|
||||||
|
constructor(row) {
|
||||||
|
this.updateFromRow(row);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromRow(row) {
|
||||||
|
this.update([
|
||||||
|
row.branchId,
|
||||||
|
row.noteId,
|
||||||
|
row.parentNoteId,
|
||||||
|
row.prefix,
|
||||||
|
row.notePosition,
|
||||||
|
row.isExpanded,
|
||||||
|
row.utcDateModified
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
update([branchId, noteId, parentNoteId, prefix, notePosition, isExpanded]) {
|
||||||
|
/** @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;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (this.branchId === 'root') {
|
||||||
|
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.children.includes(childNote)) {
|
||||||
|
parentNote.children.push(childNote);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.becca.branches[this.branchId] = this;
|
||||||
|
this.becca.childParentToBranch[`${this.noteId}-${this.parentNoteId}`] = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return {Note} */
|
||||||
|
get childNote() {
|
||||||
|
if (!(this.noteId in this.becca.notes)) {
|
||||||
|
// entities can come out of order in sync, create skeleton which will be filled later
|
||||||
|
this.becca.addNote(this.noteId, new Note({noteId: this.noteId}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.becca.notes[this.noteId];
|
||||||
|
}
|
||||||
|
|
||||||
|
getNote() {
|
||||||
|
return this.childNote;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return {Note} */
|
||||||
|
get parentNote() {
|
||||||
|
if (!(this.parentNoteId in this.becca.notes)) {
|
||||||
|
// entities can come out of order in sync, create skeleton which will be filled later
|
||||||
|
this.becca.addNote(this.parentNoteId, new Note({noteId: this.parentNoteId}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.becca.notes[this.parentNoteId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Branch;
|
577
src/share/entities/note.js
Normal file
577
src/share/entities/note.js
Normal file
@ -0,0 +1,577 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const sql = require('../sql');
|
||||||
|
const utils = require('../../services/utils');
|
||||||
|
|
||||||
|
const LABEL = 'label';
|
||||||
|
const RELATION = 'relation';
|
||||||
|
|
||||||
|
class Note {
|
||||||
|
constructor(row) {
|
||||||
|
this.updateFromRow(row);
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromRow(row) {
|
||||||
|
this.update([
|
||||||
|
row.noteId,
|
||||||
|
row.title,
|
||||||
|
row.type,
|
||||||
|
row.mime
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
update([noteId, title, type, mime]) {
|
||||||
|
/** @param {string} */
|
||||||
|
this.noteId = noteId;
|
||||||
|
/** @param {string} */
|
||||||
|
this.title = title;
|
||||||
|
/** @param {string} */
|
||||||
|
this.type = type;
|
||||||
|
/** @param {string} */
|
||||||
|
this.mime = mime;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
/** @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.becca.addNote(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.becca.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.becca.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;
|
207
src/share/shaca/shaca_loader.js
Normal file
207
src/share/shaca/shaca_loader.js
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const sql = require('../services/sql');
|
||||||
|
const eventService = require('../services/events');
|
||||||
|
const shaca = require('./shaca.js');
|
||||||
|
const sqlInit = require('../services/sql_init');
|
||||||
|
const log = require('../services/log');
|
||||||
|
const Note = require('./entities/note');
|
||||||
|
const Branch = require('./entities/branch');
|
||||||
|
const Attribute = require('./entities/attribute');
|
||||||
|
const Option = require('./entities/option');
|
||||||
|
const entityConstructor = require("../becca/entity_constructor");
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
const start = Date.now();
|
||||||
|
shaca.reset();
|
||||||
|
|
||||||
|
// 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`, [])) {
|
||||||
|
new Note().update(row).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
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`, [])) {
|
||||||
|
new Attribute().update(row).init();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of sql.getRows(`SELECT name, value, isSynced, utcDateModified FROM options`)) {
|
||||||
|
new Option(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
shaca.loaded = true;
|
||||||
|
|
||||||
|
log.info(`Shaca load took ${Date.now() - start}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
eventService.subscribe([eventService.ENTITY_CHANGE_SYNCED], ({entityName, entityRow}) => {
|
||||||
|
if (!becca.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (["notes", "branches", "attributes"].includes(entityName)) {
|
||||||
|
const EntityClass = entityConstructor.getEntityFromEntityName(entityName);
|
||||||
|
const primaryKeyName = EntityClass.primaryKeyName;
|
||||||
|
|
||||||
|
let beccaEntity = becca.getEntity(entityName, entityRow[primaryKeyName]);
|
||||||
|
|
||||||
|
if (beccaEntity) {
|
||||||
|
beccaEntity.updateFromRow(entityRow);
|
||||||
|
} else {
|
||||||
|
beccaEntity = new EntityClass();
|
||||||
|
beccaEntity.updateFromRow(entityRow);
|
||||||
|
beccaEntity.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
postProcessEntityUpdate(entityName, entityRow);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventService.subscribe(eventService.ENTITY_CHANGED, ({entityName, entity}) => {
|
||||||
|
if (!becca.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postProcessEntityUpdate(entityName, entity);
|
||||||
|
});
|
||||||
|
|
||||||
|
eventService.subscribe([eventService.ENTITY_DELETED, eventService.ENTITY_DELETE_SYNCED], ({entityName, entityId}) => {
|
||||||
|
if (!becca.loaded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entityName === 'notes') {
|
||||||
|
noteDeleted(entityId);
|
||||||
|
} else if (entityName === 'branches') {
|
||||||
|
branchDeleted(entityId);
|
||||||
|
} else if (entityName === 'attributes') {
|
||||||
|
attributeDeleted(entityId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function noteDeleted(noteId) {
|
||||||
|
delete becca.notes[noteId];
|
||||||
|
|
||||||
|
becca.dirtyNoteSetCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
function branchDeleted(branchId) {
|
||||||
|
const branch = becca.branches[branchId];
|
||||||
|
|
||||||
|
if (!branch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childNote = becca.notes[branch.noteId];
|
||||||
|
|
||||||
|
if (childNote) {
|
||||||
|
childNote.parents = childNote.parents.filter(parent => parent.noteId !== branch.parentNoteId);
|
||||||
|
childNote.parentBranches = childNote.parentBranches
|
||||||
|
.filter(parentBranch => parentBranch.branchId !== branch.branchId);
|
||||||
|
|
||||||
|
if (childNote.parents.length > 0) {
|
||||||
|
childNote.invalidateSubTree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentNote = becca.notes[branch.parentNoteId];
|
||||||
|
|
||||||
|
if (parentNote) {
|
||||||
|
parentNote.children = parentNote.children.filter(child => child.noteId !== branch.noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete becca.childParentToBranch[`${branch.noteId}-${branch.parentNoteId}`];
|
||||||
|
delete becca.branches[branch.branchId];
|
||||||
|
}
|
||||||
|
|
||||||
|
function branchUpdated(branch) {
|
||||||
|
const childNote = becca.notes[branch.noteId];
|
||||||
|
|
||||||
|
if (childNote) {
|
||||||
|
childNote.flatTextCache = null;
|
||||||
|
childNote.resortParents();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attributeDeleted(attributeId) {
|
||||||
|
const attribute = becca.attributes[attributeId];
|
||||||
|
|
||||||
|
if (!attribute) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = becca.notes[attribute.noteId];
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
// first invalidate and only then remove the attribute (otherwise invalidation wouldn't be complete)
|
||||||
|
if (attribute.isAffectingSubtree || note.isTemplate()) {
|
||||||
|
note.invalidateSubTree();
|
||||||
|
} else {
|
||||||
|
note.invalidateThisCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
note.ownedAttributes = note.ownedAttributes.filter(attr => attr.attributeId !== attribute.attributeId);
|
||||||
|
|
||||||
|
const targetNote = attribute.targetNote;
|
||||||
|
|
||||||
|
if (targetNote) {
|
||||||
|
targetNote.targetRelations = targetNote.targetRelations.filter(rel => rel.attributeId !== attribute.attributeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete becca.attributes[attribute.attributeId];
|
||||||
|
|
||||||
|
const key = `${attribute.type}-${attribute.name.toLowerCase()}`;
|
||||||
|
|
||||||
|
if (key in becca.attributeIndex) {
|
||||||
|
becca.attributeIndex[key] = becca.attributeIndex[key].filter(attr => attr.attributeId !== attribute.attributeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attributeUpdated(attribute) {
|
||||||
|
const note = becca.notes[attribute.noteId];
|
||||||
|
|
||||||
|
if (note) {
|
||||||
|
if (attribute.isAffectingSubtree || note.isTemplate()) {
|
||||||
|
note.invalidateSubTree();
|
||||||
|
} else {
|
||||||
|
note.invalidateThisCache();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function noteReorderingUpdated(branchIdList) {
|
||||||
|
const parentNoteIds = new Set();
|
||||||
|
|
||||||
|
for (const branchId in branchIdList) {
|
||||||
|
const branch = becca.branches[branchId];
|
||||||
|
|
||||||
|
if (branch) {
|
||||||
|
branch.notePosition = branchIdList[branchId];
|
||||||
|
|
||||||
|
parentNoteIds.add(branch.parentNoteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => {
|
||||||
|
try {
|
||||||
|
becca.decryptProtectedNotes();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
log.error(`Could not decrypt protected notes: ${e.message} ${e.stack}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
eventService.subscribe(eventService.LEAVE_PROTECTED_SESSION, load);
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
load,
|
||||||
|
reload,
|
||||||
|
beccaLoaded
|
||||||
|
};
|
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
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user